diff --git a/cmd/main.go b/cmd/main.go index 4008087..366a3cd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -60,6 +60,7 @@ import ( virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/orchestration" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor" @@ -251,6 +252,12 @@ func main() { aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) veliVirtualGameService := veli.New(virtualGameSvc, vitualGameRepo, veliCLient, walletSvc, repository.NewTransferStore(store), domain.MongoDBLogger, cfg) + orchestrationSvc := orchestration.New( + virtualGameSvc, + virtuaGamesRepo, + cfg, + veliCLient, + ) atlasClient := atlas.NewClient(cfg, walletSvc) atlasVirtualGameService := atlas.New(virtualGameSvc, vitualGameRepo, atlasClient, walletSvc, repository.NewTransferStore(store), cfg) recommendationSvc := recommendation.NewService(recommendationRepo) @@ -301,8 +308,8 @@ func main() { store, ) - go httpserver.SetupReportandVirtualGameCronJobs(context.Background(), reportSvc, veliVirtualGameService, "C:/Users/User/Desktop") go httpserver.StartEnetPulseCron(enePulseSvc, domain.MongoDBLogger) + go httpserver.SetupReportandVirtualGameCronJobs(context.Background(), reportSvc, orchestrationSvc, "C:/Users/User/Desktop") go httpserver.ProcessBetCashback(context.TODO(), betSvc) bankRepository := repository.NewBankRepository(store) @@ -339,9 +346,9 @@ func main() { fixerFertcherSvc, ) - exchangeWorker := currency.NewExchangeRateWorker(fixerFertcherSvc, logger, cfg) - exchangeWorker.Start(context.Background()) - defer exchangeWorker.Stop() + // exchangeWorker := currency.NewExchangeRateWorker(fixerFertcherSvc, logger, cfg) + // exchangeWorker.Start(context.Background()) + // defer exchangeWorker.Stop() go walletMonitorSvc.Start() httpserver.StartBetAPIDataFetchingCrons(eventSvc, *oddsSvc, resultSvc, domain.MongoDBLogger) @@ -369,6 +376,7 @@ func main() { enetPulseSvc, atlasVirtualGameService, veliVirtualGameService, + orchestrationSvc, telebirrSvc, arifpaySvc, santimpaySvc, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 37f53b7..2529dc5 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -36,9 +36,27 @@ CREATE TABLE IF NOT EXISTS virtual_game_providers ( created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ ); + +CREATE TABLE IF NOT EXISTS virtual_game_provider_reports ( + id BIGSERIAL PRIMARY KEY, + provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE, + report_date DATE NOT NULL, + total_games_played BIGINT DEFAULT 0, + total_bets NUMERIC(18,2) DEFAULT 0, + total_payouts NUMERIC(18,2) DEFAULT 0, + total_profit NUMERIC(18,2) GENERATED ALWAYS AS (total_bets - total_payouts) STORED, + total_players BIGINT DEFAULT 0, + report_type VARCHAR(50) DEFAULT 'daily', + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_provider_report +ON virtual_game_provider_reports (provider_id, report_date, report_type); + CREATE TABLE IF NOT EXISTS virtual_games ( id BIGSERIAL PRIMARY KEY, - game_id VARCHAR(150) NOT NULL, + game_id VARCHAR(150) UNIQUE NOT NULL, provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, category VARCHAR(100), @@ -54,6 +72,25 @@ CREATE TABLE IF NOT EXISTS virtual_games ( updated_at TIMESTAMPTZ ); CREATE UNIQUE INDEX IF NOT EXISTS ux_virtual_games_provider_game ON virtual_games (provider_id, game_id); + +CREATE TABLE IF NOT EXISTS virtual_game_reports ( + id BIGSERIAL PRIMARY KEY, + game_id VARCHAR(150) NOT NULL REFERENCES virtual_games(game_id) ON DELETE CASCADE, + provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE, + report_date DATE NOT NULL, + total_rounds BIGINT DEFAULT 0, + total_bets NUMERIC(18,2) DEFAULT 0, + total_payouts NUMERIC(18,2) DEFAULT 0, + total_profit NUMERIC(18,2) GENERATED ALWAYS AS (total_bets - total_payouts) STORED, + total_players BIGINT DEFAULT 0, + report_type VARCHAR(50) DEFAULT 'daily', + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_game_report +ON virtual_game_reports (game_id, report_date, report_type); + CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, balance BIGINT NOT NULL DEFAULT 0, @@ -234,6 +271,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer ( cashier_id BIGINT, verified BOOLEAN DEFAULT false, reference_number VARCHAR(255) NOT NULL, + ext_reference_number VARCHAR(255), session_id VARCHAR(255), status VARCHAR(255), payment_method VARCHAR(255), diff --git a/db/migrations/000004_virtual_game_Session.up.sql b/db/migrations/000004_virtual_game_Session.up.sql index 2dc5ed2..b9fbd25 100644 --- a/db/migrations/000004_virtual_game_Session.up.sql +++ b/db/migrations/000004_virtual_game_Session.up.sql @@ -3,11 +3,8 @@ CREATE TABLE virtual_game_sessions ( user_id BIGINT NOT NULL REFERENCES users(id), game_id VARCHAR(50) NOT NULL, session_token VARCHAR(255) NOT NULL UNIQUE, - currency VARCHAR(3) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, COMPLETED, FAILED created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP WITH TIME ZONE NOT NULL + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE virtual_game_transactions ( diff --git a/db/migrations/000011_enet_pulse.down.sql b/db/migrations/000011_enet_pulse.down.sql deleted file mode 100644 index 10f25f6..0000000 --- a/db/migrations/000011_enet_pulse.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE IF EXISTS enetpulse_sports; -DROP TABLE IF EXISTS enetpulse_tournament_templates; \ No newline at end of file diff --git a/db/migrations/000011_enet_pulse.up.sql b/db/migrations/000011_enet_pulse.up.sql deleted file mode 100644 index 7b52a4d..0000000 --- a/db/migrations/000011_enet_pulse.up.sql +++ /dev/null @@ -1,64 +0,0 @@ -CREATE TABLE IF NOT EXISTS enetpulse_sports ( - id BIGSERIAL PRIMARY KEY, - sport_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" - name VARCHAR(255) NOT NULL, -- from API "name" - updates_count INT DEFAULT 0, -- from API "n" - last_updated_at TIMESTAMPTZ, -- from API "ut" - status INT DEFAULT 1, -- optional status (active/inactive) - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ -); - -CREATE TABLE IF NOT EXISTS enetpulse_tournament_templates ( - id BIGSERIAL PRIMARY KEY, - template_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" - name VARCHAR(255) NOT NULL, -- from API "name" - sport_fk VARCHAR(50) NOT NULL REFERENCES enetpulse_sports(sport_id) ON DELETE CASCADE, - gender VARCHAR(20) DEFAULT 'unknown', -- from API "gender" {male, female, mixed, unknown} - updates_count INT DEFAULT 0, -- from API "n" - last_updated_at TIMESTAMPTZ, -- from API "ut" - status INT DEFAULT 1, -- optional status (active/inactive) - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ -); - -CREATE TABLE IF NOT EXISTS enetpulse_tournaments ( - id BIGSERIAL PRIMARY KEY, - tournament_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" - name VARCHAR(255) NOT NULL, -- from API "name" - - -- Link to the template it belongs to: - tournament_template_fk VARCHAR(50) NOT NULL - REFERENCES enetpulse_tournament_templates(template_id) ON DELETE CASCADE, - - updates_count INT DEFAULT 0, -- from API "n" - last_updated_at TIMESTAMPTZ, -- from API "ut" - status INT DEFAULT 1, -- optional active/inactive flag - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ -); - --- Create table for tournament stages -CREATE TABLE IF NOT EXISTS enetpulse_tournament_stages ( - id BIGSERIAL PRIMARY KEY, - stage_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" - name VARCHAR(255) NOT NULL, -- from API "name" - tournament_fk VARCHAR(50) NOT NULL REFERENCES enetpulse_tournaments(tournament_id) ON DELETE CASCADE, - -- from API "tournamentFK" - gender VARCHAR(20) DEFAULT 'unknown', -- from API "gender" {male, female, mixed, unknown} - country_fk VARCHAR(50), -- from API "countryFK" - country_name VARCHAR(255), -- from API "country_name" - start_date TIMESTAMPTZ, -- from API "startdate" - end_date TIMESTAMPTZ, -- from API "enddate" - updates_count INT DEFAULT 0, -- from API "n" - last_updated_at TIMESTAMPTZ, -- from API "ut" - status INT DEFAULT 1, -- optional status (active/inactive) - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ -); - --- Index to quickly query by tournament_fk -CREATE INDEX IF NOT EXISTS idx_enetpulse_tournament_stages_tournament_fk - ON enetpulse_tournament_stages (tournament_fk); - - diff --git a/db/migrations/00008_enet_pulse.down.sql b/db/migrations/00008_enet_pulse.down.sql index 10f25f6..43196f1 100644 --- a/db/migrations/00008_enet_pulse.down.sql +++ b/db/migrations/00008_enet_pulse.down.sql @@ -1,2 +1,11 @@ DROP TABLE IF EXISTS enetpulse_sports; -DROP TABLE IF EXISTS enetpulse_tournament_templates; \ No newline at end of file +DROP TABLE IF EXISTS enetpulse_tournament_templates; +DROP TABLE IF EXISTS enetpulse_tournaments; +DROP TABLE IF EXISTS enetpulse_tournament_stages; +DROP TABLE IF EXISTS enetpulse_fixtures; +DROP TABLE IF EXISTS enetpulse_results; +DROP TABLE IF EXISTS enetpulse_result_participants; +DROP TABLE IF EXISTS enetpulse_result_referees; +DROP TABLE IF EXISTS enetpulse_outcome_types; +DROP TABLE IF EXISTS enetpulse_preodds; +DROP TABLE IF EXISTS enetpulse_preodds_bettingoffers; \ No newline at end of file diff --git a/db/migrations/00008_enet_pulse.up.sql b/db/migrations/00008_enet_pulse.up.sql index 059db33..416bddf 100644 --- a/db/migrations/00008_enet_pulse.up.sql +++ b/db/migrations/00008_enet_pulse.up.sql @@ -21,3 +21,207 @@ CREATE TABLE IF NOT EXISTS enetpulse_tournament_templates ( created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ ); + +CREATE TABLE IF NOT EXISTS enetpulse_tournaments ( + id BIGSERIAL PRIMARY KEY, + tournament_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" + name VARCHAR(255) NOT NULL, -- from API "name" + + -- Link to the template it belongs to: + tournament_template_fk VARCHAR(50) NOT NULL + REFERENCES enetpulse_tournament_templates(template_id) ON DELETE CASCADE, + + updates_count INT DEFAULT 0, -- from API "n" + last_updated_at TIMESTAMPTZ, -- from API "ut" + status INT DEFAULT 1, -- optional active/inactive flag + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS enetpulse_tournament_stages ( + id BIGSERIAL PRIMARY KEY, + stage_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" + name VARCHAR(255) NOT NULL, -- from API "name" + tournament_fk VARCHAR(50) NOT NULL REFERENCES enetpulse_tournaments(tournament_id) ON DELETE CASCADE, + -- from API "tournamentFK" + gender VARCHAR(20) DEFAULT 'unknown', -- from API "gender" {male, female, mixed, unknown} + country_fk VARCHAR(50), -- from API "countryFK" + country_name VARCHAR(255), -- from API "country_name" + start_date TIMESTAMPTZ, -- from API "startdate" + end_date TIMESTAMPTZ, -- from API "enddate" + updates_count INT DEFAULT 0, -- from API "n" + last_updated_at TIMESTAMPTZ, -- from API "ut" + status INT DEFAULT 1, -- optional status (active/inactive) + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_enetpulse_tournament_stages_tournament_fk + ON enetpulse_tournament_stages (tournament_fk); + +CREATE TABLE IF NOT EXISTS enetpulse_fixtures ( + id BIGSERIAL PRIMARY KEY, + fixture_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" + name VARCHAR(255) NOT NULL, -- fixture name (e.g. 12 de Junio-Sol de America) + + sport_fk VARCHAR(50) NOT NULL REFERENCES enetpulse_sports(sport_id) ON DELETE CASCADE, + tournament_fk VARCHAR(50), -- raw tournamentFK (optional) + tournament_template_fk VARCHAR(50) REFERENCES enetpulse_tournament_templates(template_id) ON DELETE CASCADE, + -- tournament_stage_fk VARCHAR(50) REFERENCES enetpulse_tournament_stages(stage_id) ON DELETE CASCADE, + + -- tournament_stage_name VARCHAR(255), + tournament_name VARCHAR(255), + tournament_template_name VARCHAR(255), + sport_name VARCHAR(255), + gender VARCHAR(20), + + start_date TIMESTAMPTZ NOT NULL, -- startdate + status_type VARCHAR(50), -- Not started, Live, Finished + status_desc_fk VARCHAR(50), -- status_descFK + round_type_fk VARCHAR(50), -- round_typeFK + updates_count INT DEFAULT 0, -- n + last_updated_at TIMESTAMPTZ, -- ut + + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS enetpulse_results ( + id BIGSERIAL PRIMARY KEY, + result_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" + name VARCHAR(255) NOT NULL, -- event name (e.g. Brentford-Manchester City) + + sport_fk VARCHAR(50) NOT NULL REFERENCES enetpulse_sports(sport_id) ON DELETE CASCADE, + tournament_fk VARCHAR(50), + tournament_template_fk VARCHAR(50) REFERENCES enetpulse_tournament_templates(template_id) ON DELETE CASCADE, + -- tournament_stage_fk VARCHAR(50) REFERENCES enetpulse_tournament_stages(stage_id) ON DELETE CASCADE, + + -- tournament_stage_name VARCHAR(255), + tournament_name VARCHAR(255), + tournament_template_name VARCHAR(255), + sport_name VARCHAR(255), + + start_date TIMESTAMPTZ NOT NULL, -- startdate + status_type VARCHAR(50), -- e.g. Finished + status_desc_fk VARCHAR(50), -- status_descFK + round_type_fk VARCHAR(50), -- round_typeFK + updates_count INT DEFAULT 0, -- n + last_updated_at TIMESTAMPTZ, -- ut + + -- Optional metadata (dynamic but common fields are included) + round VARCHAR(50), + live VARCHAR(10), + venue_name VARCHAR(255), + livestats_plus VARCHAR(10), + livestats_type VARCHAR(50), + commentary VARCHAR(50), + lineup_confirmed BOOLEAN, + verified BOOLEAN, + spectators INT, + + -- Time-related metadata + game_started TIMESTAMPTZ, + first_half_ended TIMESTAMPTZ, + second_half_started TIMESTAMPTZ, + second_half_ended TIMESTAMPTZ, + game_ended TIMESTAMPTZ, + + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +-- Event Participants (teams involved in the result) +CREATE TABLE IF NOT EXISTS enetpulse_result_participants ( + id BIGSERIAL PRIMARY KEY, + participant_map_id VARCHAR(50) NOT NULL UNIQUE, -- from event_participants.*.id + result_fk VARCHAR(50) NOT NULL REFERENCES enetpulse_results(result_id) ON DELETE CASCADE, + participant_fk VARCHAR(50) NOT NULL, -- team/player FK (from API participantFK) + number INT, -- 1 or 2 (home/away indicator) + name VARCHAR(255), -- team/player name + gender VARCHAR(20), + type VARCHAR(50), + country_fk VARCHAR(50), + country_name VARCHAR(100), + + -- Result details + ordinary_time VARCHAR(10), + running_score VARCHAR(10), + halftime VARCHAR(10), + final_result VARCHAR(10), + + last_updated_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Referees / Match officials +CREATE TABLE IF NOT EXISTS enetpulse_result_referees ( + id BIGSERIAL PRIMARY KEY, + result_fk VARCHAR(50) NOT NULL REFERENCES enetpulse_results(result_id) ON DELETE CASCADE, + referee_fk VARCHAR(50), + assistant1_referee_fk VARCHAR(50), + assistant2_referee_fk VARCHAR(50), + fourth_referee_fk VARCHAR(50), + var1_referee_fk VARCHAR(50), + var2_referee_fk VARCHAR(50), + last_updated_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS enetpulse_outcome_types ( + id BIGSERIAL PRIMARY KEY, + outcome_type_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" + name VARCHAR(100) NOT NULL, -- e.g. "1x2" + description VARCHAR(255), -- e.g. "1x2 - 3Way" + + updates_count INT DEFAULT 0, -- maps to "n" + last_updated_at TIMESTAMPTZ, -- maps to "ut" + + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +-- Table for pre-match odds (main outcome entries) +CREATE TABLE IF NOT EXISTS enetpulse_preodds ( + id BIGSERIAL PRIMARY KEY, + preodds_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" + event_fk BIGINT NOT NULL, -- maps to objectFK/event + outcome_type_fk INT, -- outcome_typeFK + outcome_scope_fk INT, -- outcome_scopeFK + outcome_subtype_fk INT, -- outcome_subtypeFK + event_participant_number INT, -- event_participant_number + iparam VARCHAR(50), + iparam2 VARCHAR(50), + dparam VARCHAR(50), + dparam2 VARCHAR(50), + sparam VARCHAR(50), + + updates_count INT DEFAULT 0, -- maps to "n" + last_updated_at TIMESTAMPTZ, -- maps to "ut" + + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +-- Table for nested betting offers within preodds +CREATE TABLE IF NOT EXISTS enetpulse_preodds_bettingoffers ( + id BIGSERIAL PRIMARY KEY, + bettingoffer_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" + preodds_fk VARCHAR(50) NOT NULL REFERENCES enetpulse_preodds(preodds_id) ON DELETE CASCADE, + bettingoffer_status_fk INT, -- bettingoffer_statusFK + odds_provider_fk INT, -- odds_providerFK + odds NUMERIC(10,4), -- current odds + odds_old NUMERIC(10,4), -- previous odds + active BOOLEAN, -- maps "yes"/"no" to boolean + coupon_key VARCHAR(255), -- couponKey + updates_count INT DEFAULT 0, -- maps to "n" + last_updated_at TIMESTAMPTZ, -- maps to "ut" + + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + + + + + + diff --git a/db/query/enet_pulse.sql b/db/query/enet_pulse.sql index dd1d010..f586d46 100644 --- a/db/query/enet_pulse.sql +++ b/db/query/enet_pulse.sql @@ -68,6 +68,9 @@ SELECT FROM enetpulse_tournament_templates ORDER BY name; +-- -- name: DeleteEnetpulseTournamentTemplateByID :exec +-- DELETE FROM enetpulse_tournament_templates WHERE template_id = $1; + -- name: CreateEnetpulseTournament :one INSERT INTO enetpulse_tournaments ( tournament_id, @@ -104,7 +107,8 @@ INSERT INTO enetpulse_tournament_stages ( updates_count, last_updated_at, status -) VALUES ( +) +VALUES ( $1, -- stage_id $2, -- name $3, -- tournament_fk @@ -117,6 +121,19 @@ INSERT INTO enetpulse_tournament_stages ( $10, -- last_updated_at $11 -- status ) +ON CONFLICT (stage_id) DO UPDATE +SET + name = EXCLUDED.name, + tournament_fk = EXCLUDED.tournament_fk, + gender = EXCLUDED.gender, + country_fk = EXCLUDED.country_fk, + country_name = EXCLUDED.country_name, + start_date = EXCLUDED.start_date, + end_date = EXCLUDED.end_date, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + status = EXCLUDED.status, + updated_at = NOW() RETURNING *; -- name: GetAllEnetpulseTournamentStages :many @@ -130,5 +147,391 @@ FROM enetpulse_tournament_stages WHERE tournament_fk = $1 ORDER BY created_at DESC; +-- name: CreateEnetpulseFixture :one +INSERT INTO enetpulse_fixtures ( + fixture_id, + name, + sport_fk, + tournament_fk, + tournament_template_fk, + tournament_name, + tournament_template_name, + sport_name, + gender, + start_date, + status_type, + status_desc_fk, + round_type_fk, + updates_count, + last_updated_at, + created_at, + updated_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP +) +ON CONFLICT (fixture_id) DO UPDATE +SET + name = EXCLUDED.name, + sport_fk = EXCLUDED.sport_fk, + tournament_fk = EXCLUDED.tournament_fk, + tournament_template_fk = EXCLUDED.tournament_template_fk, + tournament_name = EXCLUDED.tournament_name, + tournament_template_name = EXCLUDED.tournament_template_name, + sport_name = EXCLUDED.sport_name, + gender = EXCLUDED.gender, + start_date = EXCLUDED.start_date, + status_type = EXCLUDED.status_type, + status_desc_fk = EXCLUDED.status_desc_fk, + round_type_fk = EXCLUDED.round_type_fk, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + updated_at = CURRENT_TIMESTAMP +RETURNING *; + + +-- name: GetAllEnetpulseFixtures :many +SELECT * +FROM enetpulse_fixtures +ORDER BY created_at DESC; + +-- name: CreateEnetpulseResult :one +INSERT INTO enetpulse_results ( + result_id, + name, + sport_fk, + tournament_fk, + tournament_template_fk, + tournament_name, + tournament_template_name, + sport_name, + start_date, + status_type, + status_desc_fk, + round_type_fk, + updates_count, + last_updated_at, + round, + live, + venue_name, + livestats_plus, + livestats_type, + commentary, + lineup_confirmed, + verified, + spectators, + game_started, + first_half_ended, + second_half_started, + second_half_ended, + game_ended, + created_at, + updated_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10, + $11,$12,$13,$14,$15,$16,$17,$18, + $19,$20,$21,$22,$23,$24,$25,$26, + $27,$28,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP +) +ON CONFLICT (result_id) DO UPDATE +SET + name = EXCLUDED.name, + sport_fk = EXCLUDED.sport_fk, + tournament_fk = EXCLUDED.tournament_fk, + tournament_template_fk = EXCLUDED.tournament_template_fk, + tournament_name = EXCLUDED.tournament_name, + tournament_template_name = EXCLUDED.tournament_template_name, + sport_name = EXCLUDED.sport_name, + start_date = EXCLUDED.start_date, + status_type = EXCLUDED.status_type, + status_desc_fk = EXCLUDED.status_desc_fk, + round_type_fk = EXCLUDED.round_type_fk, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + round = EXCLUDED.round, + live = EXCLUDED.live, + venue_name = EXCLUDED.venue_name, + livestats_plus = EXCLUDED.livestats_plus, + livestats_type = EXCLUDED.livestats_type, + commentary = EXCLUDED.commentary, + lineup_confirmed = EXCLUDED.lineup_confirmed, + verified = EXCLUDED.verified, + spectators = EXCLUDED.spectators, + game_started = EXCLUDED.game_started, + first_half_ended = EXCLUDED.first_half_ended, + second_half_started = EXCLUDED.second_half_started, + second_half_ended = EXCLUDED.second_half_ended, + game_ended = EXCLUDED.game_ended, + updated_at = CURRENT_TIMESTAMP +RETURNING *; + +-- name: GetAllEnetpulseResults :many +SELECT * +FROM enetpulse_results +ORDER BY created_at DESC; + +-- name: CreateEnetpulseResultParticipant :one +INSERT INTO enetpulse_result_participants ( + participant_map_id, + result_fk, + participant_fk, + number, + name, + gender, + type, + country_fk, + country_name, + ordinary_time, + running_score, + halftime, + final_result, + last_updated_at, + created_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,CURRENT_TIMESTAMP +) +ON CONFLICT (participant_map_id) DO UPDATE +SET + result_fk = EXCLUDED.result_fk, + participant_fk = EXCLUDED.participant_fk, + number = EXCLUDED.number, + name = EXCLUDED.name, + gender = EXCLUDED.gender, + type = EXCLUDED.type, + country_fk = EXCLUDED.country_fk, + country_name = EXCLUDED.country_name, + ordinary_time = EXCLUDED.ordinary_time, + running_score = EXCLUDED.running_score, + halftime = EXCLUDED.halftime, + final_result = EXCLUDED.final_result, + last_updated_at = EXCLUDED.last_updated_at +RETURNING *; + +-- name: GetEnetpulseResultParticipantsByResultFK :many +SELECT * +FROM enetpulse_result_participants +WHERE result_fk = $1 +ORDER BY created_at DESC; + +-- name: CreateEnetpulseResultReferee :one +INSERT INTO enetpulse_result_referees ( + result_fk, + referee_fk, + assistant1_referee_fk, + assistant2_referee_fk, + fourth_referee_fk, + var1_referee_fk, + var2_referee_fk, + last_updated_at, + created_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,CURRENT_TIMESTAMP +) +ON CONFLICT (result_fk) DO UPDATE +SET + referee_fk = EXCLUDED.referee_fk, + assistant1_referee_fk = EXCLUDED.assistant1_referee_fk, + assistant2_referee_fk = EXCLUDED.assistant2_referee_fk, + fourth_referee_fk = EXCLUDED.fourth_referee_fk, + var1_referee_fk = EXCLUDED.var1_referee_fk, + var2_referee_fk = EXCLUDED.var2_referee_fk, + last_updated_at = EXCLUDED.last_updated_at +RETURNING *; + +-- name: GetEnetpulseResultRefereesByResultFK :many +SELECT * +FROM enetpulse_result_referees +WHERE result_fk = $1 +ORDER BY created_at DESC; + +-- name: CreateEnetpulseOutcomeType :one +INSERT INTO enetpulse_outcome_types ( + outcome_type_id, + name, + description, + updates_count, + last_updated_at, + created_at, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP +) +ON CONFLICT (outcome_type_id) DO UPDATE +SET + name = EXCLUDED.name, + description = EXCLUDED.description, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + updated_at = CURRENT_TIMESTAMP +RETURNING *; + +-- name: GetAllEnetpulseOutcomeTypes :many +SELECT * +FROM enetpulse_outcome_types +ORDER BY created_at DESC; + +-- name: CreateEnetpulsePreodds :one +INSERT INTO enetpulse_preodds ( + preodds_id, + event_fk, + outcome_type_fk, + outcome_scope_fk, + outcome_subtype_fk, + event_participant_number, + iparam, + iparam2, + dparam, + dparam2, + sparam, + updates_count, + last_updated_at, + created_at, + updated_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP +) +ON CONFLICT (preodds_id) DO UPDATE +SET + event_fk = EXCLUDED.event_fk, + outcome_type_fk = EXCLUDED.outcome_type_fk, + outcome_scope_fk = EXCLUDED.outcome_scope_fk, + outcome_subtype_fk = EXCLUDED.outcome_subtype_fk, + event_participant_number = EXCLUDED.event_participant_number, + iparam = EXCLUDED.iparam, + iparam2 = EXCLUDED.iparam2, + dparam = EXCLUDED.dparam, + dparam2 = EXCLUDED.dparam2, + sparam = EXCLUDED.sparam, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + updated_at = CURRENT_TIMESTAMP +RETURNING *; + +-- name: GetAllEnetpulsePreodds :many +SELECT * +FROM enetpulse_preodds +ORDER BY created_at DESC; + + +-- name: CreateEnetpulsePreoddsBettingOffer :one +INSERT INTO enetpulse_preodds_bettingoffers ( + bettingoffer_id, + preodds_fk, + bettingoffer_status_fk, + odds_provider_fk, + odds, + odds_old, + active, + coupon_key, + updates_count, + last_updated_at, + created_at, + updated_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP +) +ON CONFLICT (bettingoffer_id) DO UPDATE +SET + preodds_fk = EXCLUDED.preodds_fk, + bettingoffer_status_fk = EXCLUDED.bettingoffer_status_fk, + odds_provider_fk = EXCLUDED.odds_provider_fk, + odds = EXCLUDED.odds, + odds_old = EXCLUDED.odds_old, + active = EXCLUDED.active, + coupon_key = EXCLUDED.coupon_key, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + updated_at = CURRENT_TIMESTAMP +RETURNING *; + +-- name: GetAllEnetpulsePreoddsBettingOffers :many +SELECT * +FROM enetpulse_preodds_bettingoffers +ORDER BY created_at DESC; + +-- name: GetAllEnetpulsePreoddsWithBettingOffers :many +SELECT + p.id AS preodds_db_id, + p.preodds_id, + p.event_fk, + p.outcome_type_fk, + p.outcome_scope_fk, + p.outcome_subtype_fk, + p.event_participant_number, + p.iparam, + p.iparam2, + p.dparam, + p.dparam2, + p.sparam, + p.updates_count AS preodds_updates_count, + p.last_updated_at AS preodds_last_updated_at, + p.created_at AS preodds_created_at, + p.updated_at AS preodds_updated_at, + + -- Betting offer fields + bo.id AS bettingoffer_db_id, + bo.bettingoffer_id, + bo.preodds_fk, -- ✅ ensure alias matches struct field + bo.bettingoffer_status_fk, + bo.odds_provider_fk, + bo.odds, + bo.odds_old, + bo.active, + bo.coupon_key, + bo.updates_count AS bettingoffer_updates_count, + bo.last_updated_at AS bettingoffer_last_updated_at, + bo.created_at AS bettingoffer_created_at, + bo.updated_at AS bettingoffer_updated_at + +FROM enetpulse_preodds p +LEFT JOIN enetpulse_preodds_bettingoffers bo + ON bo.preodds_fk = p.preodds_id +ORDER BY p.created_at DESC, bo.created_at DESC; + + +-- name: GetFixturesWithPreodds :many +SELECT + f.fixture_id AS id, + f.fixture_id AS fixture_id, + f.name AS fixture_name, + f.sport_fk, + f.tournament_fk, + f.tournament_template_fk, + f.start_date, + f.status_type, + f.status_desc_fk, + f.round_type_fk, + f.updates_count AS fixture_updates_count, + f.last_updated_at AS fixture_last_updated_at, + f.created_at AS fixture_created_at, + f.updated_at AS fixture_updated_at, + + -- Preodds fields + p.id AS preodds_db_id, + p.preodds_id, + p.event_fk, + p.outcome_type_fk, + p.outcome_scope_fk, + p.outcome_subtype_fk, + p.event_participant_number, + p.iparam, + p.iparam2, + p.dparam, + p.dparam2, + p.sparam, + p.updates_count AS preodds_updates_count, + p.last_updated_at AS preodds_last_updated_at, + p.created_at AS preodds_created_at, + p.updated_at AS preodds_updated_at + +FROM enetpulse_fixtures f +LEFT JOIN enetpulse_preodds p + ON p.event_fk = f.id +ORDER BY f.start_date DESC; + + + + + + diff --git a/db/query/transfer.sql b/db/query/transfer.sql index 0229d0f..bd44f37 100644 --- a/db/query/transfer.sql +++ b/db/query/transfer.sql @@ -8,11 +8,12 @@ INSERT INTO wallet_transfer ( cashier_id, verified, reference_number, + ext_reference_number, session_id, status, payment_method ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *; -- name: GetAllTransfers :many SELECT * diff --git a/db/query/virtual_games.sql b/db/query/virtual_games.sql index 46e5061..32b61d1 100644 --- a/db/query/virtual_games.sql +++ b/db/query/virtual_games.sql @@ -61,40 +61,40 @@ RETURNING id, updated_at; -- name: CreateVirtualGameSession :one INSERT INTO virtual_game_sessions ( - user_id, - game_id, - session_token, - currency, - status, - expires_at - ) -VALUES ($1, $2, $3, $4, $5, $6) -RETURNING id, + user_id, + game_id, + session_token +) +VALUES ($1, $2, $3) +RETURNING + id, user_id, game_id, session_token, - currency, - status, created_at, - updated_at, - expires_at; + updated_at; + +-- name: GetVirtualGameSessionByUserID :one +SELECT + id, + user_id, + game_id, + session_token, + created_at, + updated_at +FROM virtual_game_sessions +WHERE user_id = $1; + -- name: GetVirtualGameSessionByToken :one SELECT id, user_id, game_id, session_token, - currency, - status, created_at, - updated_at, - expires_at + updated_at FROM virtual_game_sessions WHERE session_token = $1; --- name: UpdateVirtualGameSessionStatus :exec -UPDATE virtual_game_sessions -SET status = $2, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1; + -- name: CreateVirtualGameTransaction :one INSERT INTO virtual_game_transactions ( session_id, @@ -287,4 +287,87 @@ WHERE ( ORDER BY vg.created_at DESC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: DeleteAllVirtualGames :exec -DELETE FROM virtual_games; \ No newline at end of file +DELETE FROM virtual_games; + + +-- name: CreateVirtualGameProviderReport :one +INSERT INTO virtual_game_provider_reports ( + provider_id, + report_date, + total_games_played, + total_bets, + total_payouts, + total_players, + report_type, + created_at, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, $6, COALESCE($7, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP +) +ON CONFLICT (provider_id, report_date, report_type) DO UPDATE +SET + total_games_played = EXCLUDED.total_games_played, + total_bets = EXCLUDED.total_bets, + total_payouts = EXCLUDED.total_payouts, + total_players = EXCLUDED.total_players, + updated_at = CURRENT_TIMESTAMP +RETURNING *; + + +-- name: CreateVirtualGameReport :one +INSERT INTO virtual_game_reports ( + game_id, + provider_id, + report_date, + total_rounds, + total_bets, + total_payouts, + total_players, + report_type, + created_at, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, COALESCE($8, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP +) +ON CONFLICT (game_id, report_date, report_type) DO UPDATE +SET + total_rounds = EXCLUDED.total_rounds, + total_bets = EXCLUDED.total_bets, + total_payouts = EXCLUDED.total_payouts, + total_players = EXCLUDED.total_players, + updated_at = CURRENT_TIMESTAMP +RETURNING *; + +-- name: GetVirtualGameProviderReportByProviderAndDate :one +SELECT * +FROM virtual_game_provider_reports +WHERE provider_id = $1 + AND report_date = $2 + AND report_type = $3; + +-- name: UpdateVirtualGameProviderReportByDate :exec +UPDATE virtual_game_provider_reports +SET + total_games_played = total_games_played + $4, + total_bets = total_bets + $5, + total_payouts = total_payouts + $6, + total_players = total_players + $7, + updated_at = CURRENT_TIMESTAMP +WHERE + provider_id = $1 + AND report_date = $2 + AND report_type = $3; + +-- name: ListVirtualGameProviderReportsByGamesPlayedAsc :many +SELECT * +FROM virtual_game_provider_reports +ORDER BY total_games_played ASC; + +-- name: ListVirtualGameProviderReportsByGamesPlayedDesc :many +SELECT * +FROM virtual_game_provider_reports +ORDER BY total_games_played DESC; + + + + diff --git a/docker-compose.yml b/docker-compose.yml index 3005b90..f8ef704 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: container_name: fortunebet-backend-postgres-1 image: postgres:16-alpine ports: - - 5422:5432 + - "5422:5432" environment: - POSTGRES_PASSWORD=secret - POSTGRES_USER=root @@ -20,12 +20,13 @@ services: volumes: - postgres_data:/var/lib/postgresql/data - ./exports:/exports + mongo: container_name: fortunebet-mongo image: mongo:7.0.11 restart: always ports: - - "27017:27017" + - "27025:27017" environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: secret @@ -38,6 +39,7 @@ services: interval: 10s timeout: 5s retries: 5 + migrate: image: migrate/migrate volumes: @@ -72,7 +74,7 @@ services: dockerfile: Dockerfile target: runner ports: - - ${PORT}:8080 + - "${PORT}:8080" environment: - DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable - MONGO_URI=mongodb://root:secret@mongo:27017 diff --git a/docs/docs.go b/docs/docs.go index 3114939..361e99b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -458,7 +458,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ArifpayB2CRequest" + "$ref": "#/definitions/domain.CheckoutSessionClientRequest" } } ], @@ -585,7 +585,7 @@ const docTemplate = `{ } } }, - "/api/v1/arifpay/checkout/{sessionId}/cancel": { + "/api/v1/arifpay/checkout/cancel/{sessionId}": { "post": { "description": "Cancels a payment session using Arifpay before completion.", "consumes": [ @@ -2085,6 +2085,49 @@ const docTemplate = `{ } } }, + "/api/v1/chapa/balances": { + "get": { + "description": "Retrieve Chapa account balance, optionally filtered by currency code (e.g., ETB, USD)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get Chapa account balance", + "parameters": [ + { + "type": "string", + "description": "Currency code (optional)", + "name": "currency_code", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/chapa/banks": { "get": { "description": "Get list of banks supported by Chapa", @@ -2165,9 +2208,9 @@ const docTemplate = `{ } } }, - "/api/v1/chapa/payments/manual/verify/{tx_ref}": { + "/api/v1/chapa/payments/receipt/{chapa_ref}": { "get": { - "description": "Manually verify a payment using Chapa's API", + "description": "Retrieve the Chapa payment receipt URL using the reference ID", "consumes": [ "application/json" ], @@ -2177,12 +2220,12 @@ const docTemplate = `{ "tags": [ "Chapa" ], - "summary": "Verify a payment manually", + "summary": "Get Chapa Payment Receipt URL", "parameters": [ { "type": "string", - "description": "Transaction Reference", - "name": "tx_ref", + "description": "Chapa Reference ID", + "name": "chapa_ref", "in": "path", "required": true } @@ -2191,7 +2234,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.ChapaVerificationResponse" + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -2211,7 +2254,7 @@ const docTemplate = `{ }, "/api/v1/chapa/payments/webhook/verify": { "post": { - "description": "Handles payment notifications from Chapa", + "description": "Handles payment and transfer notifications from Chapa", "consumes": [ "application/json" ], @@ -2229,7 +2272,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ChapaWebhookPayload" + "$ref": "#/definitions/domain.ChapaWebhookPayment" } } ], @@ -2237,8 +2280,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -2286,7 +2328,7 @@ const docTemplate = `{ } ], "responses": { - "201": { + "200": { "description": "Chapa withdrawal process initiated successfully", "schema": { "$ref": "#/definitions/domain.Response" @@ -2319,6 +2361,270 @@ const docTemplate = `{ } } }, + "/api/v1/chapa/swap": { + "post": { + "description": "Convert an amount from one currency to another using Chapa's currency swap API", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Swap currency using Chapa API", + "parameters": [ + { + "description": "Swap request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.SwapRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/chapa/transaction/cancel/{tx_ref}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Cancels an active Chapa transaction using its transaction reference", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Cancel a Chapa deposit transaction", + "parameters": [ + { + "type": "string", + "description": "Transaction Reference", + "name": "tx_ref", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ChapaCancelResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/chapa/transaction/events/{ref_id}": { + "get": { + "description": "Retrieve the timeline of events for a specific Chapa transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Fetch transaction events", + "parameters": [ + { + "type": "string", + "description": "Transaction Reference", + "name": "ref_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ChapaTransactionEvent" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/chapa/transaction/manual/verify/{tx_ref}": { + "get": { + "description": "Manually verify a payment using Chapa's API", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Verify a payment manually", + "parameters": [ + { + "type": "string", + "description": "Transaction Reference", + "name": "tx_ref", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/chapa/transactions": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Retrieves all transactions from Chapa payment gateway", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get all Chapa transactions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ChapaTransaction" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/chapa/transfers": { + "get": { + "description": "Retrieve all transfer records from Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get all Chapa transfers", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/company": { "get": { "description": "Gets all companies", @@ -3048,6 +3354,270 @@ const docTemplate = `{ } } }, + "/api/v1/enetpulse/betting-offers": { + "get": { + "description": "Fetches all EnetPulse preodds betting offers stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get all betting offers", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreoddsBettingOffer" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/fixtures": { + "get": { + "description": "Fetches all fixtures stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get all stored fixtures", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseFixture" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/fixtures/preodds": { + "get": { + "description": "Fetches all EnetPulse fixtures along with their associated pre-match odds", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get fixtures with preodds", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseFixtureWithPreodds" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/preodds": { + "get": { + "description": "Fetches all EnetPulse pre-match odds stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get all preodds", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreodds" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/preodds-with-offers": { + "get": { + "description": "Fetches all EnetPulse pre-match odds along with their associated betting offers stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get all preodds with betting offers", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreodds" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/results": { + "get": { + "description": "Fetches all EnetPulse match results stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get all results", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseResult" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/enetpulse/sports": { "get": { "description": "Fetches all sports stored in the database", @@ -3058,7 +3628,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Sports" + "EnetPulse" ], "summary": "Get all sports", "responses": { @@ -3102,7 +3672,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Tournament Stages" + "EnetPulse" ], "summary": "Get all tournament stages", "responses": { @@ -3146,7 +3716,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Tournament Templates" + "EnetPulse" ], "summary": "Get all tournament templates", "responses": { @@ -3190,7 +3760,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Tournaments" + "EnetPulse" ], "summary": "Get all tournaments", "responses": { @@ -4124,108 +4694,6 @@ const docTemplate = `{ } } }, - "/api/v1/odds/pre-match": { - "get": { - "description": "Fetches pre-match odds from EnetPulse for a given event", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "EnetPulse - PreMatch" - ], - "summary": "Get pre-match odds for an event", - "parameters": [ - { - "type": "integer", - "description": "Event ID", - "name": "objectFK", - "in": "query", - "required": true - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "csv", - "description": "Odds provider IDs (comma separated)", - "name": "oddsProviderFK", - "in": "query" - }, - { - "type": "integer", - "description": "Outcome type ID", - "name": "outcomeTypeFK", - "in": "query" - }, - { - "type": "integer", - "description": "Outcome scope ID", - "name": "outcomeScopeFK", - "in": "query" - }, - { - "type": "integer", - "description": "Outcome subtype ID", - "name": "outcomeSubtypeFK", - "in": "query" - }, - { - "type": "integer", - "description": "Limit results", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset results", - "name": "offset", - "in": "query" - }, - { - "type": "integer", - "description": "Language type ID", - "name": "languageTypeFK", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.PreMatchOddsResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, "/api/v1/odds/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", @@ -4385,6 +4853,58 @@ const docTemplate = `{ } } }, + "/api/v1/orchestrator/virtual-game/provider-reports/asc": { + "get": { + "description": "Retrieves all virtual game provider reports sorted by total_games_played in ascending order", + "tags": [ + "VirtualGames - Orchestration" + ], + "summary": "List virtual game provider reports (ascending)", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.VirtualGameProviderReport" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/orchestrator/virtual-game/provider-reports/desc": { + "get": { + "description": "Retrieves all virtual game provider reports sorted by total_games_played in descending order", + "tags": [ + "VirtualGames - Orchestration" + ], + "summary": "List virtual game provider reports (descending)", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.VirtualGameProviderReport" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/orchestrator/virtual-games": { "get": { "description": "Returns all virtual games with optional filters (category, search, pagination)", @@ -6378,6 +6898,88 @@ const docTemplate = `{ } } }, + "/api/v1/ticket": { + "get": { + "description": "Retrieve all tickets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ticket" + ], + "summary": "Get all tickets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.TicketRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/ticket/{id}": { + "get": { + "description": "Retrieve ticket details by ticket ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ticket" + ], + "summary": "Get ticket by ID", + "parameters": [ + { + "type": "integer", + "description": "Ticket ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.TicketRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/transfer/refill/:id": { "post": { "description": "Super Admin route to refill a wallet", @@ -9626,28 +10228,6 @@ const docTemplate = `{ } } }, - "domain.ArifpayB2CRequest": { - "type": "object", - "required": [ - "amount", - "customerEmail", - "customerPhone" - ], - "properties": { - "Phonenumber": { - "type": "string" - }, - "amount": { - "type": "number" - }, - "customerEmail": { - "type": "string" - }, - "customerPhone": { - "type": "string" - } - } - }, "domain.ArifpayVerifyByTransactionIDRequest": { "type": "object", "properties": { @@ -9681,7 +10261,7 @@ const docTemplate = `{ "type": "string" }, "round_id": { - "type": "integer" + "type": "string" }, "session_id": { "type": "string" @@ -9931,6 +10511,9 @@ const docTemplate = `{ }, "timerStatus": { "$ref": "#/definitions/domain.ValidString" + }, + "totalOddOutcomes": { + "type": "integer" } } }, @@ -10044,6 +10627,10 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "company_slug": { + "type": "string", + "example": "fortune" + }, "created_at": { "type": "string", "example": "2025-04-08T12:00:00Z" @@ -10247,6 +10834,52 @@ const docTemplate = `{ } } }, + "domain.ChapaCancelResponse": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tx_ref": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.ChapaCustomer": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "type": "string" + }, + "mobile": { + "type": "string" + } + } + }, "domain.ChapaDepositRequestPayload": { "type": "object", "required": [ @@ -10269,37 +10902,131 @@ const docTemplate = `{ } } }, - "domain.ChapaVerificationResponse": { + "domain.ChapaTransaction": { "type": "object", "properties": { "amount": { - "type": "number" + "type": "string" + }, + "charge": { + "type": "string" + }, + "created_at": { + "type": "string" }, "currency": { "type": "string" }, + "customer": { + "$ref": "#/definitions/domain.ChapaCustomer" + }, + "payment_method": { + "type": "string" + }, + "ref_id": { + "type": "string" + }, "status": { "type": "string" }, - "tx_ref": { + "trans_id": { + "type": "string" + }, + "type": { "type": "string" } } }, - "domain.ChapaWebhookPayload": { + "domain.ChapaTransactionEvent": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "item": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.ChapaWebhookCustomization": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "logo": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "domain.ChapaWebhookPayment": { "type": "object", "properties": { "amount": { - "type": "integer" + "type": "string" + }, + "charge": { + "type": "string" + }, + "created_at": { + "type": "string" }, "currency": { "type": "string" }, + "customization": { + "$ref": "#/definitions/domain.ChapaWebhookCustomization" + }, + "email": { + "type": "string" + }, + "event": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "meta": { + "description": "may vary in structure, so kept flexible" + }, + "mobile": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "payment_method": { + "type": "string" + }, + "reference": { + "type": "string" + }, "status": { - "$ref": "#/definitions/domain.PaymentStatus" + "type": "string" }, "tx_ref": { "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" } } }, @@ -10752,6 +11479,324 @@ const docTemplate = `{ } } }, + "domain.EnetpulseFixture": { + "type": "object", + "properties": { + "gender": { + "type": "string" + }, + "id": { + "type": "string" + }, + "n": { + "description": "convert to int", + "type": "string" + }, + "name": { + "type": "string" + }, + "round_typeFK": { + "type": "string" + }, + "sportFK": { + "type": "string" + }, + "sport_name": { + "type": "string" + }, + "startdate": { + "description": "ISO 8601", + "type": "string" + }, + "status_descFK": { + "type": "string" + }, + "status_type": { + "type": "string" + }, + "tournamentFK": { + "type": "string" + }, + "tournament_name": { + "type": "string" + }, + "tournament_stage_name": { + "description": "TournamentStageFK string ` + "`" + `json:\"tournament_stageFK\"` + "`" + `", + "type": "string" + }, + "tournament_templateFK": { + "type": "string" + }, + "tournament_template_name": { + "type": "string" + }, + "ut": { + "description": "parse to time.Time", + "type": "string" + } + } + }, + "domain.EnetpulseFixtureWithPreodds": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "fixtureApiID": { + "type": "string" + }, + "fixtureID": { + "type": "string" + }, + "fixtureName": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "preodds": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreodds" + } + }, + "roundTypeFk": { + "type": "string" + }, + "sportFk": { + "type": "string" + }, + "startDate": { + "type": "string" + }, + "statusDescFk": { + "type": "string" + }, + "statusType": { + "type": "string" + }, + "tournamentFk": { + "type": "string" + }, + "tournamentStageFk": { + "type": "string" + }, + "tournamentTemplateFk": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "updatesCount": { + "type": "integer" + } + } + }, + "domain.EnetpulsePreodds": { + "type": "object", + "properties": { + "bettingOffers": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreoddsBettingOffer" + } + }, + "createdAt": { + "type": "string" + }, + "dparam": { + "type": "string" + }, + "dparam2": { + "type": "string" + }, + "eventFK": { + "type": "integer" + }, + "eventParticipantNumber": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "iparam": { + "type": "string" + }, + "iparam2": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "outcomeScopeFK": { + "type": "integer" + }, + "outcomeSubtypeFK": { + "type": "integer" + }, + "outcomeTypeFK": { + "type": "integer" + }, + "preoddsID": { + "type": "string" + }, + "sparam": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "updatesCount": { + "type": "integer" + } + } + }, + "domain.EnetpulsePreoddsBettingOffer": { + "type": "object", + "properties": { + "active": { + "type": "string" + }, + "betting_offer_id": { + "type": "string" + }, + "betting_offer_status_fk": { + "type": "integer" + }, + "coupon_key": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_at": { + "type": "string" + }, + "odds": { + "type": "number" + }, + "odds_old": { + "type": "number" + }, + "odds_provider_fk": { + "type": "integer" + }, + "preodds_fk": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "updates_count": { + "type": "integer" + } + } + }, + "domain.EnetpulseResult": { + "type": "object", + "properties": { + "commentary": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "first_half_ended": { + "type": "string" + }, + "game_ended": { + "type": "string" + }, + "game_started": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_at": { + "type": "string" + }, + "lineup_confirmed": { + "type": "boolean" + }, + "live": { + "type": "string" + }, + "livestats_plus": { + "type": "string" + }, + "livestats_type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "result_id": { + "type": "string" + }, + "round": { + "type": "string" + }, + "round_type_fk": { + "type": "string" + }, + "second_half_ended": { + "type": "string" + }, + "second_half_started": { + "type": "string" + }, + "spectators": { + "type": "integer" + }, + "sport_fk": { + "type": "string" + }, + "sport_name": { + "type": "string" + }, + "start_date": { + "type": "string" + }, + "status_desc_fk": { + "type": "string" + }, + "status_type": { + "type": "string" + }, + "tournament_fk": { + "type": "string" + }, + "tournament_name": { + "type": "string" + }, + "tournament_stage_name": { + "description": "TournamentStageFK string ` + "`" + `json:\"tournament_stage_fk\"` + "`" + `", + "type": "string" + }, + "tournament_template_fk": { + "type": "string" + }, + "tournament_template_name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "updates_count": { + "type": "integer" + }, + "venue_name": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + } + }, "domain.EnetpulseSport": { "type": "object", "properties": { @@ -11069,6 +12114,9 @@ const docTemplate = `{ "timer_status": { "type": "string" }, + "total_odd_outcomes": { + "type": "integer" + }, "updated_at": { "type": "string" }, @@ -11796,21 +12844,6 @@ const docTemplate = `{ "BANK" ] }, - "domain.PaymentStatus": { - "type": "string", - "enum": [ - "success", - "pending", - "completed", - "failed" - ], - "x-enum-varnames": [ - "PaymentStatusSuccessful", - "PaymentStatusPending", - "PaymentStatusCompleted", - "PaymentStatusFailed" - ] - }, "domain.PopOKCallback": { "type": "object", "properties": { @@ -11862,41 +12895,6 @@ const docTemplate = `{ } } }, - "domain.PreMatchOddsResponse": { - "type": "object", - "properties": { - "eventFK": { - "description": "Define fields according to the Enetpulse preodds response structure\nExample:", - "type": "integer" - }, - "odds": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.PreMatchOutcome" - } - } - } - }, - "domain.PreMatchOutcome": { - "type": "object", - "properties": { - "oddsProviderFK": { - "type": "integer" - }, - "oddsValue": { - "type": "number" - }, - "outcomeFK": { - "type": "integer" - }, - "outcomeTypeFK": { - "type": "integer" - }, - "outcomeValue": { - "type": "string" - } - } - }, "domain.ProviderRequest": { "type": "object", "properties": { @@ -12270,7 +13268,7 @@ const docTemplate = `{ "type": "object", "properties": { "amount": { - "type": "integer" + "type": "number" }, "bet_id": { "type": "integer", @@ -12531,6 +13529,20 @@ const docTemplate = `{ } } }, + "domain.SwapRequest": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + } + } + }, "domain.TelebirrPaymentCallbackPayload": { "type": "object", "properties": { @@ -12867,6 +13879,44 @@ const docTemplate = `{ } } }, + "domain.VirtualGameProviderReport": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "provider_id": { + "type": "string" + }, + "report_date": { + "type": "string" + }, + "report_type": { + "type": "string" + }, + "total_bets": { + "type": "number" + }, + "total_games_played": { + "type": "integer" + }, + "total_payouts": { + "type": "number" + }, + "total_players": { + "type": "integer" + }, + "total_profit": { + "type": "number" + }, + "updated_at": { + "type": "string" + } + } + }, "domain.WebhookRequest": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index bae0014..fa4aba2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -450,7 +450,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ArifpayB2CRequest" + "$ref": "#/definitions/domain.CheckoutSessionClientRequest" } } ], @@ -577,7 +577,7 @@ } } }, - "/api/v1/arifpay/checkout/{sessionId}/cancel": { + "/api/v1/arifpay/checkout/cancel/{sessionId}": { "post": { "description": "Cancels a payment session using Arifpay before completion.", "consumes": [ @@ -2077,6 +2077,49 @@ } } }, + "/api/v1/chapa/balances": { + "get": { + "description": "Retrieve Chapa account balance, optionally filtered by currency code (e.g., ETB, USD)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get Chapa account balance", + "parameters": [ + { + "type": "string", + "description": "Currency code (optional)", + "name": "currency_code", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/chapa/banks": { "get": { "description": "Get list of banks supported by Chapa", @@ -2157,9 +2200,9 @@ } } }, - "/api/v1/chapa/payments/manual/verify/{tx_ref}": { + "/api/v1/chapa/payments/receipt/{chapa_ref}": { "get": { - "description": "Manually verify a payment using Chapa's API", + "description": "Retrieve the Chapa payment receipt URL using the reference ID", "consumes": [ "application/json" ], @@ -2169,12 +2212,12 @@ "tags": [ "Chapa" ], - "summary": "Verify a payment manually", + "summary": "Get Chapa Payment Receipt URL", "parameters": [ { "type": "string", - "description": "Transaction Reference", - "name": "tx_ref", + "description": "Chapa Reference ID", + "name": "chapa_ref", "in": "path", "required": true } @@ -2183,7 +2226,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.ChapaVerificationResponse" + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -2203,7 +2246,7 @@ }, "/api/v1/chapa/payments/webhook/verify": { "post": { - "description": "Handles payment notifications from Chapa", + "description": "Handles payment and transfer notifications from Chapa", "consumes": [ "application/json" ], @@ -2221,7 +2264,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ChapaWebhookPayload" + "$ref": "#/definitions/domain.ChapaWebhookPayment" } } ], @@ -2229,8 +2272,7 @@ "200": { "description": "OK", "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -2278,7 +2320,7 @@ } ], "responses": { - "201": { + "200": { "description": "Chapa withdrawal process initiated successfully", "schema": { "$ref": "#/definitions/domain.Response" @@ -2311,6 +2353,270 @@ } } }, + "/api/v1/chapa/swap": { + "post": { + "description": "Convert an amount from one currency to another using Chapa's currency swap API", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Swap currency using Chapa API", + "parameters": [ + { + "description": "Swap request payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.SwapRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/chapa/transaction/cancel/{tx_ref}": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Cancels an active Chapa transaction using its transaction reference", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Cancel a Chapa deposit transaction", + "parameters": [ + { + "type": "string", + "description": "Transaction Reference", + "name": "tx_ref", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ChapaCancelResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/chapa/transaction/events/{ref_id}": { + "get": { + "description": "Retrieve the timeline of events for a specific Chapa transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Fetch transaction events", + "parameters": [ + { + "type": "string", + "description": "Transaction Reference", + "name": "ref_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ChapaTransactionEvent" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/chapa/transaction/manual/verify/{tx_ref}": { + "get": { + "description": "Manually verify a payment using Chapa's API", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Verify a payment manually", + "parameters": [ + { + "type": "string", + "description": "Transaction Reference", + "name": "tx_ref", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/chapa/transactions": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Retrieves all transactions from Chapa payment gateway", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get all Chapa transactions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ChapaTransaction" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/chapa/transfers": { + "get": { + "description": "Retrieve all transfer records from Chapa", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get all Chapa transfers", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/company": { "get": { "description": "Gets all companies", @@ -3040,6 +3346,270 @@ } } }, + "/api/v1/enetpulse/betting-offers": { + "get": { + "description": "Fetches all EnetPulse preodds betting offers stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get all betting offers", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreoddsBettingOffer" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/fixtures": { + "get": { + "description": "Fetches all fixtures stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get all stored fixtures", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseFixture" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/fixtures/preodds": { + "get": { + "description": "Fetches all EnetPulse fixtures along with their associated pre-match odds", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get fixtures with preodds", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseFixtureWithPreodds" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/preodds": { + "get": { + "description": "Fetches all EnetPulse pre-match odds stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get all preodds", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreodds" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/preodds-with-offers": { + "get": { + "description": "Fetches all EnetPulse pre-match odds along with their associated betting offers stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get all preodds with betting offers", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreodds" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/enetpulse/results": { + "get": { + "description": "Fetches all EnetPulse match results stored in the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "EnetPulse" + ], + "summary": "Get all results", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulseResult" + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/enetpulse/sports": { "get": { "description": "Fetches all sports stored in the database", @@ -3050,7 +3620,7 @@ "application/json" ], "tags": [ - "EnetPulse - Sports" + "EnetPulse" ], "summary": "Get all sports", "responses": { @@ -3094,7 +3664,7 @@ "application/json" ], "tags": [ - "EnetPulse - Tournament Stages" + "EnetPulse" ], "summary": "Get all tournament stages", "responses": { @@ -3138,7 +3708,7 @@ "application/json" ], "tags": [ - "EnetPulse - Tournament Templates" + "EnetPulse" ], "summary": "Get all tournament templates", "responses": { @@ -3182,7 +3752,7 @@ "application/json" ], "tags": [ - "EnetPulse - Tournaments" + "EnetPulse" ], "summary": "Get all tournaments", "responses": { @@ -4116,108 +4686,6 @@ } } }, - "/api/v1/odds/pre-match": { - "get": { - "description": "Fetches pre-match odds from EnetPulse for a given event", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "EnetPulse - PreMatch" - ], - "summary": "Get pre-match odds for an event", - "parameters": [ - { - "type": "integer", - "description": "Event ID", - "name": "objectFK", - "in": "query", - "required": true - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "csv", - "description": "Odds provider IDs (comma separated)", - "name": "oddsProviderFK", - "in": "query" - }, - { - "type": "integer", - "description": "Outcome type ID", - "name": "outcomeTypeFK", - "in": "query" - }, - { - "type": "integer", - "description": "Outcome scope ID", - "name": "outcomeScopeFK", - "in": "query" - }, - { - "type": "integer", - "description": "Outcome subtype ID", - "name": "outcomeSubtypeFK", - "in": "query" - }, - { - "type": "integer", - "description": "Limit results", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset results", - "name": "offset", - "in": "query" - }, - { - "type": "integer", - "description": "Language type ID", - "name": "languageTypeFK", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.PreMatchOddsResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "502": { - "description": "Bad Gateway", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, "/api/v1/odds/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", @@ -4377,6 +4845,58 @@ } } }, + "/api/v1/orchestrator/virtual-game/provider-reports/asc": { + "get": { + "description": "Retrieves all virtual game provider reports sorted by total_games_played in ascending order", + "tags": [ + "VirtualGames - Orchestration" + ], + "summary": "List virtual game provider reports (ascending)", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.VirtualGameProviderReport" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/orchestrator/virtual-game/provider-reports/desc": { + "get": { + "description": "Retrieves all virtual game provider reports sorted by total_games_played in descending order", + "tags": [ + "VirtualGames - Orchestration" + ], + "summary": "List virtual game provider reports (descending)", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.VirtualGameProviderReport" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/orchestrator/virtual-games": { "get": { "description": "Returns all virtual games with optional filters (category, search, pagination)", @@ -6370,6 +6890,88 @@ } } }, + "/api/v1/ticket": { + "get": { + "description": "Retrieve all tickets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ticket" + ], + "summary": "Get all tickets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.TicketRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/ticket/{id}": { + "get": { + "description": "Retrieve ticket details by ticket ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ticket" + ], + "summary": "Get ticket by ID", + "parameters": [ + { + "type": "integer", + "description": "Ticket ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.TicketRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/api/v1/transfer/refill/:id": { "post": { "description": "Super Admin route to refill a wallet", @@ -9618,28 +10220,6 @@ } } }, - "domain.ArifpayB2CRequest": { - "type": "object", - "required": [ - "amount", - "customerEmail", - "customerPhone" - ], - "properties": { - "Phonenumber": { - "type": "string" - }, - "amount": { - "type": "number" - }, - "customerEmail": { - "type": "string" - }, - "customerPhone": { - "type": "string" - } - } - }, "domain.ArifpayVerifyByTransactionIDRequest": { "type": "object", "properties": { @@ -9673,7 +10253,7 @@ "type": "string" }, "round_id": { - "type": "integer" + "type": "string" }, "session_id": { "type": "string" @@ -9923,6 +10503,9 @@ }, "timerStatus": { "$ref": "#/definitions/domain.ValidString" + }, + "totalOddOutcomes": { + "type": "integer" } } }, @@ -10036,6 +10619,10 @@ "type": "integer", "example": 1 }, + "company_slug": { + "type": "string", + "example": "fortune" + }, "created_at": { "type": "string", "example": "2025-04-08T12:00:00Z" @@ -10239,6 +10826,52 @@ } } }, + "domain.ChapaCancelResponse": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "message": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tx_ref": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.ChapaCustomer": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_name": { + "type": "string" + }, + "mobile": { + "type": "string" + } + } + }, "domain.ChapaDepositRequestPayload": { "type": "object", "required": [ @@ -10261,37 +10894,131 @@ } } }, - "domain.ChapaVerificationResponse": { + "domain.ChapaTransaction": { "type": "object", "properties": { "amount": { - "type": "number" + "type": "string" + }, + "charge": { + "type": "string" + }, + "created_at": { + "type": "string" }, "currency": { "type": "string" }, + "customer": { + "$ref": "#/definitions/domain.ChapaCustomer" + }, + "payment_method": { + "type": "string" + }, + "ref_id": { + "type": "string" + }, "status": { "type": "string" }, - "tx_ref": { + "trans_id": { + "type": "string" + }, + "type": { "type": "string" } } }, - "domain.ChapaWebhookPayload": { + "domain.ChapaTransactionEvent": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "item": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.ChapaWebhookCustomization": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "logo": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "domain.ChapaWebhookPayment": { "type": "object", "properties": { "amount": { - "type": "integer" + "type": "string" + }, + "charge": { + "type": "string" + }, + "created_at": { + "type": "string" }, "currency": { "type": "string" }, + "customization": { + "$ref": "#/definitions/domain.ChapaWebhookCustomization" + }, + "email": { + "type": "string" + }, + "event": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "meta": { + "description": "may vary in structure, so kept flexible" + }, + "mobile": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "payment_method": { + "type": "string" + }, + "reference": { + "type": "string" + }, "status": { - "$ref": "#/definitions/domain.PaymentStatus" + "type": "string" }, "tx_ref": { "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" } } }, @@ -10744,6 +11471,324 @@ } } }, + "domain.EnetpulseFixture": { + "type": "object", + "properties": { + "gender": { + "type": "string" + }, + "id": { + "type": "string" + }, + "n": { + "description": "convert to int", + "type": "string" + }, + "name": { + "type": "string" + }, + "round_typeFK": { + "type": "string" + }, + "sportFK": { + "type": "string" + }, + "sport_name": { + "type": "string" + }, + "startdate": { + "description": "ISO 8601", + "type": "string" + }, + "status_descFK": { + "type": "string" + }, + "status_type": { + "type": "string" + }, + "tournamentFK": { + "type": "string" + }, + "tournament_name": { + "type": "string" + }, + "tournament_stage_name": { + "description": "TournamentStageFK string `json:\"tournament_stageFK\"`", + "type": "string" + }, + "tournament_templateFK": { + "type": "string" + }, + "tournament_template_name": { + "type": "string" + }, + "ut": { + "description": "parse to time.Time", + "type": "string" + } + } + }, + "domain.EnetpulseFixtureWithPreodds": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "fixtureApiID": { + "type": "string" + }, + "fixtureID": { + "type": "string" + }, + "fixtureName": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "preodds": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreodds" + } + }, + "roundTypeFk": { + "type": "string" + }, + "sportFk": { + "type": "string" + }, + "startDate": { + "type": "string" + }, + "statusDescFk": { + "type": "string" + }, + "statusType": { + "type": "string" + }, + "tournamentFk": { + "type": "string" + }, + "tournamentStageFk": { + "type": "string" + }, + "tournamentTemplateFk": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "updatesCount": { + "type": "integer" + } + } + }, + "domain.EnetpulsePreodds": { + "type": "object", + "properties": { + "bettingOffers": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreoddsBettingOffer" + } + }, + "createdAt": { + "type": "string" + }, + "dparam": { + "type": "string" + }, + "dparam2": { + "type": "string" + }, + "eventFK": { + "type": "integer" + }, + "eventParticipantNumber": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "iparam": { + "type": "string" + }, + "iparam2": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "outcomeScopeFK": { + "type": "integer" + }, + "outcomeSubtypeFK": { + "type": "integer" + }, + "outcomeTypeFK": { + "type": "integer" + }, + "preoddsID": { + "type": "string" + }, + "sparam": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "updatesCount": { + "type": "integer" + } + } + }, + "domain.EnetpulsePreoddsBettingOffer": { + "type": "object", + "properties": { + "active": { + "type": "string" + }, + "betting_offer_id": { + "type": "string" + }, + "betting_offer_status_fk": { + "type": "integer" + }, + "coupon_key": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_at": { + "type": "string" + }, + "odds": { + "type": "number" + }, + "odds_old": { + "type": "number" + }, + "odds_provider_fk": { + "type": "integer" + }, + "preodds_fk": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "updates_count": { + "type": "integer" + } + } + }, + "domain.EnetpulseResult": { + "type": "object", + "properties": { + "commentary": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "first_half_ended": { + "type": "string" + }, + "game_ended": { + "type": "string" + }, + "game_started": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_updated_at": { + "type": "string" + }, + "lineup_confirmed": { + "type": "boolean" + }, + "live": { + "type": "string" + }, + "livestats_plus": { + "type": "string" + }, + "livestats_type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "result_id": { + "type": "string" + }, + "round": { + "type": "string" + }, + "round_type_fk": { + "type": "string" + }, + "second_half_ended": { + "type": "string" + }, + "second_half_started": { + "type": "string" + }, + "spectators": { + "type": "integer" + }, + "sport_fk": { + "type": "string" + }, + "sport_name": { + "type": "string" + }, + "start_date": { + "type": "string" + }, + "status_desc_fk": { + "type": "string" + }, + "status_type": { + "type": "string" + }, + "tournament_fk": { + "type": "string" + }, + "tournament_name": { + "type": "string" + }, + "tournament_stage_name": { + "description": "TournamentStageFK string `json:\"tournament_stage_fk\"`", + "type": "string" + }, + "tournament_template_fk": { + "type": "string" + }, + "tournament_template_name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "updates_count": { + "type": "integer" + }, + "venue_name": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + } + }, "domain.EnetpulseSport": { "type": "object", "properties": { @@ -11061,6 +12106,9 @@ "timer_status": { "type": "string" }, + "total_odd_outcomes": { + "type": "integer" + }, "updated_at": { "type": "string" }, @@ -11788,21 +12836,6 @@ "BANK" ] }, - "domain.PaymentStatus": { - "type": "string", - "enum": [ - "success", - "pending", - "completed", - "failed" - ], - "x-enum-varnames": [ - "PaymentStatusSuccessful", - "PaymentStatusPending", - "PaymentStatusCompleted", - "PaymentStatusFailed" - ] - }, "domain.PopOKCallback": { "type": "object", "properties": { @@ -11854,41 +12887,6 @@ } } }, - "domain.PreMatchOddsResponse": { - "type": "object", - "properties": { - "eventFK": { - "description": "Define fields according to the Enetpulse preodds response structure\nExample:", - "type": "integer" - }, - "odds": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.PreMatchOutcome" - } - } - } - }, - "domain.PreMatchOutcome": { - "type": "object", - "properties": { - "oddsProviderFK": { - "type": "integer" - }, - "oddsValue": { - "type": "number" - }, - "outcomeFK": { - "type": "integer" - }, - "outcomeTypeFK": { - "type": "integer" - }, - "outcomeValue": { - "type": "string" - } - } - }, "domain.ProviderRequest": { "type": "object", "properties": { @@ -12262,7 +13260,7 @@ "type": "object", "properties": { "amount": { - "type": "integer" + "type": "number" }, "bet_id": { "type": "integer", @@ -12523,6 +13521,20 @@ } } }, + "domain.SwapRequest": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + } + } + }, "domain.TelebirrPaymentCallbackPayload": { "type": "object", "properties": { @@ -12859,6 +13871,44 @@ } } }, + "domain.VirtualGameProviderReport": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "provider_id": { + "type": "string" + }, + "report_date": { + "type": "string" + }, + "report_type": { + "type": "string" + }, + "total_bets": { + "type": "number" + }, + "total_games_played": { + "type": "integer" + }, + "total_payouts": { + "type": "number" + }, + "total_players": { + "type": "integer" + }, + "total_profit": { + "type": "number" + }, + "updated_at": { + "type": "string" + } + } + }, "domain.WebhookRequest": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 9a56b7a..6dddb84 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -38,21 +38,6 @@ definitions: user_id: type: string type: object - domain.ArifpayB2CRequest: - properties: - Phonenumber: - type: string - amount: - type: number - customerEmail: - type: string - customerPhone: - type: string - required: - - amount - - customerEmail - - customerPhone - type: object domain.ArifpayVerifyByTransactionIDRequest: properties: paymentType: @@ -75,7 +60,7 @@ definitions: player_id: type: string round_id: - type: integer + type: string session_id: type: string timestamp: @@ -241,6 +226,8 @@ definitions: $ref: '#/definitions/domain.EventStatus' timerStatus: $ref: '#/definitions/domain.ValidString' + totalOddOutcomes: + type: integer type: object domain.BaseLeague: properties: @@ -319,6 +306,9 @@ definitions: company_id: example: 1 type: integer + company_slug: + example: fortune + type: string created_at: example: "2025-04-08T12:00:00Z" type: string @@ -461,6 +451,36 @@ definitions: reference_number: type: string type: object + domain.ChapaCancelResponse: + properties: + amount: + type: number + created_at: + type: string + currency: + type: string + message: + type: string + status: + type: string + tx_ref: + type: string + updated_at: + type: string + type: object + domain.ChapaCustomer: + properties: + email: + type: string + first_name: + type: string + id: + type: integer + last_name: + type: string + mobile: + type: string + type: object domain.ChapaDepositRequestPayload: properties: amount: @@ -475,27 +495,89 @@ definitions: reference: type: string type: object - domain.ChapaVerificationResponse: + domain.ChapaTransaction: properties: amount: - type: number + type: string + charge: + type: string + created_at: + type: string currency: type: string + customer: + $ref: '#/definitions/domain.ChapaCustomer' + payment_method: + type: string + ref_id: + type: string status: type: string - tx_ref: + trans_id: + type: string + type: type: string type: object - domain.ChapaWebhookPayload: + domain.ChapaTransactionEvent: + properties: + created_at: + type: string + item: + type: integer + message: + type: string + type: + type: string + updated_at: + type: string + type: object + domain.ChapaWebhookCustomization: + properties: + description: + type: string + logo: + type: string + title: + type: string + type: object + domain.ChapaWebhookPayment: properties: amount: - type: integer + type: string + charge: + type: string + created_at: + type: string currency: type: string + customization: + $ref: '#/definitions/domain.ChapaWebhookCustomization' + email: + type: string + event: + type: string + first_name: + type: string + last_name: + type: string + meta: + description: may vary in structure, so kept flexible + mobile: + type: string + mode: + type: string + payment_method: + type: string + reference: + type: string status: - $ref: '#/definitions/domain.PaymentStatus' + type: string tx_ref: type: string + type: + type: string + updated_at: + type: string type: object domain.ChapaWithdrawalRequest: properties: @@ -806,6 +888,218 @@ definitions: - customer_id - sender_account type: object + domain.EnetpulseFixture: + properties: + gender: + type: string + id: + type: string + "n": + description: convert to int + type: string + name: + type: string + round_typeFK: + type: string + sport_name: + type: string + sportFK: + type: string + startdate: + description: ISO 8601 + type: string + status_descFK: + type: string + status_type: + type: string + tournament_name: + type: string + tournament_stage_name: + description: TournamentStageFK string `json:"tournament_stageFK"` + type: string + tournament_template_name: + type: string + tournament_templateFK: + type: string + tournamentFK: + type: string + ut: + description: parse to time.Time + type: string + type: object + domain.EnetpulseFixtureWithPreodds: + properties: + createdAt: + type: string + fixtureApiID: + type: string + fixtureID: + type: string + fixtureName: + type: string + lastUpdatedAt: + type: string + preodds: + items: + $ref: '#/definitions/domain.EnetpulsePreodds' + type: array + roundTypeFk: + type: string + sportFk: + type: string + startDate: + type: string + statusDescFk: + type: string + statusType: + type: string + tournamentFk: + type: string + tournamentStageFk: + type: string + tournamentTemplateFk: + type: string + updatedAt: + type: string + updatesCount: + type: integer + type: object + domain.EnetpulsePreodds: + properties: + bettingOffers: + items: + $ref: '#/definitions/domain.EnetpulsePreoddsBettingOffer' + type: array + createdAt: + type: string + dparam: + type: string + dparam2: + type: string + eventFK: + type: integer + eventParticipantNumber: + type: integer + id: + type: integer + iparam: + type: string + iparam2: + type: string + lastUpdatedAt: + type: string + outcomeScopeFK: + type: integer + outcomeSubtypeFK: + type: integer + outcomeTypeFK: + type: integer + preoddsID: + type: string + sparam: + type: string + updatedAt: + type: string + updatesCount: + type: integer + type: object + domain.EnetpulsePreoddsBettingOffer: + properties: + active: + type: string + betting_offer_id: + type: string + betting_offer_status_fk: + type: integer + coupon_key: + type: string + created_at: + type: string + id: + type: integer + last_updated_at: + type: string + odds: + type: number + odds_old: + type: number + odds_provider_fk: + type: integer + preodds_fk: + type: string + updated_at: + type: string + updates_count: + type: integer + type: object + domain.EnetpulseResult: + properties: + commentary: + type: string + created_at: + type: string + first_half_ended: + type: string + game_ended: + type: string + game_started: + type: string + id: + type: integer + last_updated_at: + type: string + lineup_confirmed: + type: boolean + live: + type: string + livestats_plus: + type: string + livestats_type: + type: string + name: + type: string + result_id: + type: string + round: + type: string + round_type_fk: + type: string + second_half_ended: + type: string + second_half_started: + type: string + spectators: + type: integer + sport_fk: + type: string + sport_name: + type: string + start_date: + type: string + status_desc_fk: + type: string + status_type: + type: string + tournament_fk: + type: string + tournament_name: + type: string + tournament_stage_name: + description: TournamentStageFK string `json:"tournament_stage_fk"` + type: string + tournament_template_fk: + type: string + tournament_template_name: + type: string + updated_at: + type: string + updates_count: + type: integer + venue_name: + type: string + verified: + type: boolean + type: object domain.EnetpulseSport: properties: created_at: @@ -1034,6 +1328,8 @@ definitions: $ref: '#/definitions/domain.EventStatus' timer_status: type: string + total_odd_outcomes: + type: integer updated_at: type: string winning_upper_limit: @@ -1523,18 +1819,6 @@ definitions: - TELEBIRR_TRANSACTION - ARIFPAY_TRANSACTION - BANK - domain.PaymentStatus: - enum: - - success - - pending - - completed - - failed - type: string - x-enum-varnames: - - PaymentStatusSuccessful - - PaymentStatusPending - - PaymentStatusCompleted - - PaymentStatusFailed domain.PopOKCallback: properties: amount: @@ -1569,31 +1853,6 @@ definitions: thumbnail: type: string type: object - domain.PreMatchOddsResponse: - properties: - eventFK: - description: |- - Define fields according to the Enetpulse preodds response structure - Example: - type: integer - odds: - items: - $ref: '#/definitions/domain.PreMatchOutcome' - type: array - type: object - domain.PreMatchOutcome: - properties: - oddsProviderFK: - type: integer - oddsValue: - type: number - outcomeFK: - type: integer - outcomeTypeFK: - type: integer - outcomeValue: - type: string - type: object domain.ProviderRequest: properties: brandId: @@ -1845,7 +2104,7 @@ definitions: domain.ShopBetRes: properties: amount: - type: integer + type: number bet_id: example: 1 type: integer @@ -2028,6 +2287,15 @@ definitions: example: SportsBook type: string type: object + domain.SwapRequest: + properties: + amount: + type: number + from: + type: string + to: + type: string + type: object domain.TelebirrPaymentCallbackPayload: properties: appid: @@ -2259,6 +2527,31 @@ definitions: updated_at: type: string type: object + domain.VirtualGameProviderReport: + properties: + created_at: + type: string + id: + type: integer + provider_id: + type: string + report_date: + type: string + report_type: + type: string + total_bets: + type: number + total_games_played: + type: integer + total_payouts: + type: number + total_players: + type: integer + total_profit: + type: number + updated_at: + type: string + type: object domain.WebhookRequest: properties: nonce: @@ -4278,7 +4571,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/domain.ArifpayB2CRequest' + $ref: '#/definitions/domain.CheckoutSessionClientRequest' produces: - application/json responses: @@ -4365,7 +4658,7 @@ paths: summary: Create Arifpay Checkout Session tags: - Arifpay - /api/v1/arifpay/checkout/{sessionId}/cancel: + /api/v1/arifpay/checkout/cancel/{sessionId}: post: consumes: - application/json @@ -5346,6 +5639,35 @@ paths: summary: Update cashier tags: - cashier + /api/v1/chapa/balances: + get: + consumes: + - application/json + description: Retrieve Chapa account balance, optionally filtered by currency + code (e.g., ETB, USD) + parameters: + - description: Currency code (optional) + in: query + name: currency_code + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get Chapa account balance + tags: + - Chapa /api/v1/chapa/banks: get: consumes: @@ -5397,15 +5719,15 @@ paths: summary: Initiate a deposit tags: - Chapa - /api/v1/chapa/payments/manual/verify/{tx_ref}: + /api/v1/chapa/payments/receipt/{chapa_ref}: get: consumes: - application/json - description: Manually verify a payment using Chapa's API + description: Retrieve the Chapa payment receipt URL using the reference ID parameters: - - description: Transaction Reference + - description: Chapa Reference ID in: path - name: tx_ref + name: chapa_ref required: true type: string produces: @@ -5414,7 +5736,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/domain.ChapaVerificationResponse' + $ref: '#/definitions/domain.Response' "400": description: Bad Request schema: @@ -5423,29 +5745,28 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: Verify a payment manually + summary: Get Chapa Payment Receipt URL tags: - Chapa /api/v1/chapa/payments/webhook/verify: post: consumes: - application/json - description: Handles payment notifications from Chapa + description: Handles payment and transfer notifications from Chapa parameters: - description: Webhook payload in: body name: request required: true schema: - $ref: '#/definitions/domain.ChapaWebhookPayload' + $ref: '#/definitions/domain.ChapaWebhookPayment' produces: - application/json responses: "200": description: OK schema: - additionalProperties: true - type: object + $ref: '#/definitions/domain.Response' "400": description: Bad Request schema: @@ -5473,7 +5794,7 @@ paths: produces: - application/json responses: - "201": + "200": description: Chapa withdrawal process initiated successfully schema: $ref: '#/definitions/domain.Response' @@ -5498,6 +5819,178 @@ paths: summary: Initiate a withdrawal tags: - Chapa + /api/v1/chapa/swap: + post: + consumes: + - application/json + description: Convert an amount from one currency to another using Chapa's currency + swap API + parameters: + - description: Swap request payload + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.SwapRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Swap currency using Chapa API + tags: + - Chapa + /api/v1/chapa/transaction/cancel/{tx_ref}: + put: + consumes: + - application/json + description: Cancels an active Chapa transaction using its transaction reference + parameters: + - description: Transaction Reference + in: path + name: tx_ref + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ChapaCancelResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: Cancel a Chapa deposit transaction + tags: + - Chapa + /api/v1/chapa/transaction/events/{ref_id}: + get: + consumes: + - application/json + description: Retrieve the timeline of events for a specific Chapa transaction + parameters: + - description: Transaction Reference + in: path + name: ref_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.ChapaTransactionEvent' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Fetch transaction events + tags: + - Chapa + /api/v1/chapa/transaction/manual/verify/{tx_ref}: + get: + consumes: + - application/json + description: Manually verify a payment using Chapa's API + parameters: + - description: Transaction Reference + in: path + name: tx_ref + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Verify a payment manually + tags: + - Chapa + /api/v1/chapa/transactions: + get: + consumes: + - application/json + description: Retrieves all transactions from Chapa payment gateway + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.ChapaTransaction' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: Get all Chapa transactions + tags: + - Chapa + /api/v1/chapa/transfers: + get: + consumes: + - application/json + description: Retrieve all transfer records from Chapa + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all Chapa transfers + tags: + - Chapa /api/v1/company: get: consumes: @@ -5973,6 +6466,164 @@ paths: summary: Verify a direct deposit tags: - Direct Deposits + /api/v1/enetpulse/betting-offers: + get: + consumes: + - application/json + description: Fetches all EnetPulse preodds betting offers stored in the database + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.EnetpulsePreoddsBettingOffer' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all betting offers + tags: + - EnetPulse + /api/v1/enetpulse/fixtures: + get: + consumes: + - application/json + description: Fetches all fixtures stored in the database + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.EnetpulseFixture' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all stored fixtures + tags: + - EnetPulse + /api/v1/enetpulse/fixtures/preodds: + get: + consumes: + - application/json + description: Fetches all EnetPulse fixtures along with their associated pre-match + odds + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.EnetpulseFixtureWithPreodds' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get fixtures with preodds + tags: + - EnetPulse + /api/v1/enetpulse/preodds: + get: + consumes: + - application/json + description: Fetches all EnetPulse pre-match odds stored in the database + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.EnetpulsePreodds' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all preodds + tags: + - EnetPulse + /api/v1/enetpulse/preodds-with-offers: + get: + consumes: + - application/json + description: Fetches all EnetPulse pre-match odds along with their associated + betting offers stored in the database + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.EnetpulsePreodds' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all preodds with betting offers + tags: + - EnetPulse + /api/v1/enetpulse/results: + get: + consumes: + - application/json + description: Fetches all EnetPulse match results stored in the database + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.EnetpulseResult' + type: array + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get all results + tags: + - EnetPulse /api/v1/enetpulse/sports: get: consumes: @@ -5998,7 +6649,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all sports tags: - - EnetPulse - Sports + - EnetPulse /api/v1/enetpulse/tournament-stages: get: consumes: @@ -6024,7 +6675,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all tournament stages tags: - - EnetPulse - Tournament Stages + - EnetPulse /api/v1/enetpulse/tournament-templates: get: consumes: @@ -6050,7 +6701,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all tournament templates tags: - - EnetPulse - Tournament Templates + - EnetPulse /api/v1/enetpulse/tournaments: get: consumes: @@ -6076,7 +6727,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all tournaments tags: - - EnetPulse - Tournaments + - EnetPulse /api/v1/events: get: consumes: @@ -6673,71 +7324,6 @@ paths: summary: Retrieve all odds tags: - prematch - /api/v1/odds/pre-match: - get: - consumes: - - application/json - description: Fetches pre-match odds from EnetPulse for a given event - parameters: - - description: Event ID - in: query - name: objectFK - required: true - type: integer - - collectionFormat: csv - description: Odds provider IDs (comma separated) - in: query - items: - type: integer - name: oddsProviderFK - type: array - - description: Outcome type ID - in: query - name: outcomeTypeFK - type: integer - - description: Outcome scope ID - in: query - name: outcomeScopeFK - type: integer - - description: Outcome subtype ID - in: query - name: outcomeSubtypeFK - type: integer - - description: Limit results - in: query - name: limit - type: integer - - description: Offset results - in: query - name: offset - type: integer - - description: Language type ID - in: query - name: languageTypeFK - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.PreMatchOddsResponse' - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/domain.ErrorResponse' - "502": - description: Bad Gateway - schema: - $ref: '#/definitions/domain.ErrorResponse' - summary: Get pre-match odds for an event - tags: - - EnetPulse - PreMatch /api/v1/odds/upcoming/{upcoming_id}: get: consumes: @@ -6844,6 +7430,42 @@ paths: summary: Create a operation tags: - branch + /api/v1/orchestrator/virtual-game/provider-reports/asc: + get: + description: Retrieves all virtual game provider reports sorted by total_games_played + in ascending order + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.VirtualGameProviderReport' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List virtual game provider reports (ascending) + tags: + - VirtualGames - Orchestration + /api/v1/orchestrator/virtual-game/provider-reports/desc: + get: + description: Retrieves all virtual game provider reports sorted by total_games_played + in descending order + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.VirtualGameProviderReport' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List virtual game provider reports (descending) + tags: + - VirtualGames - Orchestration /api/v1/orchestrator/virtual-games: get: consumes: @@ -8154,6 +8776,60 @@ paths: summary: Get referral statistics tags: - referral + /api/v1/ticket: + get: + consumes: + - application/json + description: Retrieve all tickets + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.TicketRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get all tickets + tags: + - ticket + /api/v1/ticket/{id}: + get: + consumes: + - application/json + description: Retrieve ticket details by ticket ID + parameters: + - description: Ticket ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.TicketRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get ticket by ID + tags: + - ticket /api/v1/transfer/refill/:id: post: consumes: diff --git a/gen/db/enet_pulse.sql.go b/gen/db/enet_pulse.sql.go index a2c131b..f06cd69 100644 --- a/gen/db/enet_pulse.sql.go +++ b/gen/db/enet_pulse.sql.go @@ -11,6 +11,657 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const CreateEnetpulseFixture = `-- name: CreateEnetpulseFixture :one +INSERT INTO enetpulse_fixtures ( + fixture_id, + name, + sport_fk, + tournament_fk, + tournament_template_fk, + tournament_name, + tournament_template_name, + sport_name, + gender, + start_date, + status_type, + status_desc_fk, + round_type_fk, + updates_count, + last_updated_at, + created_at, + updated_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP +) +ON CONFLICT (fixture_id) DO UPDATE +SET + name = EXCLUDED.name, + sport_fk = EXCLUDED.sport_fk, + tournament_fk = EXCLUDED.tournament_fk, + tournament_template_fk = EXCLUDED.tournament_template_fk, + tournament_name = EXCLUDED.tournament_name, + tournament_template_name = EXCLUDED.tournament_template_name, + sport_name = EXCLUDED.sport_name, + gender = EXCLUDED.gender, + start_date = EXCLUDED.start_date, + status_type = EXCLUDED.status_type, + status_desc_fk = EXCLUDED.status_desc_fk, + round_type_fk = EXCLUDED.round_type_fk, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + updated_at = CURRENT_TIMESTAMP +RETURNING id, fixture_id, name, sport_fk, tournament_fk, tournament_template_fk, tournament_name, tournament_template_name, sport_name, gender, start_date, status_type, status_desc_fk, round_type_fk, updates_count, last_updated_at, created_at, updated_at +` + +type CreateEnetpulseFixtureParams struct { + FixtureID string `json:"fixture_id"` + Name string `json:"name"` + SportFk string `json:"sport_fk"` + TournamentFk pgtype.Text `json:"tournament_fk"` + TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"` + TournamentName pgtype.Text `json:"tournament_name"` + TournamentTemplateName pgtype.Text `json:"tournament_template_name"` + SportName pgtype.Text `json:"sport_name"` + Gender pgtype.Text `json:"gender"` + StartDate pgtype.Timestamptz `json:"start_date"` + StatusType pgtype.Text `json:"status_type"` + StatusDescFk pgtype.Text `json:"status_desc_fk"` + RoundTypeFk pgtype.Text `json:"round_type_fk"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` +} + +func (q *Queries) CreateEnetpulseFixture(ctx context.Context, arg CreateEnetpulseFixtureParams) (EnetpulseFixture, error) { + row := q.db.QueryRow(ctx, CreateEnetpulseFixture, + arg.FixtureID, + arg.Name, + arg.SportFk, + arg.TournamentFk, + arg.TournamentTemplateFk, + arg.TournamentName, + arg.TournamentTemplateName, + arg.SportName, + arg.Gender, + arg.StartDate, + arg.StatusType, + arg.StatusDescFk, + arg.RoundTypeFk, + arg.UpdatesCount, + arg.LastUpdatedAt, + ) + var i EnetpulseFixture + err := row.Scan( + &i.ID, + &i.FixtureID, + &i.Name, + &i.SportFk, + &i.TournamentFk, + &i.TournamentTemplateFk, + &i.TournamentName, + &i.TournamentTemplateName, + &i.SportName, + &i.Gender, + &i.StartDate, + &i.StatusType, + &i.StatusDescFk, + &i.RoundTypeFk, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateEnetpulseOutcomeType = `-- name: CreateEnetpulseOutcomeType :one +INSERT INTO enetpulse_outcome_types ( + outcome_type_id, + name, + description, + updates_count, + last_updated_at, + created_at, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP +) +ON CONFLICT (outcome_type_id) DO UPDATE +SET + name = EXCLUDED.name, + description = EXCLUDED.description, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + updated_at = CURRENT_TIMESTAMP +RETURNING id, outcome_type_id, name, description, updates_count, last_updated_at, created_at, updated_at +` + +type CreateEnetpulseOutcomeTypeParams struct { + OutcomeTypeID string `json:"outcome_type_id"` + Name string `json:"name"` + Description pgtype.Text `json:"description"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` +} + +func (q *Queries) CreateEnetpulseOutcomeType(ctx context.Context, arg CreateEnetpulseOutcomeTypeParams) (EnetpulseOutcomeType, error) { + row := q.db.QueryRow(ctx, CreateEnetpulseOutcomeType, + arg.OutcomeTypeID, + arg.Name, + arg.Description, + arg.UpdatesCount, + arg.LastUpdatedAt, + ) + var i EnetpulseOutcomeType + err := row.Scan( + &i.ID, + &i.OutcomeTypeID, + &i.Name, + &i.Description, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateEnetpulsePreodds = `-- name: CreateEnetpulsePreodds :one +INSERT INTO enetpulse_preodds ( + preodds_id, + event_fk, + outcome_type_fk, + outcome_scope_fk, + outcome_subtype_fk, + event_participant_number, + iparam, + iparam2, + dparam, + dparam2, + sparam, + updates_count, + last_updated_at, + created_at, + updated_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP +) +ON CONFLICT (preodds_id) DO UPDATE +SET + event_fk = EXCLUDED.event_fk, + outcome_type_fk = EXCLUDED.outcome_type_fk, + outcome_scope_fk = EXCLUDED.outcome_scope_fk, + outcome_subtype_fk = EXCLUDED.outcome_subtype_fk, + event_participant_number = EXCLUDED.event_participant_number, + iparam = EXCLUDED.iparam, + iparam2 = EXCLUDED.iparam2, + dparam = EXCLUDED.dparam, + dparam2 = EXCLUDED.dparam2, + sparam = EXCLUDED.sparam, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + updated_at = CURRENT_TIMESTAMP +RETURNING id, preodds_id, event_fk, outcome_type_fk, outcome_scope_fk, outcome_subtype_fk, event_participant_number, iparam, iparam2, dparam, dparam2, sparam, updates_count, last_updated_at, created_at, updated_at +` + +type CreateEnetpulsePreoddsParams struct { + PreoddsID string `json:"preodds_id"` + EventFk int64 `json:"event_fk"` + OutcomeTypeFk pgtype.Int4 `json:"outcome_type_fk"` + OutcomeScopeFk pgtype.Int4 `json:"outcome_scope_fk"` + OutcomeSubtypeFk pgtype.Int4 `json:"outcome_subtype_fk"` + EventParticipantNumber pgtype.Int4 `json:"event_participant_number"` + Iparam pgtype.Text `json:"iparam"` + Iparam2 pgtype.Text `json:"iparam2"` + Dparam pgtype.Text `json:"dparam"` + Dparam2 pgtype.Text `json:"dparam2"` + Sparam pgtype.Text `json:"sparam"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` +} + +func (q *Queries) CreateEnetpulsePreodds(ctx context.Context, arg CreateEnetpulsePreoddsParams) (EnetpulsePreodd, error) { + row := q.db.QueryRow(ctx, CreateEnetpulsePreodds, + arg.PreoddsID, + arg.EventFk, + arg.OutcomeTypeFk, + arg.OutcomeScopeFk, + arg.OutcomeSubtypeFk, + arg.EventParticipantNumber, + arg.Iparam, + arg.Iparam2, + arg.Dparam, + arg.Dparam2, + arg.Sparam, + arg.UpdatesCount, + arg.LastUpdatedAt, + ) + var i EnetpulsePreodd + err := row.Scan( + &i.ID, + &i.PreoddsID, + &i.EventFk, + &i.OutcomeTypeFk, + &i.OutcomeScopeFk, + &i.OutcomeSubtypeFk, + &i.EventParticipantNumber, + &i.Iparam, + &i.Iparam2, + &i.Dparam, + &i.Dparam2, + &i.Sparam, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateEnetpulsePreoddsBettingOffer = `-- name: CreateEnetpulsePreoddsBettingOffer :one +INSERT INTO enetpulse_preodds_bettingoffers ( + bettingoffer_id, + preodds_fk, + bettingoffer_status_fk, + odds_provider_fk, + odds, + odds_old, + active, + coupon_key, + updates_count, + last_updated_at, + created_at, + updated_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP +) +ON CONFLICT (bettingoffer_id) DO UPDATE +SET + preodds_fk = EXCLUDED.preodds_fk, + bettingoffer_status_fk = EXCLUDED.bettingoffer_status_fk, + odds_provider_fk = EXCLUDED.odds_provider_fk, + odds = EXCLUDED.odds, + odds_old = EXCLUDED.odds_old, + active = EXCLUDED.active, + coupon_key = EXCLUDED.coupon_key, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + updated_at = CURRENT_TIMESTAMP +RETURNING id, bettingoffer_id, preodds_fk, bettingoffer_status_fk, odds_provider_fk, odds, odds_old, active, coupon_key, updates_count, last_updated_at, created_at, updated_at +` + +type CreateEnetpulsePreoddsBettingOfferParams struct { + BettingofferID string `json:"bettingoffer_id"` + PreoddsFk string `json:"preodds_fk"` + BettingofferStatusFk pgtype.Int4 `json:"bettingoffer_status_fk"` + OddsProviderFk pgtype.Int4 `json:"odds_provider_fk"` + Odds pgtype.Numeric `json:"odds"` + OddsOld pgtype.Numeric `json:"odds_old"` + Active pgtype.Bool `json:"active"` + CouponKey pgtype.Text `json:"coupon_key"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` +} + +func (q *Queries) CreateEnetpulsePreoddsBettingOffer(ctx context.Context, arg CreateEnetpulsePreoddsBettingOfferParams) (EnetpulsePreoddsBettingoffer, error) { + row := q.db.QueryRow(ctx, CreateEnetpulsePreoddsBettingOffer, + arg.BettingofferID, + arg.PreoddsFk, + arg.BettingofferStatusFk, + arg.OddsProviderFk, + arg.Odds, + arg.OddsOld, + arg.Active, + arg.CouponKey, + arg.UpdatesCount, + arg.LastUpdatedAt, + ) + var i EnetpulsePreoddsBettingoffer + err := row.Scan( + &i.ID, + &i.BettingofferID, + &i.PreoddsFk, + &i.BettingofferStatusFk, + &i.OddsProviderFk, + &i.Odds, + &i.OddsOld, + &i.Active, + &i.CouponKey, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateEnetpulseResult = `-- name: CreateEnetpulseResult :one +INSERT INTO enetpulse_results ( + result_id, + name, + sport_fk, + tournament_fk, + tournament_template_fk, + tournament_name, + tournament_template_name, + sport_name, + start_date, + status_type, + status_desc_fk, + round_type_fk, + updates_count, + last_updated_at, + round, + live, + venue_name, + livestats_plus, + livestats_type, + commentary, + lineup_confirmed, + verified, + spectators, + game_started, + first_half_ended, + second_half_started, + second_half_ended, + game_ended, + created_at, + updated_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10, + $11,$12,$13,$14,$15,$16,$17,$18, + $19,$20,$21,$22,$23,$24,$25,$26, + $27,$28,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP +) +ON CONFLICT (result_id) DO UPDATE +SET + name = EXCLUDED.name, + sport_fk = EXCLUDED.sport_fk, + tournament_fk = EXCLUDED.tournament_fk, + tournament_template_fk = EXCLUDED.tournament_template_fk, + tournament_name = EXCLUDED.tournament_name, + tournament_template_name = EXCLUDED.tournament_template_name, + sport_name = EXCLUDED.sport_name, + start_date = EXCLUDED.start_date, + status_type = EXCLUDED.status_type, + status_desc_fk = EXCLUDED.status_desc_fk, + round_type_fk = EXCLUDED.round_type_fk, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + round = EXCLUDED.round, + live = EXCLUDED.live, + venue_name = EXCLUDED.venue_name, + livestats_plus = EXCLUDED.livestats_plus, + livestats_type = EXCLUDED.livestats_type, + commentary = EXCLUDED.commentary, + lineup_confirmed = EXCLUDED.lineup_confirmed, + verified = EXCLUDED.verified, + spectators = EXCLUDED.spectators, + game_started = EXCLUDED.game_started, + first_half_ended = EXCLUDED.first_half_ended, + second_half_started = EXCLUDED.second_half_started, + second_half_ended = EXCLUDED.second_half_ended, + game_ended = EXCLUDED.game_ended, + updated_at = CURRENT_TIMESTAMP +RETURNING id, result_id, name, sport_fk, tournament_fk, tournament_template_fk, tournament_name, tournament_template_name, sport_name, start_date, status_type, status_desc_fk, round_type_fk, updates_count, last_updated_at, round, live, venue_name, livestats_plus, livestats_type, commentary, lineup_confirmed, verified, spectators, game_started, first_half_ended, second_half_started, second_half_ended, game_ended, created_at, updated_at +` + +type CreateEnetpulseResultParams struct { + ResultID string `json:"result_id"` + Name string `json:"name"` + SportFk string `json:"sport_fk"` + TournamentFk pgtype.Text `json:"tournament_fk"` + TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"` + TournamentName pgtype.Text `json:"tournament_name"` + TournamentTemplateName pgtype.Text `json:"tournament_template_name"` + SportName pgtype.Text `json:"sport_name"` + StartDate pgtype.Timestamptz `json:"start_date"` + StatusType pgtype.Text `json:"status_type"` + StatusDescFk pgtype.Text `json:"status_desc_fk"` + RoundTypeFk pgtype.Text `json:"round_type_fk"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + Round pgtype.Text `json:"round"` + Live pgtype.Text `json:"live"` + VenueName pgtype.Text `json:"venue_name"` + LivestatsPlus pgtype.Text `json:"livestats_plus"` + LivestatsType pgtype.Text `json:"livestats_type"` + Commentary pgtype.Text `json:"commentary"` + LineupConfirmed pgtype.Bool `json:"lineup_confirmed"` + Verified pgtype.Bool `json:"verified"` + Spectators pgtype.Int4 `json:"spectators"` + GameStarted pgtype.Timestamptz `json:"game_started"` + FirstHalfEnded pgtype.Timestamptz `json:"first_half_ended"` + SecondHalfStarted pgtype.Timestamptz `json:"second_half_started"` + SecondHalfEnded pgtype.Timestamptz `json:"second_half_ended"` + GameEnded pgtype.Timestamptz `json:"game_ended"` +} + +func (q *Queries) CreateEnetpulseResult(ctx context.Context, arg CreateEnetpulseResultParams) (EnetpulseResult, error) { + row := q.db.QueryRow(ctx, CreateEnetpulseResult, + arg.ResultID, + arg.Name, + arg.SportFk, + arg.TournamentFk, + arg.TournamentTemplateFk, + arg.TournamentName, + arg.TournamentTemplateName, + arg.SportName, + arg.StartDate, + arg.StatusType, + arg.StatusDescFk, + arg.RoundTypeFk, + arg.UpdatesCount, + arg.LastUpdatedAt, + arg.Round, + arg.Live, + arg.VenueName, + arg.LivestatsPlus, + arg.LivestatsType, + arg.Commentary, + arg.LineupConfirmed, + arg.Verified, + arg.Spectators, + arg.GameStarted, + arg.FirstHalfEnded, + arg.SecondHalfStarted, + arg.SecondHalfEnded, + arg.GameEnded, + ) + var i EnetpulseResult + err := row.Scan( + &i.ID, + &i.ResultID, + &i.Name, + &i.SportFk, + &i.TournamentFk, + &i.TournamentTemplateFk, + &i.TournamentName, + &i.TournamentTemplateName, + &i.SportName, + &i.StartDate, + &i.StatusType, + &i.StatusDescFk, + &i.RoundTypeFk, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.Round, + &i.Live, + &i.VenueName, + &i.LivestatsPlus, + &i.LivestatsType, + &i.Commentary, + &i.LineupConfirmed, + &i.Verified, + &i.Spectators, + &i.GameStarted, + &i.FirstHalfEnded, + &i.SecondHalfStarted, + &i.SecondHalfEnded, + &i.GameEnded, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateEnetpulseResultParticipant = `-- name: CreateEnetpulseResultParticipant :one +INSERT INTO enetpulse_result_participants ( + participant_map_id, + result_fk, + participant_fk, + number, + name, + gender, + type, + country_fk, + country_name, + ordinary_time, + running_score, + halftime, + final_result, + last_updated_at, + created_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,CURRENT_TIMESTAMP +) +ON CONFLICT (participant_map_id) DO UPDATE +SET + result_fk = EXCLUDED.result_fk, + participant_fk = EXCLUDED.participant_fk, + number = EXCLUDED.number, + name = EXCLUDED.name, + gender = EXCLUDED.gender, + type = EXCLUDED.type, + country_fk = EXCLUDED.country_fk, + country_name = EXCLUDED.country_name, + ordinary_time = EXCLUDED.ordinary_time, + running_score = EXCLUDED.running_score, + halftime = EXCLUDED.halftime, + final_result = EXCLUDED.final_result, + last_updated_at = EXCLUDED.last_updated_at +RETURNING id, participant_map_id, result_fk, participant_fk, number, name, gender, type, country_fk, country_name, ordinary_time, running_score, halftime, final_result, last_updated_at, created_at +` + +type CreateEnetpulseResultParticipantParams struct { + ParticipantMapID string `json:"participant_map_id"` + ResultFk string `json:"result_fk"` + ParticipantFk string `json:"participant_fk"` + Number pgtype.Int4 `json:"number"` + Name pgtype.Text `json:"name"` + Gender pgtype.Text `json:"gender"` + Type pgtype.Text `json:"type"` + CountryFk pgtype.Text `json:"country_fk"` + CountryName pgtype.Text `json:"country_name"` + OrdinaryTime pgtype.Text `json:"ordinary_time"` + RunningScore pgtype.Text `json:"running_score"` + Halftime pgtype.Text `json:"halftime"` + FinalResult pgtype.Text `json:"final_result"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` +} + +func (q *Queries) CreateEnetpulseResultParticipant(ctx context.Context, arg CreateEnetpulseResultParticipantParams) (EnetpulseResultParticipant, error) { + row := q.db.QueryRow(ctx, CreateEnetpulseResultParticipant, + arg.ParticipantMapID, + arg.ResultFk, + arg.ParticipantFk, + arg.Number, + arg.Name, + arg.Gender, + arg.Type, + arg.CountryFk, + arg.CountryName, + arg.OrdinaryTime, + arg.RunningScore, + arg.Halftime, + arg.FinalResult, + arg.LastUpdatedAt, + ) + var i EnetpulseResultParticipant + err := row.Scan( + &i.ID, + &i.ParticipantMapID, + &i.ResultFk, + &i.ParticipantFk, + &i.Number, + &i.Name, + &i.Gender, + &i.Type, + &i.CountryFk, + &i.CountryName, + &i.OrdinaryTime, + &i.RunningScore, + &i.Halftime, + &i.FinalResult, + &i.LastUpdatedAt, + &i.CreatedAt, + ) + return i, err +} + +const CreateEnetpulseResultReferee = `-- name: CreateEnetpulseResultReferee :one +INSERT INTO enetpulse_result_referees ( + result_fk, + referee_fk, + assistant1_referee_fk, + assistant2_referee_fk, + fourth_referee_fk, + var1_referee_fk, + var2_referee_fk, + last_updated_at, + created_at +) VALUES ( + $1,$2,$3,$4,$5,$6,$7,$8,CURRENT_TIMESTAMP +) +ON CONFLICT (result_fk) DO UPDATE +SET + referee_fk = EXCLUDED.referee_fk, + assistant1_referee_fk = EXCLUDED.assistant1_referee_fk, + assistant2_referee_fk = EXCLUDED.assistant2_referee_fk, + fourth_referee_fk = EXCLUDED.fourth_referee_fk, + var1_referee_fk = EXCLUDED.var1_referee_fk, + var2_referee_fk = EXCLUDED.var2_referee_fk, + last_updated_at = EXCLUDED.last_updated_at +RETURNING id, result_fk, referee_fk, assistant1_referee_fk, assistant2_referee_fk, fourth_referee_fk, var1_referee_fk, var2_referee_fk, last_updated_at, created_at +` + +type CreateEnetpulseResultRefereeParams struct { + ResultFk string `json:"result_fk"` + RefereeFk pgtype.Text `json:"referee_fk"` + Assistant1RefereeFk pgtype.Text `json:"assistant1_referee_fk"` + Assistant2RefereeFk pgtype.Text `json:"assistant2_referee_fk"` + FourthRefereeFk pgtype.Text `json:"fourth_referee_fk"` + Var1RefereeFk pgtype.Text `json:"var1_referee_fk"` + Var2RefereeFk pgtype.Text `json:"var2_referee_fk"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` +} + +func (q *Queries) CreateEnetpulseResultReferee(ctx context.Context, arg CreateEnetpulseResultRefereeParams) (EnetpulseResultReferee, error) { + row := q.db.QueryRow(ctx, CreateEnetpulseResultReferee, + arg.ResultFk, + arg.RefereeFk, + arg.Assistant1RefereeFk, + arg.Assistant2RefereeFk, + arg.FourthRefereeFk, + arg.Var1RefereeFk, + arg.Var2RefereeFk, + arg.LastUpdatedAt, + ) + var i EnetpulseResultReferee + err := row.Scan( + &i.ID, + &i.ResultFk, + &i.RefereeFk, + &i.Assistant1RefereeFk, + &i.Assistant2RefereeFk, + &i.FourthRefereeFk, + &i.Var1RefereeFk, + &i.Var2RefereeFk, + &i.LastUpdatedAt, + &i.CreatedAt, + ) + return i, err +} + const CreateEnetpulseSport = `-- name: CreateEnetpulseSport :one INSERT INTO enetpulse_sports ( sport_id, @@ -63,6 +714,7 @@ func (q *Queries) CreateEnetpulseSport(ctx context.Context, arg CreateEnetpulseS } const CreateEnetpulseTournament = `-- name: CreateEnetpulseTournament :one + INSERT INTO enetpulse_tournaments ( tournament_id, name, @@ -90,6 +742,8 @@ type CreateEnetpulseTournamentParams struct { Status pgtype.Int4 `json:"status"` } +// -- name: DeleteEnetpulseTournamentTemplateByID :exec +// DELETE FROM enetpulse_tournament_templates WHERE template_id = $1; func (q *Queries) CreateEnetpulseTournament(ctx context.Context, arg CreateEnetpulseTournamentParams) (EnetpulseTournament, error) { row := q.db.QueryRow(ctx, CreateEnetpulseTournament, arg.TournamentID, @@ -127,7 +781,8 @@ INSERT INTO enetpulse_tournament_stages ( updates_count, last_updated_at, status -) VALUES ( +) +VALUES ( $1, -- stage_id $2, -- name $3, -- tournament_fk @@ -140,6 +795,19 @@ INSERT INTO enetpulse_tournament_stages ( $10, -- last_updated_at $11 -- status ) +ON CONFLICT (stage_id) DO UPDATE +SET + name = EXCLUDED.name, + tournament_fk = EXCLUDED.tournament_fk, + gender = EXCLUDED.gender, + country_fk = EXCLUDED.country_fk, + country_name = EXCLUDED.country_name, + start_date = EXCLUDED.start_date, + end_date = EXCLUDED.end_date, + updates_count = EXCLUDED.updates_count, + last_updated_at = EXCLUDED.last_updated_at, + status = EXCLUDED.status, + updated_at = NOW() RETURNING id, stage_id, name, tournament_fk, gender, country_fk, country_name, start_date, end_date, updates_count, last_updated_at, status, created_at, updated_at ` @@ -250,6 +918,349 @@ func (q *Queries) CreateEnetpulseTournamentTemplate(ctx context.Context, arg Cre return i, err } +const GetAllEnetpulseFixtures = `-- name: GetAllEnetpulseFixtures :many +SELECT id, fixture_id, name, sport_fk, tournament_fk, tournament_template_fk, tournament_name, tournament_template_name, sport_name, gender, start_date, status_type, status_desc_fk, round_type_fk, updates_count, last_updated_at, created_at, updated_at +FROM enetpulse_fixtures +ORDER BY created_at DESC +` + +func (q *Queries) GetAllEnetpulseFixtures(ctx context.Context) ([]EnetpulseFixture, error) { + rows, err := q.db.Query(ctx, GetAllEnetpulseFixtures) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EnetpulseFixture + for rows.Next() { + var i EnetpulseFixture + if err := rows.Scan( + &i.ID, + &i.FixtureID, + &i.Name, + &i.SportFk, + &i.TournamentFk, + &i.TournamentTemplateFk, + &i.TournamentName, + &i.TournamentTemplateName, + &i.SportName, + &i.Gender, + &i.StartDate, + &i.StatusType, + &i.StatusDescFk, + &i.RoundTypeFk, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetAllEnetpulseOutcomeTypes = `-- name: GetAllEnetpulseOutcomeTypes :many +SELECT id, outcome_type_id, name, description, updates_count, last_updated_at, created_at, updated_at +FROM enetpulse_outcome_types +ORDER BY created_at DESC +` + +func (q *Queries) GetAllEnetpulseOutcomeTypes(ctx context.Context) ([]EnetpulseOutcomeType, error) { + rows, err := q.db.Query(ctx, GetAllEnetpulseOutcomeTypes) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EnetpulseOutcomeType + for rows.Next() { + var i EnetpulseOutcomeType + if err := rows.Scan( + &i.ID, + &i.OutcomeTypeID, + &i.Name, + &i.Description, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetAllEnetpulsePreodds = `-- name: GetAllEnetpulsePreodds :many +SELECT id, preodds_id, event_fk, outcome_type_fk, outcome_scope_fk, outcome_subtype_fk, event_participant_number, iparam, iparam2, dparam, dparam2, sparam, updates_count, last_updated_at, created_at, updated_at +FROM enetpulse_preodds +ORDER BY created_at DESC +` + +func (q *Queries) GetAllEnetpulsePreodds(ctx context.Context) ([]EnetpulsePreodd, error) { + rows, err := q.db.Query(ctx, GetAllEnetpulsePreodds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EnetpulsePreodd + for rows.Next() { + var i EnetpulsePreodd + if err := rows.Scan( + &i.ID, + &i.PreoddsID, + &i.EventFk, + &i.OutcomeTypeFk, + &i.OutcomeScopeFk, + &i.OutcomeSubtypeFk, + &i.EventParticipantNumber, + &i.Iparam, + &i.Iparam2, + &i.Dparam, + &i.Dparam2, + &i.Sparam, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetAllEnetpulsePreoddsBettingOffers = `-- name: GetAllEnetpulsePreoddsBettingOffers :many +SELECT id, bettingoffer_id, preodds_fk, bettingoffer_status_fk, odds_provider_fk, odds, odds_old, active, coupon_key, updates_count, last_updated_at, created_at, updated_at +FROM enetpulse_preodds_bettingoffers +ORDER BY created_at DESC +` + +func (q *Queries) GetAllEnetpulsePreoddsBettingOffers(ctx context.Context) ([]EnetpulsePreoddsBettingoffer, error) { + rows, err := q.db.Query(ctx, GetAllEnetpulsePreoddsBettingOffers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EnetpulsePreoddsBettingoffer + for rows.Next() { + var i EnetpulsePreoddsBettingoffer + if err := rows.Scan( + &i.ID, + &i.BettingofferID, + &i.PreoddsFk, + &i.BettingofferStatusFk, + &i.OddsProviderFk, + &i.Odds, + &i.OddsOld, + &i.Active, + &i.CouponKey, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetAllEnetpulsePreoddsWithBettingOffers = `-- name: GetAllEnetpulsePreoddsWithBettingOffers :many +SELECT + p.id AS preodds_db_id, + p.preodds_id, + p.event_fk, + p.outcome_type_fk, + p.outcome_scope_fk, + p.outcome_subtype_fk, + p.event_participant_number, + p.iparam, + p.iparam2, + p.dparam, + p.dparam2, + p.sparam, + p.updates_count AS preodds_updates_count, + p.last_updated_at AS preodds_last_updated_at, + p.created_at AS preodds_created_at, + p.updated_at AS preodds_updated_at, + + -- Betting offer fields + bo.id AS bettingoffer_db_id, + bo.bettingoffer_id, + bo.preodds_fk, -- ✅ ensure alias matches struct field + bo.bettingoffer_status_fk, + bo.odds_provider_fk, + bo.odds, + bo.odds_old, + bo.active, + bo.coupon_key, + bo.updates_count AS bettingoffer_updates_count, + bo.last_updated_at AS bettingoffer_last_updated_at, + bo.created_at AS bettingoffer_created_at, + bo.updated_at AS bettingoffer_updated_at + +FROM enetpulse_preodds p +LEFT JOIN enetpulse_preodds_bettingoffers bo + ON bo.preodds_fk = p.preodds_id +ORDER BY p.created_at DESC, bo.created_at DESC +` + +type GetAllEnetpulsePreoddsWithBettingOffersRow struct { + PreoddsDbID int64 `json:"preodds_db_id"` + PreoddsID string `json:"preodds_id"` + EventFk int64 `json:"event_fk"` + OutcomeTypeFk pgtype.Int4 `json:"outcome_type_fk"` + OutcomeScopeFk pgtype.Int4 `json:"outcome_scope_fk"` + OutcomeSubtypeFk pgtype.Int4 `json:"outcome_subtype_fk"` + EventParticipantNumber pgtype.Int4 `json:"event_participant_number"` + Iparam pgtype.Text `json:"iparam"` + Iparam2 pgtype.Text `json:"iparam2"` + Dparam pgtype.Text `json:"dparam"` + Dparam2 pgtype.Text `json:"dparam2"` + Sparam pgtype.Text `json:"sparam"` + PreoddsUpdatesCount pgtype.Int4 `json:"preodds_updates_count"` + PreoddsLastUpdatedAt pgtype.Timestamptz `json:"preodds_last_updated_at"` + PreoddsCreatedAt pgtype.Timestamptz `json:"preodds_created_at"` + PreoddsUpdatedAt pgtype.Timestamptz `json:"preodds_updated_at"` + BettingofferDbID pgtype.Int8 `json:"bettingoffer_db_id"` + BettingofferID pgtype.Text `json:"bettingoffer_id"` + PreoddsFk pgtype.Text `json:"preodds_fk"` + BettingofferStatusFk pgtype.Int4 `json:"bettingoffer_status_fk"` + OddsProviderFk pgtype.Int4 `json:"odds_provider_fk"` + Odds pgtype.Numeric `json:"odds"` + OddsOld pgtype.Numeric `json:"odds_old"` + Active pgtype.Bool `json:"active"` + CouponKey pgtype.Text `json:"coupon_key"` + BettingofferUpdatesCount pgtype.Int4 `json:"bettingoffer_updates_count"` + BettingofferLastUpdatedAt pgtype.Timestamptz `json:"bettingoffer_last_updated_at"` + BettingofferCreatedAt pgtype.Timestamptz `json:"bettingoffer_created_at"` + BettingofferUpdatedAt pgtype.Timestamptz `json:"bettingoffer_updated_at"` +} + +func (q *Queries) GetAllEnetpulsePreoddsWithBettingOffers(ctx context.Context) ([]GetAllEnetpulsePreoddsWithBettingOffersRow, error) { + rows, err := q.db.Query(ctx, GetAllEnetpulsePreoddsWithBettingOffers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllEnetpulsePreoddsWithBettingOffersRow + for rows.Next() { + var i GetAllEnetpulsePreoddsWithBettingOffersRow + if err := rows.Scan( + &i.PreoddsDbID, + &i.PreoddsID, + &i.EventFk, + &i.OutcomeTypeFk, + &i.OutcomeScopeFk, + &i.OutcomeSubtypeFk, + &i.EventParticipantNumber, + &i.Iparam, + &i.Iparam2, + &i.Dparam, + &i.Dparam2, + &i.Sparam, + &i.PreoddsUpdatesCount, + &i.PreoddsLastUpdatedAt, + &i.PreoddsCreatedAt, + &i.PreoddsUpdatedAt, + &i.BettingofferDbID, + &i.BettingofferID, + &i.PreoddsFk, + &i.BettingofferStatusFk, + &i.OddsProviderFk, + &i.Odds, + &i.OddsOld, + &i.Active, + &i.CouponKey, + &i.BettingofferUpdatesCount, + &i.BettingofferLastUpdatedAt, + &i.BettingofferCreatedAt, + &i.BettingofferUpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetAllEnetpulseResults = `-- name: GetAllEnetpulseResults :many +SELECT id, result_id, name, sport_fk, tournament_fk, tournament_template_fk, tournament_name, tournament_template_name, sport_name, start_date, status_type, status_desc_fk, round_type_fk, updates_count, last_updated_at, round, live, venue_name, livestats_plus, livestats_type, commentary, lineup_confirmed, verified, spectators, game_started, first_half_ended, second_half_started, second_half_ended, game_ended, created_at, updated_at +FROM enetpulse_results +ORDER BY created_at DESC +` + +func (q *Queries) GetAllEnetpulseResults(ctx context.Context) ([]EnetpulseResult, error) { + rows, err := q.db.Query(ctx, GetAllEnetpulseResults) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EnetpulseResult + for rows.Next() { + var i EnetpulseResult + if err := rows.Scan( + &i.ID, + &i.ResultID, + &i.Name, + &i.SportFk, + &i.TournamentFk, + &i.TournamentTemplateFk, + &i.TournamentName, + &i.TournamentTemplateName, + &i.SportName, + &i.StartDate, + &i.StatusType, + &i.StatusDescFk, + &i.RoundTypeFk, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.Round, + &i.Live, + &i.VenueName, + &i.LivestatsPlus, + &i.LivestatsType, + &i.Commentary, + &i.LineupConfirmed, + &i.Verified, + &i.Spectators, + &i.GameStarted, + &i.FirstHalfEnded, + &i.SecondHalfStarted, + &i.SecondHalfEnded, + &i.GameEnded, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetAllEnetpulseSports = `-- name: GetAllEnetpulseSports :many SELECT id, @@ -417,6 +1428,213 @@ func (q *Queries) GetAllEnetpulseTournaments(ctx context.Context) ([]EnetpulseTo return items, nil } +const GetEnetpulseResultParticipantsByResultFK = `-- name: GetEnetpulseResultParticipantsByResultFK :many +SELECT id, participant_map_id, result_fk, participant_fk, number, name, gender, type, country_fk, country_name, ordinary_time, running_score, halftime, final_result, last_updated_at, created_at +FROM enetpulse_result_participants +WHERE result_fk = $1 +ORDER BY created_at DESC +` + +func (q *Queries) GetEnetpulseResultParticipantsByResultFK(ctx context.Context, resultFk string) ([]EnetpulseResultParticipant, error) { + rows, err := q.db.Query(ctx, GetEnetpulseResultParticipantsByResultFK, resultFk) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EnetpulseResultParticipant + for rows.Next() { + var i EnetpulseResultParticipant + if err := rows.Scan( + &i.ID, + &i.ParticipantMapID, + &i.ResultFk, + &i.ParticipantFk, + &i.Number, + &i.Name, + &i.Gender, + &i.Type, + &i.CountryFk, + &i.CountryName, + &i.OrdinaryTime, + &i.RunningScore, + &i.Halftime, + &i.FinalResult, + &i.LastUpdatedAt, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetEnetpulseResultRefereesByResultFK = `-- name: GetEnetpulseResultRefereesByResultFK :many +SELECT id, result_fk, referee_fk, assistant1_referee_fk, assistant2_referee_fk, fourth_referee_fk, var1_referee_fk, var2_referee_fk, last_updated_at, created_at +FROM enetpulse_result_referees +WHERE result_fk = $1 +ORDER BY created_at DESC +` + +func (q *Queries) GetEnetpulseResultRefereesByResultFK(ctx context.Context, resultFk string) ([]EnetpulseResultReferee, error) { + rows, err := q.db.Query(ctx, GetEnetpulseResultRefereesByResultFK, resultFk) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EnetpulseResultReferee + for rows.Next() { + var i EnetpulseResultReferee + if err := rows.Scan( + &i.ID, + &i.ResultFk, + &i.RefereeFk, + &i.Assistant1RefereeFk, + &i.Assistant2RefereeFk, + &i.FourthRefereeFk, + &i.Var1RefereeFk, + &i.Var2RefereeFk, + &i.LastUpdatedAt, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetFixturesWithPreodds = `-- name: GetFixturesWithPreodds :many +SELECT + f.fixture_id AS id, + f.fixture_id AS fixture_id, + f.name AS fixture_name, + f.sport_fk, + f.tournament_fk, + f.tournament_template_fk, + f.start_date, + f.status_type, + f.status_desc_fk, + f.round_type_fk, + f.updates_count AS fixture_updates_count, + f.last_updated_at AS fixture_last_updated_at, + f.created_at AS fixture_created_at, + f.updated_at AS fixture_updated_at, + + -- Preodds fields + p.id AS preodds_db_id, + p.preodds_id, + p.event_fk, + p.outcome_type_fk, + p.outcome_scope_fk, + p.outcome_subtype_fk, + p.event_participant_number, + p.iparam, + p.iparam2, + p.dparam, + p.dparam2, + p.sparam, + p.updates_count AS preodds_updates_count, + p.last_updated_at AS preodds_last_updated_at, + p.created_at AS preodds_created_at, + p.updated_at AS preodds_updated_at + +FROM enetpulse_fixtures f +LEFT JOIN enetpulse_preodds p + ON p.event_fk = f.id +ORDER BY f.start_date DESC +` + +type GetFixturesWithPreoddsRow struct { + ID string `json:"id"` + FixtureID string `json:"fixture_id"` + FixtureName string `json:"fixture_name"` + SportFk string `json:"sport_fk"` + TournamentFk pgtype.Text `json:"tournament_fk"` + TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"` + StartDate pgtype.Timestamptz `json:"start_date"` + StatusType pgtype.Text `json:"status_type"` + StatusDescFk pgtype.Text `json:"status_desc_fk"` + RoundTypeFk pgtype.Text `json:"round_type_fk"` + FixtureUpdatesCount pgtype.Int4 `json:"fixture_updates_count"` + FixtureLastUpdatedAt pgtype.Timestamptz `json:"fixture_last_updated_at"` + FixtureCreatedAt pgtype.Timestamptz `json:"fixture_created_at"` + FixtureUpdatedAt pgtype.Timestamptz `json:"fixture_updated_at"` + PreoddsDbID pgtype.Int8 `json:"preodds_db_id"` + PreoddsID pgtype.Text `json:"preodds_id"` + EventFk pgtype.Int8 `json:"event_fk"` + OutcomeTypeFk pgtype.Int4 `json:"outcome_type_fk"` + OutcomeScopeFk pgtype.Int4 `json:"outcome_scope_fk"` + OutcomeSubtypeFk pgtype.Int4 `json:"outcome_subtype_fk"` + EventParticipantNumber pgtype.Int4 `json:"event_participant_number"` + Iparam pgtype.Text `json:"iparam"` + Iparam2 pgtype.Text `json:"iparam2"` + Dparam pgtype.Text `json:"dparam"` + Dparam2 pgtype.Text `json:"dparam2"` + Sparam pgtype.Text `json:"sparam"` + PreoddsUpdatesCount pgtype.Int4 `json:"preodds_updates_count"` + PreoddsLastUpdatedAt pgtype.Timestamptz `json:"preodds_last_updated_at"` + PreoddsCreatedAt pgtype.Timestamptz `json:"preodds_created_at"` + PreoddsUpdatedAt pgtype.Timestamptz `json:"preodds_updated_at"` +} + +func (q *Queries) GetFixturesWithPreodds(ctx context.Context) ([]GetFixturesWithPreoddsRow, error) { + rows, err := q.db.Query(ctx, GetFixturesWithPreodds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFixturesWithPreoddsRow + for rows.Next() { + var i GetFixturesWithPreoddsRow + if err := rows.Scan( + &i.ID, + &i.FixtureID, + &i.FixtureName, + &i.SportFk, + &i.TournamentFk, + &i.TournamentTemplateFk, + &i.StartDate, + &i.StatusType, + &i.StatusDescFk, + &i.RoundTypeFk, + &i.FixtureUpdatesCount, + &i.FixtureLastUpdatedAt, + &i.FixtureCreatedAt, + &i.FixtureUpdatedAt, + &i.PreoddsDbID, + &i.PreoddsID, + &i.EventFk, + &i.OutcomeTypeFk, + &i.OutcomeScopeFk, + &i.OutcomeSubtypeFk, + &i.EventParticipantNumber, + &i.Iparam, + &i.Iparam2, + &i.Dparam, + &i.Dparam2, + &i.Sparam, + &i.PreoddsUpdatesCount, + &i.PreoddsLastUpdatedAt, + &i.PreoddsCreatedAt, + &i.PreoddsUpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetTournamentStagesByTournamentFK = `-- name: GetTournamentStagesByTournamentFK :many SELECT id, stage_id, name, tournament_fk, gender, country_fk, country_name, start_date, end_date, updates_count, last_updated_at, status, created_at, updated_at FROM enetpulse_tournament_stages diff --git a/gen/db/models.go b/gen/db/models.go index 6e3c87e..e8872d6 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -325,6 +325,139 @@ type DirectDeposit struct { VerifiedAt pgtype.Timestamp `json:"verified_at"` } +type EnetpulseFixture struct { + ID int64 `json:"id"` + FixtureID string `json:"fixture_id"` + Name string `json:"name"` + SportFk string `json:"sport_fk"` + TournamentFk pgtype.Text `json:"tournament_fk"` + TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"` + TournamentName pgtype.Text `json:"tournament_name"` + TournamentTemplateName pgtype.Text `json:"tournament_template_name"` + SportName pgtype.Text `json:"sport_name"` + Gender pgtype.Text `json:"gender"` + StartDate pgtype.Timestamptz `json:"start_date"` + StatusType pgtype.Text `json:"status_type"` + StatusDescFk pgtype.Text `json:"status_desc_fk"` + RoundTypeFk pgtype.Text `json:"round_type_fk"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type EnetpulseOutcomeType struct { + ID int64 `json:"id"` + OutcomeTypeID string `json:"outcome_type_id"` + Name string `json:"name"` + Description pgtype.Text `json:"description"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type EnetpulsePreodd struct { + ID int64 `json:"id"` + PreoddsID string `json:"preodds_id"` + EventFk int64 `json:"event_fk"` + OutcomeTypeFk pgtype.Int4 `json:"outcome_type_fk"` + OutcomeScopeFk pgtype.Int4 `json:"outcome_scope_fk"` + OutcomeSubtypeFk pgtype.Int4 `json:"outcome_subtype_fk"` + EventParticipantNumber pgtype.Int4 `json:"event_participant_number"` + Iparam pgtype.Text `json:"iparam"` + Iparam2 pgtype.Text `json:"iparam2"` + Dparam pgtype.Text `json:"dparam"` + Dparam2 pgtype.Text `json:"dparam2"` + Sparam pgtype.Text `json:"sparam"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type EnetpulsePreoddsBettingoffer struct { + ID int64 `json:"id"` + BettingofferID string `json:"bettingoffer_id"` + PreoddsFk string `json:"preodds_fk"` + BettingofferStatusFk pgtype.Int4 `json:"bettingoffer_status_fk"` + OddsProviderFk pgtype.Int4 `json:"odds_provider_fk"` + Odds pgtype.Numeric `json:"odds"` + OddsOld pgtype.Numeric `json:"odds_old"` + Active pgtype.Bool `json:"active"` + CouponKey pgtype.Text `json:"coupon_key"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type EnetpulseResult struct { + ID int64 `json:"id"` + ResultID string `json:"result_id"` + Name string `json:"name"` + SportFk string `json:"sport_fk"` + TournamentFk pgtype.Text `json:"tournament_fk"` + TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"` + TournamentName pgtype.Text `json:"tournament_name"` + TournamentTemplateName pgtype.Text `json:"tournament_template_name"` + SportName pgtype.Text `json:"sport_name"` + StartDate pgtype.Timestamptz `json:"start_date"` + StatusType pgtype.Text `json:"status_type"` + StatusDescFk pgtype.Text `json:"status_desc_fk"` + RoundTypeFk pgtype.Text `json:"round_type_fk"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + Round pgtype.Text `json:"round"` + Live pgtype.Text `json:"live"` + VenueName pgtype.Text `json:"venue_name"` + LivestatsPlus pgtype.Text `json:"livestats_plus"` + LivestatsType pgtype.Text `json:"livestats_type"` + Commentary pgtype.Text `json:"commentary"` + LineupConfirmed pgtype.Bool `json:"lineup_confirmed"` + Verified pgtype.Bool `json:"verified"` + Spectators pgtype.Int4 `json:"spectators"` + GameStarted pgtype.Timestamptz `json:"game_started"` + FirstHalfEnded pgtype.Timestamptz `json:"first_half_ended"` + SecondHalfStarted pgtype.Timestamptz `json:"second_half_started"` + SecondHalfEnded pgtype.Timestamptz `json:"second_half_ended"` + GameEnded pgtype.Timestamptz `json:"game_ended"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type EnetpulseResultParticipant struct { + ID int64 `json:"id"` + ParticipantMapID string `json:"participant_map_id"` + ResultFk string `json:"result_fk"` + ParticipantFk string `json:"participant_fk"` + Number pgtype.Int4 `json:"number"` + Name pgtype.Text `json:"name"` + Gender pgtype.Text `json:"gender"` + Type pgtype.Text `json:"type"` + CountryFk pgtype.Text `json:"country_fk"` + CountryName pgtype.Text `json:"country_name"` + OrdinaryTime pgtype.Text `json:"ordinary_time"` + RunningScore pgtype.Text `json:"running_score"` + Halftime pgtype.Text `json:"halftime"` + FinalResult pgtype.Text `json:"final_result"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +type EnetpulseResultReferee struct { + ID int64 `json:"id"` + ResultFk string `json:"result_fk"` + RefereeFk pgtype.Text `json:"referee_fk"` + Assistant1RefereeFk pgtype.Text `json:"assistant1_referee_fk"` + Assistant2RefereeFk pgtype.Text `json:"assistant2_referee_fk"` + FourthRefereeFk pgtype.Text `json:"fourth_referee_fk"` + Var1RefereeFk pgtype.Text `json:"var1_referee_fk"` + Var2RefereeFk pgtype.Text `json:"var2_referee_fk"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + type EnetpulseSport struct { ID int64 `json:"id"` SportID string `json:"sport_id"` @@ -1050,16 +1183,42 @@ type VirtualGameProvider struct { UpdatedAt pgtype.Timestamptz `json:"updated_at"` } +type VirtualGameProviderReport struct { + ID int64 `json:"id"` + ProviderID string `json:"provider_id"` + ReportDate pgtype.Date `json:"report_date"` + TotalGamesPlayed pgtype.Int8 `json:"total_games_played"` + TotalBets pgtype.Numeric `json:"total_bets"` + TotalPayouts pgtype.Numeric `json:"total_payouts"` + TotalProfit pgtype.Numeric `json:"total_profit"` + TotalPlayers pgtype.Int8 `json:"total_players"` + ReportType pgtype.Text `json:"report_type"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type VirtualGameReport struct { + ID int64 `json:"id"` + GameID string `json:"game_id"` + ProviderID string `json:"provider_id"` + ReportDate pgtype.Date `json:"report_date"` + TotalRounds pgtype.Int8 `json:"total_rounds"` + TotalBets pgtype.Numeric `json:"total_bets"` + TotalPayouts pgtype.Numeric `json:"total_payouts"` + TotalProfit pgtype.Numeric `json:"total_profit"` + TotalPlayers pgtype.Int8 `json:"total_players"` + ReportType pgtype.Text `json:"report_type"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + type VirtualGameSession struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` GameID string `json:"game_id"` SessionToken string `json:"session_token"` - Currency string `json:"currency"` - Status string `json:"status"` CreatedAt pgtype.Timestamptz `json:"created_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"` - ExpiresAt pgtype.Timestamptz `json:"expires_at"` } type VirtualGameTransaction struct { @@ -1118,38 +1277,40 @@ type WalletThresholdNotification struct { } type WalletTransfer struct { - ID int64 `json:"id"` - Amount pgtype.Int8 `json:"amount"` - Message string `json:"message"` - Type pgtype.Text `json:"type"` - ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` - SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` - CashierID pgtype.Int8 `json:"cashier_id"` - Verified pgtype.Bool `json:"verified"` - ReferenceNumber string `json:"reference_number"` - SessionID pgtype.Text `json:"session_id"` - Status pgtype.Text `json:"status"` - PaymentMethod pgtype.Text `json:"payment_method"` - CreatedAt pgtype.Timestamp `json:"created_at"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` + ID int64 `json:"id"` + Amount pgtype.Int8 `json:"amount"` + Message string `json:"message"` + Type pgtype.Text `json:"type"` + ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` + SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` + CashierID pgtype.Int8 `json:"cashier_id"` + Verified pgtype.Bool `json:"verified"` + ReferenceNumber string `json:"reference_number"` + ExtReferenceNumber pgtype.Text `json:"ext_reference_number"` + SessionID pgtype.Text `json:"session_id"` + Status pgtype.Text `json:"status"` + PaymentMethod pgtype.Text `json:"payment_method"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type WalletTransferDetail struct { - ID int64 `json:"id"` - Amount pgtype.Int8 `json:"amount"` - Message string `json:"message"` - Type pgtype.Text `json:"type"` - ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` - SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` - CashierID pgtype.Int8 `json:"cashier_id"` - Verified pgtype.Bool `json:"verified"` - ReferenceNumber string `json:"reference_number"` - SessionID pgtype.Text `json:"session_id"` - Status pgtype.Text `json:"status"` - PaymentMethod pgtype.Text `json:"payment_method"` - CreatedAt pgtype.Timestamp `json:"created_at"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` - FirstName pgtype.Text `json:"first_name"` - LastName pgtype.Text `json:"last_name"` - PhoneNumber pgtype.Text `json:"phone_number"` + ID int64 `json:"id"` + Amount pgtype.Int8 `json:"amount"` + Message string `json:"message"` + Type pgtype.Text `json:"type"` + ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` + SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` + CashierID pgtype.Int8 `json:"cashier_id"` + Verified pgtype.Bool `json:"verified"` + ReferenceNumber string `json:"reference_number"` + ExtReferenceNumber pgtype.Text `json:"ext_reference_number"` + SessionID pgtype.Text `json:"session_id"` + Status pgtype.Text `json:"status"` + PaymentMethod pgtype.Text `json:"payment_method"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + FirstName pgtype.Text `json:"first_name"` + LastName pgtype.Text `json:"last_name"` + PhoneNumber pgtype.Text `json:"phone_number"` } diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index b2a1066..185225b 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -21,26 +21,28 @@ INSERT INTO wallet_transfer ( cashier_id, verified, reference_number, + ext_reference_number, session_id, status, payment_method ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) -RETURNING id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) +RETURNING id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at ` type CreateTransferParams struct { - Amount pgtype.Int8 `json:"amount"` - Message string `json:"message"` - Type pgtype.Text `json:"type"` - ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` - SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` - CashierID pgtype.Int8 `json:"cashier_id"` - Verified pgtype.Bool `json:"verified"` - ReferenceNumber string `json:"reference_number"` - SessionID pgtype.Text `json:"session_id"` - Status pgtype.Text `json:"status"` - PaymentMethod pgtype.Text `json:"payment_method"` + Amount pgtype.Int8 `json:"amount"` + Message string `json:"message"` + Type pgtype.Text `json:"type"` + ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"` + SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` + CashierID pgtype.Int8 `json:"cashier_id"` + Verified pgtype.Bool `json:"verified"` + ReferenceNumber string `json:"reference_number"` + ExtReferenceNumber pgtype.Text `json:"ext_reference_number"` + SessionID pgtype.Text `json:"session_id"` + Status pgtype.Text `json:"status"` + PaymentMethod pgtype.Text `json:"payment_method"` } func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) (WalletTransfer, error) { @@ -53,6 +55,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) arg.CashierID, arg.Verified, arg.ReferenceNumber, + arg.ExtReferenceNumber, arg.SessionID, arg.Status, arg.PaymentMethod, @@ -68,6 +71,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.ExtReferenceNumber, &i.SessionID, &i.Status, &i.PaymentMethod, @@ -78,7 +82,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) } const GetAllTransfers = `-- name: GetAllTransfers :many -SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number +SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number FROM wallet_transfer_details ` @@ -101,6 +105,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransferDetail, &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.ExtReferenceNumber, &i.SessionID, &i.Status, &i.PaymentMethod, @@ -121,7 +126,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransferDetail, } const GetTransferByID = `-- name: GetTransferByID :one -SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number +SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number FROM wallet_transfer_details WHERE id = $1 ` @@ -139,6 +144,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.ExtReferenceNumber, &i.SessionID, &i.Status, &i.PaymentMethod, @@ -152,7 +158,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer } const GetTransferByReference = `-- name: GetTransferByReference :one -SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number +SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number FROM wallet_transfer_details WHERE reference_number = $1 ` @@ -170,6 +176,7 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.ExtReferenceNumber, &i.SessionID, &i.Status, &i.PaymentMethod, @@ -217,7 +224,7 @@ func (q *Queries) GetTransferStats(ctx context.Context, senderWalletID pgtype.In } const GetTransfersByWallet = `-- name: GetTransfersByWallet :many -SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number +SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number FROM wallet_transfer_details WHERE receiver_wallet_id = $1 OR sender_wallet_id = $1 @@ -242,6 +249,7 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgt &i.CashierID, &i.Verified, &i.ReferenceNumber, + &i.ExtReferenceNumber, &i.SessionID, &i.Status, &i.PaymentMethod, diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 5a2809a..1e39f92 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -281,56 +281,164 @@ func (q *Queries) CreateVirtualGameProvider(ctx context.Context, arg CreateVirtu return i, err } +const CreateVirtualGameProviderReport = `-- name: CreateVirtualGameProviderReport :one +INSERT INTO virtual_game_provider_reports ( + provider_id, + report_date, + total_games_played, + total_bets, + total_payouts, + total_players, + report_type, + created_at, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, $6, COALESCE($7, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP +) +ON CONFLICT (provider_id, report_date, report_type) DO UPDATE +SET + total_games_played = EXCLUDED.total_games_played, + total_bets = EXCLUDED.total_bets, + total_payouts = EXCLUDED.total_payouts, + total_players = EXCLUDED.total_players, + updated_at = CURRENT_TIMESTAMP +RETURNING id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at +` + +type CreateVirtualGameProviderReportParams struct { + ProviderID string `json:"provider_id"` + ReportDate pgtype.Date `json:"report_date"` + TotalGamesPlayed pgtype.Int8 `json:"total_games_played"` + TotalBets pgtype.Numeric `json:"total_bets"` + TotalPayouts pgtype.Numeric `json:"total_payouts"` + TotalPlayers pgtype.Int8 `json:"total_players"` + Column7 interface{} `json:"column_7"` +} + +func (q *Queries) CreateVirtualGameProviderReport(ctx context.Context, arg CreateVirtualGameProviderReportParams) (VirtualGameProviderReport, error) { + row := q.db.QueryRow(ctx, CreateVirtualGameProviderReport, + arg.ProviderID, + arg.ReportDate, + arg.TotalGamesPlayed, + arg.TotalBets, + arg.TotalPayouts, + arg.TotalPlayers, + arg.Column7, + ) + var i VirtualGameProviderReport + err := row.Scan( + &i.ID, + &i.ProviderID, + &i.ReportDate, + &i.TotalGamesPlayed, + &i.TotalBets, + &i.TotalPayouts, + &i.TotalProfit, + &i.TotalPlayers, + &i.ReportType, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateVirtualGameReport = `-- name: CreateVirtualGameReport :one +INSERT INTO virtual_game_reports ( + game_id, + provider_id, + report_date, + total_rounds, + total_bets, + total_payouts, + total_players, + report_type, + created_at, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, COALESCE($8, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP +) +ON CONFLICT (game_id, report_date, report_type) DO UPDATE +SET + total_rounds = EXCLUDED.total_rounds, + total_bets = EXCLUDED.total_bets, + total_payouts = EXCLUDED.total_payouts, + total_players = EXCLUDED.total_players, + updated_at = CURRENT_TIMESTAMP +RETURNING id, game_id, provider_id, report_date, total_rounds, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at +` + +type CreateVirtualGameReportParams struct { + GameID string `json:"game_id"` + ProviderID string `json:"provider_id"` + ReportDate pgtype.Date `json:"report_date"` + TotalRounds pgtype.Int8 `json:"total_rounds"` + TotalBets pgtype.Numeric `json:"total_bets"` + TotalPayouts pgtype.Numeric `json:"total_payouts"` + TotalPlayers pgtype.Int8 `json:"total_players"` + Column8 interface{} `json:"column_8"` +} + +func (q *Queries) CreateVirtualGameReport(ctx context.Context, arg CreateVirtualGameReportParams) (VirtualGameReport, error) { + row := q.db.QueryRow(ctx, CreateVirtualGameReport, + arg.GameID, + arg.ProviderID, + arg.ReportDate, + arg.TotalRounds, + arg.TotalBets, + arg.TotalPayouts, + arg.TotalPlayers, + arg.Column8, + ) + var i VirtualGameReport + err := row.Scan( + &i.ID, + &i.GameID, + &i.ProviderID, + &i.ReportDate, + &i.TotalRounds, + &i.TotalBets, + &i.TotalPayouts, + &i.TotalProfit, + &i.TotalPlayers, + &i.ReportType, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one INSERT INTO virtual_game_sessions ( - user_id, - game_id, - session_token, - currency, - status, - expires_at - ) -VALUES ($1, $2, $3, $4, $5, $6) -RETURNING id, + user_id, + game_id, + session_token +) +VALUES ($1, $2, $3) +RETURNING + id, user_id, game_id, session_token, - currency, - status, created_at, - updated_at, - expires_at + updated_at ` type CreateVirtualGameSessionParams struct { - UserID int64 `json:"user_id"` - GameID string `json:"game_id"` - SessionToken string `json:"session_token"` - Currency string `json:"currency"` - Status string `json:"status"` - ExpiresAt pgtype.Timestamptz `json:"expires_at"` + UserID int64 `json:"user_id"` + GameID string `json:"game_id"` + SessionToken string `json:"session_token"` } func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtualGameSessionParams) (VirtualGameSession, error) { - row := q.db.QueryRow(ctx, CreateVirtualGameSession, - arg.UserID, - arg.GameID, - arg.SessionToken, - arg.Currency, - arg.Status, - arg.ExpiresAt, - ) + row := q.db.QueryRow(ctx, CreateVirtualGameSession, arg.UserID, arg.GameID, arg.SessionToken) var i VirtualGameSession err := row.Scan( &i.ID, &i.UserID, &i.GameID, &i.SessionToken, - &i.Currency, - &i.Status, &i.CreatedAt, &i.UpdatedAt, - &i.ExpiresAt, ) return i, err } @@ -587,16 +695,46 @@ func (q *Queries) GetVirtualGameProviderByID(ctx context.Context, providerID str return i, err } +const GetVirtualGameProviderReportByProviderAndDate = `-- name: GetVirtualGameProviderReportByProviderAndDate :one +SELECT id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at +FROM virtual_game_provider_reports +WHERE provider_id = $1 + AND report_date = $2 + AND report_type = $3 +` + +type GetVirtualGameProviderReportByProviderAndDateParams struct { + ProviderID string `json:"provider_id"` + ReportDate pgtype.Date `json:"report_date"` + ReportType pgtype.Text `json:"report_type"` +} + +func (q *Queries) GetVirtualGameProviderReportByProviderAndDate(ctx context.Context, arg GetVirtualGameProviderReportByProviderAndDateParams) (VirtualGameProviderReport, error) { + row := q.db.QueryRow(ctx, GetVirtualGameProviderReportByProviderAndDate, arg.ProviderID, arg.ReportDate, arg.ReportType) + var i VirtualGameProviderReport + err := row.Scan( + &i.ID, + &i.ProviderID, + &i.ReportDate, + &i.TotalGamesPlayed, + &i.TotalBets, + &i.TotalPayouts, + &i.TotalProfit, + &i.TotalPlayers, + &i.ReportType, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const GetVirtualGameSessionByToken = `-- name: GetVirtualGameSessionByToken :one SELECT id, user_id, game_id, session_token, - currency, - status, created_at, - updated_at, - expires_at + updated_at FROM virtual_game_sessions WHERE session_token = $1 ` @@ -609,11 +747,34 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken &i.UserID, &i.GameID, &i.SessionToken, - &i.Currency, - &i.Status, &i.CreatedAt, &i.UpdatedAt, - &i.ExpiresAt, + ) + return i, err +} + +const GetVirtualGameSessionByUserID = `-- name: GetVirtualGameSessionByUserID :one +SELECT + id, + user_id, + game_id, + session_token, + created_at, + updated_at +FROM virtual_game_sessions +WHERE user_id = $1 +` + +func (q *Queries) GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (VirtualGameSession, error) { + row := q.db.QueryRow(ctx, GetVirtualGameSessionByUserID, userID) + var i VirtualGameSession + err := row.Scan( + &i.ID, + &i.UserID, + &i.GameID, + &i.SessionToken, + &i.CreatedAt, + &i.UpdatedAt, ) return i, err } @@ -745,6 +906,82 @@ func (q *Queries) ListFavoriteGames(ctx context.Context, userID int64) ([]int64, return items, nil } +const ListVirtualGameProviderReportsByGamesPlayedAsc = `-- name: ListVirtualGameProviderReportsByGamesPlayedAsc :many +SELECT id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at +FROM virtual_game_provider_reports +ORDER BY total_games_played ASC +` + +func (q *Queries) ListVirtualGameProviderReportsByGamesPlayedAsc(ctx context.Context) ([]VirtualGameProviderReport, error) { + rows, err := q.db.Query(ctx, ListVirtualGameProviderReportsByGamesPlayedAsc) + if err != nil { + return nil, err + } + defer rows.Close() + var items []VirtualGameProviderReport + for rows.Next() { + var i VirtualGameProviderReport + if err := rows.Scan( + &i.ID, + &i.ProviderID, + &i.ReportDate, + &i.TotalGamesPlayed, + &i.TotalBets, + &i.TotalPayouts, + &i.TotalProfit, + &i.TotalPlayers, + &i.ReportType, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const ListVirtualGameProviderReportsByGamesPlayedDesc = `-- name: ListVirtualGameProviderReportsByGamesPlayedDesc :many +SELECT id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at +FROM virtual_game_provider_reports +ORDER BY total_games_played DESC +` + +func (q *Queries) ListVirtualGameProviderReportsByGamesPlayedDesc(ctx context.Context) ([]VirtualGameProviderReport, error) { + rows, err := q.db.Query(ctx, ListVirtualGameProviderReportsByGamesPlayedDesc) + if err != nil { + return nil, err + } + defer rows.Close() + var items []VirtualGameProviderReport + for rows.Next() { + var i VirtualGameProviderReport + if err := rows.Scan( + &i.ID, + &i.ProviderID, + &i.ReportDate, + &i.TotalGamesPlayed, + &i.TotalBets, + &i.TotalPayouts, + &i.TotalProfit, + &i.TotalPlayers, + &i.ReportType, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListVirtualGameProviders = `-- name: ListVirtualGameProviders :many SELECT id, provider_id, @@ -845,20 +1082,40 @@ func (q *Queries) UpdateVirtualGameProviderEnabled(ctx context.Context, arg Upda return i, err } -const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec -UPDATE virtual_game_sessions -SET status = $2, +const UpdateVirtualGameProviderReportByDate = `-- name: UpdateVirtualGameProviderReportByDate :exec +UPDATE virtual_game_provider_reports +SET + total_games_played = total_games_played + $4, + total_bets = total_bets + $5, + total_payouts = total_payouts + $6, + total_players = total_players + $7, updated_at = CURRENT_TIMESTAMP -WHERE id = $1 +WHERE + provider_id = $1 + AND report_date = $2 + AND report_type = $3 ` -type UpdateVirtualGameSessionStatusParams struct { - ID int64 `json:"id"` - Status string `json:"status"` +type UpdateVirtualGameProviderReportByDateParams struct { + ProviderID string `json:"provider_id"` + ReportDate pgtype.Date `json:"report_date"` + ReportType pgtype.Text `json:"report_type"` + TotalGamesPlayed pgtype.Int8 `json:"total_games_played"` + TotalBets pgtype.Numeric `json:"total_bets"` + TotalPayouts pgtype.Numeric `json:"total_payouts"` + TotalPlayers pgtype.Int8 `json:"total_players"` } -func (q *Queries) UpdateVirtualGameSessionStatus(ctx context.Context, arg UpdateVirtualGameSessionStatusParams) error { - _, err := q.db.Exec(ctx, UpdateVirtualGameSessionStatus, arg.ID, arg.Status) +func (q *Queries) UpdateVirtualGameProviderReportByDate(ctx context.Context, arg UpdateVirtualGameProviderReportByDateParams) error { + _, err := q.db.Exec(ctx, UpdateVirtualGameProviderReportByDate, + arg.ProviderID, + arg.ReportDate, + arg.ReportType, + arg.TotalGamesPlayed, + arg.TotalBets, + arg.TotalPayouts, + arg.TotalPlayers, + ) return err } diff --git a/go.mod b/go.mod index 9001f12..207087d 100644 --- a/go.mod +++ b/go.mod @@ -81,19 +81,11 @@ require ( require github.com/twilio/twilio-go v1.26.3 require ( - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang/mock v1.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/redis/go-redis/v9 v9.10.0 // direct go.uber.org/atomic v1.9.0 // indirect ) -require ( - github.com/pierrec/lz4/v4 v4.1.15 // indirect - github.com/segmentio/kafka-go v0.4.48 // direct -) - // require github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f // require github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f // direct diff --git a/go.sum b/go.sum index 868b34b..d639aba 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f h1:UOp9At84RG8OT2Nw2TQidYWaoW1rAfTqChOJLdhYcm8= -github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f/go.mod h1:N2NQ6ad3i+oLQU+MlPci2f7mx6Mtg+wUcvzCv77KCl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= @@ -12,17 +10,11 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= @@ -31,8 +23,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -91,7 +81,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= @@ -129,15 +118,11 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= -github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= -github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/resend/resend-go/v2 v2.20.0 h1:MrIrgV0aHhwRgmcRPw33Nexn6aGJvCvG2XwfFpAMBGM= github.com/resend/resend-go/v2 v2.20.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -150,8 +135,6 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/segmentio/kafka-go v0.4.48 h1:9jyu9CWK4W5W+SroCe8EffbrRZVqAOkuaLd/ApID4Vs= -github.com/segmentio/kafka-go v0.4.48/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -215,12 +198,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -233,15 +214,11 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -257,25 +234,16 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -284,7 +252,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/config/config.go b/internal/config/config.go index 3df630f..329cf75 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -47,8 +47,9 @@ var ( ) type EnetPulseConfig struct { - UserName string `mapstructure:"username"` // "https://api.aleaplay.com" - Token string `mapstructure:"token"` // Your operator ID with Alea + UserName string `mapstructure:"username"` + Token string `mapstructure:"token"` + ProviderID string `mapstructure:"provider_id"` } type AleaPlayConfig struct { @@ -136,6 +137,7 @@ type Config struct { AFRO_SMS_SENDER_NAME string AFRO_SMS_RECEIVER_PHONE_NUMBER string ADRO_SMS_HOST_URL string + CHAPA_WEBHOOK_SECRET string CHAPA_TRANSFER_TYPE string CHAPA_PAYMENT_TYPE string CHAPA_SECRET_KEY string @@ -144,6 +146,7 @@ type Config struct { CHAPA_ENCRYPTION_KEY string CHAPA_CALLBACK_URL string CHAPA_RETURN_URL string + CHAPA_RECEIPT_URL string Bet365Token string EnetPulseConfig EnetPulseConfig PopOK domain.PopOKConfig @@ -192,6 +195,7 @@ func (c *Config) loadEnv() error { c.EnetPulseConfig.Token = os.Getenv("ENETPULSE_TOKEN") c.EnetPulseConfig.UserName = os.Getenv("ENETPULSE_USERNAME") + c.EnetPulseConfig.ProviderID = os.Getenv("ENETPULSE_PROVIDER_ID") c.CHAPA_TRANSFER_TYPE = os.Getenv("CHAPA_TRANSFER_TYPE") c.CHAPA_PAYMENT_TYPE = os.Getenv("CHAPA_PAYMENT_TYPE") @@ -260,10 +264,12 @@ func (c *Config) loadEnv() error { c.TELEBIRR.TelebirrCallbackURL = os.Getenv("TELEBIRR_CALLBACK_URL") //Chapa + c.CHAPA_WEBHOOK_SECRET = os.Getenv("CHAPA_WEBHOOK_SECRET") c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY") c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY") c.CHAPA_ENCRYPTION_KEY = os.Getenv("CHAPA_ENCRYPTION_KEY") c.CHAPA_BASE_URL = os.Getenv("CHAPA_BASE_URL") + c.CHAPA_RECEIPT_URL = os.Getenv("CHAPA_RECEIPT_URL") if c.CHAPA_BASE_URL == "" { c.CHAPA_BASE_URL = "https://api.chapa.co/v1" } @@ -431,34 +437,34 @@ func (c *Config) loadEnv() error { Platform: popOKPlatform, } - AtlasBaseUrl := os.Getenv("ATLAS_BASE_URL") - if AtlasBaseUrl == "" { - return ErrInvalidAtlasBaseUrl - } - AtlasSecretKey := os.Getenv("ATLAS_SECRET_KEY") - if AtlasSecretKey == "" { - return ErrInvalidAtlasSecretKey - } - AtlasBrandID := os.Getenv("ATLAS_BRAND_ID") - if AtlasBrandID == "" { - return ErrInvalidAtlasBrandID - } - AtlasPartnerID := os.Getenv("ATLAS_PARTNER_ID") - if AtlasPartnerID == "" { - return ErrInvalidAtlasPartnerID - } - AtlasOperatorID := os.Getenv("ATLAS_OPERATOR_ID") - if AtlasOperatorID == "" { - return ErrInvalidAtlasOperatorID - } + // AtlasBaseUrl := os.Getenv("ATLAS_BASE_URL") + // if AtlasBaseUrl == "" { + // return ErrInvalidAtlasBaseUrl + // } + // AtlasSecretKey := os.Getenv("ATLAS_SECRET_KEY") + // if AtlasSecretKey == "" { + // return ErrInvalidAtlasSecretKey + // } + // AtlasBrandID := os.Getenv("ATLAS_BRAND_ID") + // if AtlasBrandID == "" { + // return ErrInvalidAtlasBrandID + // } + // AtlasPartnerID := os.Getenv("ATLAS_PARTNER_ID") + // if AtlasPartnerID == "" { + // return ErrInvalidAtlasPartnerID + // } + // AtlasOperatorID := os.Getenv("ATLAS_OPERATOR_ID") + // if AtlasOperatorID == "" { + // return ErrInvalidAtlasOperatorID + // } - c.Atlas = AtlasConfig{ - BaseURL: AtlasBaseUrl, - SecretKey: AtlasSecretKey, - CasinoID: AtlasBrandID, - PartnerID: AtlasPartnerID, - OperatorID: AtlasOperatorID, - } + // c.Atlas = AtlasConfig{ + // BaseURL: AtlasBaseUrl, + // SecretKey: AtlasSecretKey, + // CasinoID: AtlasBrandID, + // PartnerID: AtlasPartnerID, + // OperatorID: AtlasOperatorID, + // } betToken := os.Getenv("BET365_TOKEN") if betToken == "" { diff --git a/internal/domain/arifpay.go b/internal/domain/arifpay.go index 94ac010..2be1616 100644 --- a/internal/domain/arifpay.go +++ b/internal/domain/arifpay.go @@ -59,12 +59,12 @@ type WebhookRequest struct { SessionID string `json:"sessionId"` } -type ArifpayB2CRequest struct{ - PhoneNumber string `json:"Phonenumber"` - Amount float64 `json:"amount" binding:"required"` - CustomerEmail string `json:"customerEmail" binding:"required"` - CustomerPhone string `json:"customerPhone" binding:"required"` -} +// type ArifpayB2CRequest struct{ +// PhoneNumber string `json:"Phonenumber"` +// Amount float64 `json:"amount" binding:"required"` +// CustomerEmail string `json:"customerEmail" binding:"required"` +// // CustomerPhone string `json:"customerPhone" binding:"required"` +// } type ArifpayVerifyByTransactionIDRequest struct{ TransactionId string `json:"transactionId"` diff --git a/internal/domain/atlas.go b/internal/domain/atlas.go index 66f8cb7..bf3fa47 100644 --- a/internal/domain/atlas.go +++ b/internal/domain/atlas.go @@ -46,7 +46,7 @@ type AtlasBetResponse struct { type AtlasBetWinRequest struct { Game string `json:"game"` CasinoID string `json:"casino_id"` - RoundID int64 `json:"round_id"` + RoundID string `json:"round_id"` PlayerID string `json:"player_id"` SessionID string `json:"session_id"` BetAmount float64 `json:"betAmount"` diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index d1451cd..ef749d9 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -33,7 +33,7 @@ const ( PaymentStatusFailed PaymentStatus = "failed" ) -type ChapaDepositRequest struct { +type ChapaInitDepositRequest struct { Amount Currency `json:"amount"` Currency string `json:"currency"` Email string `json:"email"` @@ -42,6 +42,8 @@ type ChapaDepositRequest struct { TxRef string `json:"tx_ref"` CallbackURL string `json:"callback_url"` ReturnURL string `json:"return_url"` + PhoneNumber string `json:"phone_number"` + // PhoneNumber string `json:"phone_number"` } type ChapaDepositRequestPayload struct { @@ -49,10 +51,16 @@ type ChapaDepositRequestPayload struct { } type ChapaWebhookPayload struct { - TxRef string `json:"tx_ref"` - Amount Currency `json:"amount"` - Currency string `json:"currency"` - Status PaymentStatus `json:"status"` + TxRef string `json:"trx_ref"` + Amount Currency `json:"amount"` + // Currency string `json:"currency"` + Status PaymentStatus `json:"status"` +} + +type ChapaPaymentWebhookRequest struct { + TxRef string `json:"trx_ref"` + RefId string `json:"ref_id"` + Status PaymentStatus `json:"status"` } // PaymentResponse contains the response from payment initialization @@ -68,11 +76,117 @@ type ChapaDepositVerification struct { Currency string } -type ChapaVerificationResponse struct { - Status string `json:"status"` - Amount float64 `json:"amount"` - Currency string `json:"currency"` - TxRef string `json:"tx_ref"` +type ChapaPaymentVerificationResponse struct { + Message string `json:"message"` + Status string `json:"status"` + Data struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + Currency string `json:"currency"` + Amount float64 `json:"amount"` + Charge float64 `json:"charge"` + Mode string `json:"mode"` + Method string `json:"method"` + Type string `json:"type"` + Status string `json:"status"` + Reference string `json:"reference"` + TxRef string `json:"tx_ref"` + Customization struct { + Title string `json:"title"` + Description string `json:"description"` + Logo interface{} `json:"logo"` + } `json:"customization"` + Meta interface{} `json:"meta"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } `json:"data"` +} + +type ChapaTransferVerificationResponse struct { + Message string `json:"message"` + Status string `json:"status"` + Data struct { + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + Mobile interface{} `json:"mobile"` + Currency string `json:"currency"` + Amount float64 `json:"amount"` + Charge float64 `json:"charge"` + Mode string `json:"mode"` + TransferMethod string `json:"transfer_method"` + Narration interface{} `json:"narration"` + ChapaTransferID string `json:"chapa_transfer_id"` + BankCode int `json:"bank_code"` + BankName string `json:"bank_name"` + CrossPartyReference interface{} `json:"cross_party_reference"` + IPAddress string `json:"ip_address"` + Status string `json:"status"` + TxRef string `json:"tx_ref"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } `json:"data"` +} + +type ChapaAllTransactionsResponse struct { + Message string `json:"message"` + Status string `json:"status"` + Data struct { + Transactions []struct { + Status string `json:"status"` + RefID string `json:"ref_id"` + Type string `json:"type"` + CreatedAt string `json:"created_at"` + Currency string `json:"currency"` + Amount string `json:"amount"` + Charge string `json:"charge"` + TransID *string `json:"trans_id"` + PaymentMethod string `json:"payment_method"` + Customer struct { + ID int64 `json:"id"` + Email *string `json:"email"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + Mobile *string `json:"mobile"` + } `json:"customer"` + } `json:"transactions"` + Pagination struct { + PerPage int `json:"per_page"` + CurrentPage int `json:"current_page"` + FirstPageURL string `json:"first_page_url"` + NextPageURL *string `json:"next_page_url"` + PrevPageURL *string `json:"prev_page_url"` + } `json:"pagination"` + } `json:"data"` +} + +type ChapaTransactionEvent struct { + Item int64 `json:"item"` + Message string `json:"message"` + Type string `json:"type"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ChapaTransaction struct { + Status string `json:"status"` + RefID string `json:"ref_id"` + Type string `json:"type"` + CreatedAt string `json:"created_at"` + Currency string `json:"currency"` + Amount string `json:"amount"` + Charge string `json:"charge"` + TransID *string `json:"trans_id"` + PaymentMethod string `json:"payment_method"` + Customer ChapaCustomer `json:"customer"` +} + +type ChapaCustomer struct { + ID int64 `json:"id"` + Email *string `json:"email"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + Mobile *string `json:"mobile"` } // type Bank struct { @@ -93,6 +207,57 @@ type ChapaVerificationResponse struct { // BankLogo string `json:"bank_logo"` // URL or base64 // } +type SwapResponse struct { + Message string `json:"message"` + Status string `json:"status"` + Data struct { + Status string `json:"status"` + RefID string `json:"ref_id"` + FromCurrency string `json:"from_currency"` + ToCurrency string `json:"to_currency"` + Amount float64 `json:"amount"` + ExchangedAmount float64 `json:"exchanged_amount"` + Charge float64 `json:"charge"` + Rate float64 `json:"rate"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } `json:"data"` +} + +type ChapaTransfersListResponse struct { + Message string `json:"message"` + Status string `json:"status"` + Meta struct { + CurrentPage int `json:"current_page"` + FirstPageURL string `json:"first_page_url"` + LastPage int `json:"last_page"` + LastPageURL string `json:"last_page_url"` + NextPageURL string `json:"next_page_url"` + Path string `json:"path"` + PerPage int `json:"per_page"` + PrevPageURL interface{} `json:"prev_page_url"` + To int `json:"to"` + Total int `json:"total"` + Error []interface{} `json:"error"` + } `json:"meta"` + Data []struct { + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + Currency string `json:"currency"` + Amount float64 `json:"amount"` + Charge float64 `json:"charge"` + TransferType string `json:"transfer_type"` + ChapaReference string `json:"chapa_reference"` + BankCode int `json:"bank_code"` + BankName string `json:"bank_name"` + BankReference interface{} `json:"bank_reference"` + Status string `json:"status"` + Reference interface{} `json:"reference"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } `json:"data"` +} + type BankResponse struct { Message string `json:"message"` Status string `json:"status"` @@ -157,42 +322,69 @@ type ChapaTransactionType struct { Type string `json:"type"` } -type ChapaWebHookTransfer struct { - AccountName string `json:"account_name"` - AccountNumber string `json:"account_number"` - BankId string `json:"bank_id"` - BankName string `json:"bank_name"` - Currency string `json:"currency"` - Amount string `json:"amount"` - Type string `json:"type"` - Status string `json:"status"` - Reference string `json:"reference"` - TxRef string `json:"tx_ref"` - ChapaReference string `json:"chapa_reference"` - CreatedAt time.Time `json:"created_at"` +type ChapaWebhookTransfer struct { + Event string `json:"event"` + Type string `json:"type"` + AccountName string `json:"account_name"` + AccountNumber string `json:"account_number"` + BankID int `json:"bank_id"` + BankName string `json:"bank_name"` + Amount string `json:"amount"` + Charge string `json:"charge"` + Currency string `json:"currency"` + Status string `json:"status"` + Reference string `json:"reference"` + ChapaReference string `json:"chapa_reference"` + BankReference string `json:"bank_reference"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } -type ChapaWebHookPayment struct { - Event string `json:"event"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` - Mobile interface{} `json:"mobile"` - Currency string `json:"currency"` - Amount string `json:"amount"` - Charge string `json:"charge"` - Status string `json:"status"` - Mode string `json:"mode"` - Reference string `json:"reference"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Type string `json:"type"` - TxRef string `json:"tx_ref"` - PaymentMethod string `json:"payment_method"` - Customization struct { - Title interface{} `json:"title"` - Description interface{} `json:"description"` - Logo interface{} `json:"logo"` - } `json:"customization"` - Meta string `json:"meta"` +type ChapaWebhookPayment struct { + Event string `json:"event"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email *string `json:"email,omitempty"` + Mobile string `json:"mobile"` + Currency string `json:"currency"` + Amount string `json:"amount"` + Charge string `json:"charge"` + Status string `json:"status"` + Mode string `json:"mode"` + Reference string `json:"reference"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Type string `json:"type"` + TxRef string `json:"tx_ref"` + PaymentMethod string `json:"payment_method"` + Customization ChapaWebhookCustomization `json:"customization"` + Meta interface{} `json:"meta"` // may vary in structure, so kept flexible +} + +type ChapaWebhookCustomization struct { + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + Logo *string `json:"logo,omitempty"` +} + +type Balance struct { + Currency string `json:"currency"` + AvailableBalance float64 `json:"available_balance"` + LedgerBalance float64 `json:"ledger_balance"` +} + +type SwapRequest struct { + From string `json:"from"` + To string `json:"to"` + Amount float64 `json:"amount"` +} + +type ChapaCancelResponse struct { + Message string `json:"message"` + Status string `json:"status"` + TxRef string `json:"tx_ref"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } diff --git a/internal/domain/enet_pulse.go b/internal/domain/enet_pulse.go index aae67ea..00725d9 100644 --- a/internal/domain/enet_pulse.go +++ b/internal/domain/enet_pulse.go @@ -121,9 +121,9 @@ type TournamentStageParticipantsResponse struct { } type DailyEventsRequest struct { - SportFK int // one of these three required - TournamentTemplateFK int - TournamentStageFK int + SportFK int // one of these three required + TournamentTemplateFK int + // TournamentStageFK int Date string // YYYY-MM-DD optional Live string // yes/no optional IncludeVenue string // yes/no optional @@ -145,9 +145,9 @@ type DailyEventsResponse struct { } type FixturesRequest struct { - SportFK int - TournamentTemplateFK int - TournamentStageFK int + SportFK int + TournamentTemplateFK int + // TournamentStageFK int LanguageTypeFK int Date string // YYYY-MM-DD Live string // "yes" | "no" @@ -170,9 +170,9 @@ type FixtureEvent struct { } type ResultsRequest struct { - SportFK int - TournamentTemplateFK int - TournamentStageFK int + SportFK int + TournamentTemplateFK int + // TournamentStageFK int LanguageTypeFK int Date string // YYYY-MM-DD Live string // "yes" | "no" @@ -224,8 +224,8 @@ type EventDetailsResponse struct { } type EventListRequest struct { - TournamentFK int // optional - TournamentStageFK int // optional + TournamentFK int // optional + // TournamentStageFK int // optional IncludeEventProperties bool // default true StatusType string // e.g. "finished", "inprogress" IncludeVenue bool @@ -250,11 +250,11 @@ type ParticipantFixturesRequest struct { SportFK int TournamentFK int TournamentTemplateFK int - TournamentStageFK int - Date string - Live string - Limit int - Offset int + // TournamentStageFK int + Date string + Live string + Limit int + Offset int IncludeVenue bool IncludeCountryCodes bool @@ -462,3 +462,308 @@ type CreateEnetpulseTournamentStage struct { CountryName string `json:"country_name"` // country name from API Status int `json:"status"` // active/inactive } + +// For insertion +type CreateEnetpulseFixture struct { + FixtureID string + Name string + SportFK string + TournamentFK string + TournamentTemplateFK string + TournamentStageFK string + TournamentStageName string + TournamentName string + TournamentTemplateName string + SportName string + Gender string + StartDate time.Time + StatusType string + StatusDescFK string + RoundTypeFK string + UpdatesCount int + LastUpdatedAt time.Time +} + +// Full domain model +type EnetpulseFixture struct { + FixtureID string `json:"id"` + Name string `json:"name"` + SportFK string `json:"sportFK"` + TournamentFK string `json:"tournamentFK"` + TournamentTemplateFK string `json:"tournament_templateFK"` + // TournamentStageFK string `json:"tournament_stageFK"` + TournamentStageName string `json:"tournament_stage_name"` + TournamentName string `json:"tournament_name"` + TournamentTemplateName string `json:"tournament_template_name"` + SportName string `json:"sport_name"` + Gender string `json:"gender"` + StartDate string `json:"startdate"` // ISO 8601 + StatusType string `json:"status_type"` + StatusDescFK string `json:"status_descFK"` + RoundTypeFK string `json:"round_typeFK"` + UpdatesCount string `json:"n"` // convert to int + LastUpdatedAt string `json:"ut"` // parse to time.Time +} + +type CreateEnetpulseResult struct { + ResultID string `json:"result_id"` + Name string `json:"name"` + SportFK string `json:"sport_fk"` + TournamentFK string `json:"tournament_fk"` + TournamentTemplateFK string `json:"tournament_template_fk"` + // TournamentStageFK string `json:"tournament_stage_fk"` + TournamentStageName string `json:"tournament_stage_name"` + TournamentName string `json:"tournament_name"` + TournamentTemplateName string `json:"tournament_template_name"` + SportName string `json:"sport_name"` + StartDate time.Time `json:"start_date"` + StatusType string `json:"status_type"` + StatusDescFK string `json:"status_desc_fk"` + RoundTypeFK string `json:"round_type_fk"` + UpdatesCount int32 `json:"updates_count"` + LastUpdatedAt time.Time `json:"last_updated_at"` + + // Optional metadata + Round string `json:"round"` + Live string `json:"live"` + VenueName string `json:"venue_name"` + LivestatsPlus string `json:"livestats_plus"` + LivestatsType string `json:"livestats_type"` + Commentary string `json:"commentary"` + LineupConfirmed bool `json:"lineup_confirmed"` + Verified bool `json:"verified"` + Spectators int32 `json:"spectators"` + + // Time-related metadata + GameStarted *time.Time `json:"game_started"` + FirstHalfEnded *time.Time `json:"first_half_ended"` + SecondHalfStarted *time.Time `json:"second_half_started"` + SecondHalfEnded *time.Time `json:"second_half_ended"` + GameEnded *time.Time `json:"game_ended"` +} + +// ✅ Used for reading result records +type EnetpulseResult struct { + ID int64 `json:"id"` + ResultID string `json:"result_id"` + Name string `json:"name"` + SportFK string `json:"sport_fk"` + TournamentFK string `json:"tournament_fk"` + TournamentTemplateFK string `json:"tournament_template_fk"` + // TournamentStageFK string `json:"tournament_stage_fk"` + TournamentStageName string `json:"tournament_stage_name"` + TournamentName string `json:"tournament_name"` + TournamentTemplateName string `json:"tournament_template_name"` + SportName string `json:"sport_name"` + StartDate time.Time `json:"start_date"` + StatusType string `json:"status_type"` + StatusDescFK string `json:"status_desc_fk"` + RoundTypeFK string `json:"round_type_fk"` + UpdatesCount int32 `json:"updates_count"` + LastUpdatedAt *time.Time `json:"last_updated_at"` + + Round string `json:"round"` + Live string `json:"live"` + VenueName string `json:"venue_name"` + LivestatsPlus string `json:"livestats_plus"` + LivestatsType string `json:"livestats_type"` + Commentary string `json:"commentary"` + LineupConfirmed bool `json:"lineup_confirmed"` + Verified bool `json:"verified"` + Spectators int32 `json:"spectators"` + + GameStarted *time.Time `json:"game_started"` + FirstHalfEnded *time.Time `json:"first_half_ended"` + SecondHalfStarted *time.Time `json:"second_half_started"` + SecondHalfEnded *time.Time `json:"second_half_ended"` + GameEnded *time.Time `json:"game_ended"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` +} + +type EnetpulseOutcomeType struct { + ID int64 `json:"id"` + OutcomeTypeID string `json:"outcome_type_id"` // changed from int64 → string + Name string `json:"name"` + Description string `json:"description"` + UpdatesCount int32 `json:"updates_count"` + LastUpdatedAt time.Time `json:"last_updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// CreateEnetpulseOutcomeType represents the payload to create or update an outcome type. +type CreateEnetpulseOutcomeType struct { + OutcomeTypeID string `json:"outcome_type_id"` // changed from int64 → string + Name string `json:"name"` + Description string `json:"description"` + UpdatesCount int32 `json:"updates_count"` + LastUpdatedAt time.Time `json:"last_updated_at"` +} + +type EnetpulsePreoddsTemp struct { + PreoddsID string `json:"preodds_id"` + EventFK string `json:"event_fk"` + OutcomeTypeFK string `json:"outcome_type_fk"` + OutcomeScopeFK string `json:"outcome_scope_fk"` + OutcomeSubtypeFK string `json:"outcome_subtype_fk"` + EventParticipantNumber int `json:"event_participant_number"` + IParam string `json:"iparam"` + IParam2 string `json:"iparam2"` + DParam string `json:"dparam"` + DParam2 string `json:"dparam2"` + SParam string `json:"sparam"` + UpdatesCount int `json:"updates_count"` + LastUpdatedAt time.Time `json:"last_updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// CreateEnetpulsePreodds is used when inserting a new preodds record +type CreateEnetpulsePreodds struct { + PreoddsID string `json:"preodds_id"` + EventFK string `json:"event_fk"` + OutcomeTypeFK string `json:"outcome_type_fk"` + OutcomeScopeFK string `json:"outcome_scope_fk"` + OutcomeSubtypeFK string `json:"outcome_subtype_fk"` + EventParticipantNumber int `json:"event_participant_number"` + IParam string `json:"iparam"` + IParam2 string `json:"iparam2"` + DParam string `json:"dparam"` + DParam2 string `json:"dparam2"` + SParam string `json:"sparam"` + UpdatesCount int `json:"updates_count"` + LastUpdatedAt time.Time `json:"last_updated_at"` +} + +type CreateEnetpulsePreoddsBettingOffer struct { + BettingOfferID string + PreoddsFK string + BettingOfferStatusFK int32 + OddsProviderFK int32 + Odds float64 + OddsOld float64 + Active string + CouponKey string + UpdatesCount int + LastUpdatedAt time.Time +} + +// EnetpulsePreoddsBettingOffer represents the DB record of a betting offer +type EnetpulsePreoddsBettingOffer struct { + ID int64 `json:"id"` + BettingOfferID string `json:"betting_offer_id"` + PreoddsFK string `json:"preodds_fk"` + BettingOfferStatusFK int32 `json:"betting_offer_status_fk"` + OddsProviderFK int32 `json:"odds_provider_fk"` + Odds float64 `json:"odds"` + OddsOld float64 `json:"odds_old"` + Active string `json:"active"` + CouponKey string `json:"coupon_key"` + UpdatesCount int `json:"updates_count"` + LastUpdatedAt time.Time `json:"last_updated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type EnetpulseFixtureWithPreodds struct { + FixtureID string + FixtureApiID string + FixtureName string + SportFk string + TournamentFk string + TournamentTemplateFk string + TournamentStageFk string + StartDate time.Time + StatusType string + StatusDescFk string + RoundTypeFk string + UpdatesCount int32 + LastUpdatedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time + Preodds []EnetpulsePreodds +} + +type EnetpulsePreodds struct { + ID int64 + PreoddsID string + EventFK int64 + OutcomeTypeFK int32 + OutcomeScopeFK int32 + OutcomeSubtypeFK int32 + EventParticipantNumber int32 + IParam string + IParam2 string + DParam string + DParam2 string + SParam string + UpdatesCount int32 + LastUpdatedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time + BettingOffers []EnetpulsePreoddsBettingOffer +} + +type EnetpulseResultParticipant struct { + ID int64 `json:"id"` + ParticipantMapID string `json:"participant_map_id"` + ResultFk string `json:"result_fk"` + ParticipantFk string `json:"participant_fk"` + Number int32 `json:"number"` + Name string `json:"name"` + Gender string `json:"gender"` + Type string `json:"type"` + CountryFk string `json:"country_fk"` + CountryName string `json:"country_name"` + OrdinaryTime string `json:"ordinary_time"` + RunningScore string `json:"running_score"` + Halftime string `json:"halftime"` + FinalResult string `json:"final_result"` + LastUpdatedAt time.Time `json:"last_updated_at"` + CreatedAt time.Time `json:"created_at"` +} + +// CreateEnetpulseResultParticipant is the payload for inserting or updating a participant record. +type CreateEnetpulseResultParticipant struct { + ParticipantMapID string `json:"participant_map_id"` + ResultFk string `json:"result_fk"` + ParticipantFk string `json:"participant_fk"` + Number int32 `json:"number"` + Name string `json:"name"` + Gender string `json:"gender"` + Type string `json:"type"` + CountryFk string `json:"country_fk"` + CountryName string `json:"country_name"` + OrdinaryTime string `json:"ordinary_time"` + RunningScore string `json:"running_score"` + Halftime string `json:"halftime"` + FinalResult string `json:"final_result"` + LastUpdatedAt time.Time `json:"last_updated_at"` +} + +type EnetpulseResultReferee struct { + ID int64 `json:"id"` + ResultFk string `json:"result_fk"` + RefereeFk string `json:"referee_fk"` + Assistant1RefereeFk string `json:"assistant1_referee_fk"` + Assistant2RefereeFk string `json:"assistant2_referee_fk"` + FourthRefereeFk string `json:"fourth_referee_fk"` + Var1RefereeFk string `json:"var1_referee_fk"` + Var2RefereeFk string `json:"var2_referee_fk"` + LastUpdatedAt time.Time `json:"last_updated_at"` + CreatedAt time.Time `json:"created_at"` +} + +// CreateEnetpulseResultReferee is the payload for inserting or updating referee assignments. +type CreateEnetpulseResultReferee struct { + ResultFk string `json:"result_fk"` + RefereeFk string `json:"referee_fk"` + Assistant1RefereeFk string `json:"assistant1_referee_fk"` + Assistant2RefereeFk string `json:"assistant2_referee_fk"` + FourthRefereeFk string `json:"fourth_referee_fk"` + Var1RefereeFk string `json:"var1_referee_fk"` + Var2RefereeFk string `json:"var2_referee_fk"` + LastUpdatedAt time.Time `json:"last_updated_at"` +} diff --git a/internal/domain/veli_games.go b/internal/domain/veli_games.go index db6ad6e..0d02e4b 100644 --- a/internal/domain/veli_games.go +++ b/internal/domain/veli_games.go @@ -58,10 +58,10 @@ type GameStartRequest struct { Country string `json:"country"` IP string `json:"ip"` BrandID string `json:"brandId"` - UserAgent string `json:"userAgent,omitempty"` - LobbyURL string `json:"lobbyUrl,omitempty"` - CashierURL string `json:"cashierUrl,omitempty"` - PlayerName string `json:"playerName,omitempty"` + // UserAgent string `json:"userAgent,omitempty"` + // LobbyURL string `json:"lobbyUrl,omitempty"` + // CashierURL string `json:"cashierUrl,omitempty"` + // PlayerName string `json:"playerName,omitempty"` } type DemoGameRequest struct { @@ -71,8 +71,8 @@ type DemoGameRequest struct { DeviceType string `json:"deviceType"` IP string `json:"ip"` BrandID string `json:"brandId"` - PlayerID string `json:"playerId,omitempty"` - Country string `json:"country,omitempty"` + // PlayerID string `json:"playerId,omitempty"` + // Country string `json:"country,omitempty"` } type GameStartResponse struct { diff --git a/internal/domain/virtual_game.go b/internal/domain/virtual_game.go index 9929c5d..0a8bcbd 100644 --- a/internal/domain/virtual_game.go +++ b/internal/domain/virtual_game.go @@ -316,3 +316,61 @@ type UnifiedGame struct { Status int `json:"status,omitempty"` DemoURL string `json:"demoUrl"` } + +type CreateVirtualGameProviderReport struct { + ProviderID string `json:"provider_id"` + ReportDate time.Time `json:"report_date"` + TotalGamesPlayed int64 `json:"total_games_played"` + TotalBets float64 `json:"total_bets"` + TotalPayouts float64 `json:"total_payouts"` + TotalPlayers int64 `json:"total_players"` + ReportType string `json:"report_type"` // e.g., "daily", "weekly" +} + +type VirtualGameProviderReport struct { + ID int64 `json:"id"` + ProviderID string `json:"provider_id"` + ReportDate time.Time `json:"report_date"` + TotalGamesPlayed int64 `json:"total_games_played"` + TotalBets float64 `json:"total_bets"` + TotalPayouts float64 `json:"total_payouts"` + TotalProfit float64 `json:"total_profit"` + TotalPlayers int64 `json:"total_players"` + ReportType string `json:"report_type"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateVirtualGameReport struct { + GameID string `json:"game_id"` + ProviderID string `json:"provider_id"` + ReportDate time.Time `json:"report_date"` + TotalRounds int64 `json:"total_rounds"` + TotalBets float64 `json:"total_bets"` + TotalPayouts float64 `json:"total_payouts"` + TotalPlayers int64 `json:"total_players"` + ReportType string `json:"report_type"` // e.g., "daily", "weekly" +} + +type VirtualGameReport struct { + ID int64 `json:"id"` + GameID string `json:"game_id"` + ProviderID string `json:"provider_id"` + ReportDate time.Time `json:"report_date"` + TotalRounds int64 `json:"total_rounds"` + TotalBets float64 `json:"total_bets"` + TotalPayouts float64 `json:"total_payouts"` + TotalProfit float64 `json:"total_profit"` + TotalPlayers int64 `json:"total_players"` + ReportType string `json:"report_type"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateVirtualGameProviderReportsRequest struct { + Reports []CreateVirtualGameProviderReport +} + +type CreateVirtualGameReportsRequest struct { + Reports []CreateVirtualGameReport +} diff --git a/internal/repository/enet_pulse.go b/internal/repository/enet_pulse.go index 0ea921e..4d3b2df 100644 --- a/internal/repository/enet_pulse.go +++ b/internal/repository/enet_pulse.go @@ -3,6 +3,9 @@ package repository import ( "context" "fmt" + "math" + "math/big" + "strconv" "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" @@ -140,6 +143,295 @@ func (s *Store) GetTournamentStagesByTournamentFK(ctx context.Context, tournamen return stages, nil } +// Create a new fixture +func (s *Store) CreateEnetpulseFixture( + ctx context.Context, + fixture domain.CreateEnetpulseFixture, +) (domain.EnetpulseFixture, error) { + // Convert domain model to DB params (sqlc-generated struct or parameters) + dbFixture, err := s.queries.CreateEnetpulseFixture( + ctx, + ConvertCreateEnetpulseFixture(fixture), // your converter + ) + if err != nil { + return domain.EnetpulseFixture{}, err + } + return ConvertDBEnetpulseFixture(dbFixture), nil // convert DB row to domain +} + +// Fetch all fixtures +func (s *Store) GetAllEnetpulseFixtures(ctx context.Context) ([]domain.EnetpulseFixture, error) { + dbFixtures, err := s.queries.GetAllEnetpulseFixtures(ctx) + if err != nil { + return nil, err + } + + var fixtures []domain.EnetpulseFixture + for _, dbFixture := range dbFixtures { + fixtures = append(fixtures, ConvertDBEnetpulseFixture(dbFixture)) + } + + return fixtures, nil +} + +func (s *Store) CreateEnetpulseResult( + ctx context.Context, + result domain.CreateEnetpulseResult, +) (domain.EnetpulseResult, error) { + dbResult, err := s.queries.CreateEnetpulseResult( + ctx, + ConvertCreateEnetpulseResult(result), + ) + if err != nil { + return domain.EnetpulseResult{}, err + } + + return ConvertDBEnetpulseResult(dbResult), nil +} + +// GetAllEnetpulseResults retrieves all Enetpulse results. +func (s *Store) GetAllEnetpulseResults(ctx context.Context) ([]domain.EnetpulseResult, error) { + dbResults, err := s.queries.GetAllEnetpulseResults(ctx) + if err != nil { + return nil, err + } + + results := make([]domain.EnetpulseResult, 0, len(dbResults)) + for _, dbR := range dbResults { + results = append(results, ConvertDBEnetpulseResult(dbR)) + } + + return results, nil +} + +// CreateEnetpulseOutcomeType inserts or updates an EnetPulse outcome type record. +func (s *Store) CreateEnetpulseOutcomeType( + ctx context.Context, + outcomeType domain.CreateEnetpulseOutcomeType, +) (domain.EnetpulseOutcomeType, error) { + dbOutcome, err := s.queries.CreateEnetpulseOutcomeType( + ctx, + ConvertCreateEnetpulseOutcomeType(outcomeType), + ) + if err != nil { + return domain.EnetpulseOutcomeType{}, err + } + + return ConvertDBEnetpulseOutcomeType(dbOutcome), nil +} + +// GetAllEnetpulseOutcomeTypes retrieves all outcome types. +func (s *Store) GetAllEnetpulseOutcomeTypes(ctx context.Context) ([]domain.EnetpulseOutcomeType, error) { + dbOutcomes, err := s.queries.GetAllEnetpulseOutcomeTypes(ctx) + if err != nil { + return nil, err + } + + outcomes := make([]domain.EnetpulseOutcomeType, 0, len(dbOutcomes)) + for _, dbO := range dbOutcomes { + outcomes = append(outcomes, ConvertDBEnetpulseOutcomeType(dbO)) + } + + return outcomes, nil +} + +// CreateEnetpulsePreodds inserts or updates a preodds record. +func (s *Store) CreateEnetpulsePreodds( + ctx context.Context, + preodds domain.CreateEnetpulsePreodds, +) (domain.EnetpulsePreodds, error) { + + // Convert domain to DB params + params, err := ConvertCreateEnetpulsePreodds(preodds) + if err != nil { + return domain.EnetpulsePreodds{}, err + } + + // Insert into DB + dbPreodds, err := s.queries.CreateEnetpulsePreodds(ctx, params) + if err != nil { + return domain.EnetpulsePreodds{}, err + } + + return ConvertDBEnetpulsePreodds(dbPreodds), nil +} + +// GetAllEnetpulsePreodds retrieves all preodds records. +func (s *Store) GetAllEnetpulsePreodds(ctx context.Context) ([]domain.EnetpulsePreodds, error) { + dbPreodds, err := s.queries.GetAllEnetpulsePreodds(ctx) + if err != nil { + return nil, err + } + + preodds := make([]domain.EnetpulsePreodds, 0, len(dbPreodds)) + for _, dbP := range dbPreodds { + preodds = append(preodds, ConvertDBEnetpulsePreodds(dbP)) + } + + return preodds, nil +} + +// CreateEnetpulsePreoddsBettingOffer inserts or updates a betting offer +func (s *Store) CreateEnetpulsePreoddsBettingOffer( + ctx context.Context, + bettingOffer domain.CreateEnetpulsePreoddsBettingOffer, +) (domain.EnetpulsePreoddsBettingOffer, error) { + + params := ConvertCreateEnetpulsePreoddsBettingOffer(bettingOffer) + + dbOffer, err := s.queries.CreateEnetpulsePreoddsBettingOffer(ctx, params) + if err != nil { + return domain.EnetpulsePreoddsBettingOffer{}, err + } + + return ConvertDBEnetpulsePreoddsBettingOffer(dbOffer), nil +} + +// GetAllEnetpulsePreoddsBettingOffers retrieves all betting offers +func (s *Store) GetAllEnetpulsePreoddsBettingOffers(ctx context.Context) ([]domain.EnetpulsePreoddsBettingOffer, error) { + dbOffers, err := s.queries.GetAllEnetpulsePreoddsBettingOffers(ctx) + if err != nil { + return nil, err + } + + offers := make([]domain.EnetpulsePreoddsBettingOffer, 0, len(dbOffers)) + for _, dbO := range dbOffers { + offers = append(offers, ConvertDBEnetpulsePreoddsBettingOffer(dbO)) + } + + return offers, nil +} + +func (s *Store) GetAllEnetpulsePreoddsWithBettingOffers(ctx context.Context) ([]domain.EnetpulsePreodds, error) { + rows, err := s.queries.GetAllEnetpulsePreoddsWithBettingOffers(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch preodds with betting offers: %w", err) + } + + // Map for grouping betting offers under each Preodd + preoddsMap := make(map[string]*domain.EnetpulsePreodds) + + for _, row := range rows { + pid := row.PreoddsID + preodd, exists := preoddsMap[pid] + if !exists { + // Create the base Preodd entry + preodd = &domain.EnetpulsePreodds{ + ID: row.PreoddsDbID, + PreoddsID: row.PreoddsID, + EventFK: row.EventFk, + OutcomeTypeFK: row.OutcomeTypeFk.Int32, + OutcomeScopeFK: row.OutcomeScopeFk.Int32, + OutcomeSubtypeFK: row.OutcomeSubtypeFk.Int32, + EventParticipantNumber: row.EventParticipantNumber.Int32, + IParam: row.Iparam.String, + IParam2: row.Iparam2.String, + DParam: row.Dparam.String, + DParam2: row.Dparam2.String, + SParam: row.Sparam.String, + UpdatesCount: row.PreoddsUpdatesCount.Int32, + LastUpdatedAt: row.PreoddsLastUpdatedAt.Time, + CreatedAt: row.PreoddsCreatedAt.Time, + UpdatedAt: row.PreoddsUpdatedAt.Time, + BettingOffers: []domain.EnetpulsePreoddsBettingOffer{}, + } + preoddsMap[pid] = preodd + } + + // Append BettingOffer only if exists + if row.BettingofferID.Valid && row.BettingofferID.String != "" { + offer := domain.EnetpulsePreoddsBettingOffer{ + ID: row.BettingofferDbID.Int64, + BettingOfferID: row.BettingofferID.String, + BettingOfferStatusFK: row.BettingofferStatusFk.Int32, + OddsProviderFK: row.OddsProviderFk.Int32, + Odds: float64(row.Odds.Exp), + OddsOld: float64(row.OddsOld.Exp), + Active: fmt.Sprintf("%v", row.Active), + CouponKey: row.CouponKey.String, + UpdatesCount: int(row.BettingofferUpdatesCount.Int32), + LastUpdatedAt: row.BettingofferLastUpdatedAt.Time, + CreatedAt: row.BettingofferCreatedAt.Time, + UpdatedAt: row.BettingofferUpdatedAt.Time, + } + preodd.BettingOffers = append(preodd.BettingOffers, offer) + } + } + + // Convert map to slice + result := make([]domain.EnetpulsePreodds, 0, len(preoddsMap)) + for _, p := range preoddsMap { + result = append(result, *p) + } + + return result, nil +} + +func (s *Store) GetFixturesWithPreodds(ctx context.Context) ([]domain.EnetpulseFixtureWithPreodds, error) { + dbRows, err := s.queries.GetFixturesWithPreodds(ctx) + if err != nil { + return nil, err + } + + // Use a map to group preodds by fixture + fixtureMap := make(map[string]*domain.EnetpulseFixtureWithPreodds) + + for _, row := range dbRows { + // If fixture not yet in map, add it + if _, exists := fixtureMap[row.FixtureID]; !exists { + fixtureMap[row.FixtureID] = &domain.EnetpulseFixtureWithPreodds{ + FixtureID: row.FixtureID, + FixtureApiID: row.FixtureID, // same alias used in query + FixtureName: row.FixtureName, + SportFk: row.SportFk, + TournamentFk: row.TournamentFk.String, + TournamentTemplateFk: row.TournamentTemplateFk.String, + // TournamentStageFk: row.TournamentStageFk.String, + StartDate: row.StartDate.Time, + StatusType: row.StatusType.String, + StatusDescFk: row.StatusDescFk.String, + RoundTypeFk: row.RoundTypeFk.String, + UpdatesCount: row.FixtureUpdatesCount.Int32, + LastUpdatedAt: row.FixtureLastUpdatedAt.Time, + CreatedAt: row.FixtureCreatedAt.Time, + UpdatedAt: row.FixtureUpdatedAt.Time, + Preodds: []domain.EnetpulsePreodds{}, // initialize slice + } + } + + // Add preodds only if it exists (avoid NULL rows) + if row.PreoddsDbID.Valid { + preodds := domain.EnetpulsePreodds{ + ID: row.PreoddsDbID.Int64, + PreoddsID: row.PreoddsID.String, + EventFK: row.EventFk.Int64, + OutcomeTypeFK: row.OutcomeTypeFk.Int32, + OutcomeScopeFK: row.OutcomeScopeFk.Int32, + OutcomeSubtypeFK: row.OutcomeSubtypeFk.Int32, + EventParticipantNumber: row.EventParticipantNumber.Int32, + IParam: row.Iparam.String, + IParam2: row.Iparam2.String, + DParam: row.Dparam.String, + DParam2: row.Dparam2.String, + SParam: row.Sparam.String, + UpdatesCount: row.PreoddsUpdatesCount.Int32, + LastUpdatedAt: row.PreoddsLastUpdatedAt.Time, + CreatedAt: row.PreoddsCreatedAt.Time, + UpdatedAt: row.PreoddsUpdatedAt.Time, + } + fixtureMap[row.FixtureID].Preodds = append(fixtureMap[row.FixtureID].Preodds, preodds) + } + } + + // Flatten the map into a slice + result := make([]domain.EnetpulseFixtureWithPreodds, 0, len(fixtureMap)) + for _, f := range fixtureMap { + result = append(result, *f) + } + + return result, nil +} + // func ConvertCreateEnetpulseTournamentStage(stage domain.CreateEnetpulseTournamentStage) dbgen.EnetpulseTournamentStage { // return dbgen.EnetpulseTournamentStage{ // StageID: stage.StageID, @@ -157,6 +449,54 @@ func (s *Store) GetTournamentStagesByTournamentFK(ctx context.Context, tournamen // } // } +// ConvertCreateEnetpulseFixture converts the domain model to the SQLC params struct. +func ConvertCreateEnetpulseFixture(f domain.CreateEnetpulseFixture) dbgen.CreateEnetpulseFixtureParams { + return dbgen.CreateEnetpulseFixtureParams{ + FixtureID: f.FixtureID, + Name: f.Name, + SportFk: f.SportFK, + TournamentFk: pgtype.Text{String: f.TournamentFK, Valid: f.TournamentFK != ""}, + TournamentTemplateFk: pgtype.Text{String: f.TournamentTemplateFK, Valid: f.TournamentTemplateFK != ""}, + // TournamentStageFk: pgtype.Text{String: f.TournamentStageFK, Valid: f.TournamentStageFK != ""}, + // TournamentStageName: pgtype.Text{String: f.TournamentStageName, Valid: f.TournamentStageName != ""}, + TournamentName: pgtype.Text{String: f.TournamentName, Valid: f.TournamentName != ""}, + TournamentTemplateName: pgtype.Text{String: f.TournamentTemplateName, Valid: f.TournamentTemplateName != ""}, + SportName: pgtype.Text{String: f.SportName, Valid: f.SportName != ""}, + Gender: pgtype.Text{String: f.Gender, Valid: f.Gender != ""}, + StartDate: pgtype.Timestamptz{Time: f.StartDate, Valid: !f.StartDate.IsZero()}, + StatusType: pgtype.Text{String: f.StatusType, Valid: f.StatusType != ""}, + StatusDescFk: pgtype.Text{String: f.StatusDescFK, Valid: f.StatusDescFK != ""}, + RoundTypeFk: pgtype.Text{String: f.RoundTypeFK, Valid: f.RoundTypeFK != ""}, + UpdatesCount: pgtype.Int4{Int32: int32(f.UpdatesCount), Valid: true}, + LastUpdatedAt: pgtype.Timestamptz{Time: f.LastUpdatedAt, Valid: !f.LastUpdatedAt.IsZero()}, + } +} + +// ConvertDBEnetpulseFixture converts the DB row to the domain model. +func ConvertDBEnetpulseFixture(dbF dbgen.EnetpulseFixture) domain.EnetpulseFixture { + return domain.EnetpulseFixture{ + FixtureID: dbF.FixtureID, + Name: dbF.Name, + SportFK: dbF.SportFk, + TournamentFK: dbF.TournamentFk.String, + TournamentTemplateFK: dbF.TournamentTemplateFk.String, + // TournamentStageFK: dbF.TournamentStageFk.String, + // TournamentStageName: dbF.TournamentStageName.String, + TournamentName: dbF.TournamentName.String, + TournamentTemplateName: dbF.TournamentTemplateName.String, + SportName: dbF.SportName.String, + Gender: dbF.Gender.String, + StartDate: dbF.StartDate.Time.String(), + StatusType: dbF.StatusType.String, + StatusDescFK: dbF.StatusDescFk.String, + RoundTypeFK: dbF.RoundTypeFk.String, + UpdatesCount: fmt.Sprintf("%v", dbF.UpdatesCount), + LastUpdatedAt: dbF.LastUpdatedAt.Time.String(), + // CreatedAt: dbF.CreatedAt.Time, + // UpdatedAt: dbF.UpdatedAt.Time, + } +} + func ConvertCreateEnetpulseTournamentStage(stage domain.CreateEnetpulseTournamentStage) dbgen.CreateEnetpulseTournamentStageParams { return dbgen.CreateEnetpulseTournamentStageParams{ StageID: stage.StageID, @@ -321,3 +661,354 @@ func ConvertDBEnetpulseTournament(dbT dbgen.EnetpulseTournament) domain.Enetpuls }(), } } + +func ConvertCreateEnetpulseResult(input domain.CreateEnetpulseResult) dbgen.CreateEnetpulseResultParams { + return dbgen.CreateEnetpulseResultParams{ + ResultID: input.ResultID, + Name: input.Name, + SportFk: input.SportFK, + TournamentFk: pgtype.Text{String: input.TournamentFK, Valid: input.TournamentFK != ""}, + TournamentTemplateFk: pgtype.Text{String: input.TournamentTemplateFK, Valid: input.TournamentTemplateFK != ""}, + // TournamentStageFk: pgtype.Text{String: input.TournamentStageFK, Valid: input.TournamentStageFK != ""}, + // TournamentStageName: pgtype.Text{String: input.TournamentStageName, Valid: input.TournamentStageName != ""}, + TournamentName: pgtype.Text{String: input.TournamentName, Valid: input.TournamentName != ""}, + TournamentTemplateName: pgtype.Text{String: input.TournamentTemplateName, Valid: input.TournamentTemplateName != ""}, + SportName: pgtype.Text{String: input.SportName, Valid: input.SportName != ""}, + StartDate: pgtype.Timestamptz{Time: input.StartDate, Valid: !input.StartDate.IsZero()}, + StatusType: pgtype.Text{String: input.StatusType, Valid: input.StatusType != ""}, + StatusDescFk: pgtype.Text{String: input.StatusDescFK, Valid: input.StatusDescFK != ""}, + RoundTypeFk: pgtype.Text{String: input.RoundTypeFK, Valid: input.RoundTypeFK != ""}, + UpdatesCount: pgtype.Int4{Int32: int32(input.UpdatesCount), Valid: true}, + LastUpdatedAt: pgtype.Timestamptz{Time: input.LastUpdatedAt, Valid: !input.LastUpdatedAt.IsZero()}, + Round: pgtype.Text{String: input.Round, Valid: input.Round != ""}, + Live: pgtype.Text{String: input.Live, Valid: input.Live != ""}, + VenueName: pgtype.Text{String: input.VenueName, Valid: input.VenueName != ""}, + LivestatsPlus: pgtype.Text{String: input.LivestatsPlus, Valid: input.LivestatsPlus != ""}, + LivestatsType: pgtype.Text{String: input.LivestatsType, Valid: input.LivestatsType != ""}, + Commentary: pgtype.Text{String: input.Commentary, Valid: input.Commentary != ""}, + LineupConfirmed: pgtype.Bool{Bool: input.LineupConfirmed, Valid: true}, + Verified: pgtype.Bool{Bool: input.Verified, Valid: true}, + Spectators: pgtype.Int4{Int32: int32(input.Spectators), Valid: true}, + GameStarted: pgtype.Timestamptz{Time: *input.GameStarted, Valid: !input.GameStarted.IsZero()}, + FirstHalfEnded: pgtype.Timestamptz{Time: *input.FirstHalfEnded, Valid: !input.FirstHalfEnded.IsZero()}, + SecondHalfStarted: pgtype.Timestamptz{Time: *input.SecondHalfStarted, Valid: !input.SecondHalfStarted.IsZero()}, + SecondHalfEnded: pgtype.Timestamptz{Time: *input.SecondHalfEnded, Valid: !input.SecondHalfEnded.IsZero()}, + GameEnded: pgtype.Timestamptz{Time: *input.GameEnded, Valid: !input.GameEnded.IsZero()}, + } +} + +// ConvertDBEnetpulseResult maps SQLC result → domain model +func ConvertDBEnetpulseResult(db dbgen.EnetpulseResult) domain.EnetpulseResult { + return domain.EnetpulseResult{ + ID: db.ID, + ResultID: db.ResultID, + Name: db.Name, + SportFK: db.SportFk, + TournamentFK: db.TournamentFk.String, + TournamentTemplateFK: db.TournamentTemplateFk.String, + // TournamentStageFK: db.TournamentStageFk.String, + // TournamentStageName: db.TournamentStageName.String, + TournamentName: db.TournamentName.String, + TournamentTemplateName: db.TournamentTemplateName.String, + SportName: db.SportName.String, + StartDate: db.StartDate.Time, + StatusType: db.StatusType.String, + StatusDescFK: db.StatusDescFk.String, + RoundTypeFK: db.RoundTypeFk.String, + UpdatesCount: db.UpdatesCount.Int32, + LastUpdatedAt: &db.LastUpdatedAt.Time, + Round: db.Round.String, + Live: db.Live.String, + VenueName: db.VenueName.String, + LivestatsPlus: db.LivestatsPlus.String, + LivestatsType: db.LivestatsType.String, + Commentary: db.Commentary.String, + LineupConfirmed: db.LineupConfirmed.Bool, + Verified: db.Verified.Bool, + Spectators: db.Spectators.Int32, + GameStarted: &db.GameStarted.Time, + FirstHalfEnded: &db.FirstHalfEnded.Time, + SecondHalfStarted: &db.SecondHalfStarted.Time, + SecondHalfEnded: &db.SecondHalfEnded.Time, + GameEnded: &db.GameEnded.Time, + CreatedAt: db.CreatedAt.Time, + UpdatedAt: &db.UpdatedAt.Time, + } +} + +// ConvertCreateEnetpulseOutcomeType converts the domain struct to SQLC params. +func ConvertCreateEnetpulseOutcomeType(o domain.CreateEnetpulseOutcomeType) dbgen.CreateEnetpulseOutcomeTypeParams { + return dbgen.CreateEnetpulseOutcomeTypeParams{ + OutcomeTypeID: o.OutcomeTypeID, + Name: o.Name, + Description: pgtype.Text{String: o.Description, Valid: o.Description != ""}, // TODO: thiso.Description, + UpdatesCount: pgtype.Int4{Int32: int32(o.UpdatesCount), Valid: true}, + LastUpdatedAt: pgtype.Timestamptz{Time: o.LastUpdatedAt, Valid: !o.LastUpdatedAt.IsZero()}, + } +} + +// ConvertDBEnetpulseOutcomeType converts SQLC DB model to domain model. +func ConvertDBEnetpulseOutcomeType(dbO dbgen.EnetpulseOutcomeType) domain.EnetpulseOutcomeType { + return domain.EnetpulseOutcomeType{ + ID: dbO.ID, + OutcomeTypeID: dbO.OutcomeTypeID, + Name: dbO.Name, + Description: dbO.Description.String, + UpdatesCount: dbO.UpdatesCount.Int32, + LastUpdatedAt: dbO.LastUpdatedAt.Time, + CreatedAt: dbO.CreatedAt.Time, + UpdatedAt: dbO.UpdatedAt.Time, + } +} + +func ConvertCreateEnetpulsePreodds(p domain.CreateEnetpulsePreodds) (dbgen.CreateEnetpulsePreoddsParams, error) { + eventFK, err := strconv.ParseInt(p.EventFK, 10, 64) + if err != nil { + return dbgen.CreateEnetpulsePreoddsParams{}, fmt.Errorf("invalid EventFK: %w", err) + } + + outcomeTypeFK, err := strconv.ParseInt(p.OutcomeTypeFK, 10, 32) + if err != nil { + return dbgen.CreateEnetpulsePreoddsParams{}, fmt.Errorf("invalid OutcomeTypeFK: %w", err) + } + + outcomeScopeFK, err := strconv.ParseInt(p.OutcomeScopeFK, 10, 32) + if err != nil { + return dbgen.CreateEnetpulsePreoddsParams{}, fmt.Errorf("invalid OutcomeScopeFK: %w", err) + } + + outcomeSubtypeFK, err := strconv.ParseInt(p.OutcomeSubtypeFK, 10, 32) + if err != nil { + return dbgen.CreateEnetpulsePreoddsParams{}, fmt.Errorf("invalid OutcomeSubtypeFK: %w", err) + } + + return dbgen.CreateEnetpulsePreoddsParams{ + PreoddsID: p.PreoddsID, + EventFk: eventFK, + OutcomeTypeFk: pgtype.Int4{Int32: int32(outcomeTypeFK), Valid: true}, + OutcomeScopeFk: pgtype.Int4{Int32: int32(outcomeScopeFK), Valid: true}, + OutcomeSubtypeFk: pgtype.Int4{Int32: int32(outcomeSubtypeFK), Valid: true}, + EventParticipantNumber: pgtype.Int4{Int32: int32(p.EventParticipantNumber), Valid: true}, + Iparam: pgtype.Text{String: p.IParam, Valid: p.IParam != ""}, + Iparam2: pgtype.Text{String: p.IParam2, Valid: p.IParam2 != ""}, + Dparam: pgtype.Text{String: p.DParam, Valid: p.DParam != ""}, + Dparam2: pgtype.Text{String: p.DParam2, Valid: p.DParam2 != ""}, + Sparam: pgtype.Text{String: p.SParam, Valid: p.SParam != ""}, + UpdatesCount: pgtype.Int4{Int32: int32(p.UpdatesCount), Valid: true}, + LastUpdatedAt: pgtype.Timestamptz{Time: p.LastUpdatedAt, Valid: !p.LastUpdatedAt.IsZero()}, + }, nil +} + +func ConvertDBEnetpulsePreodds(dbP dbgen.EnetpulsePreodd) domain.EnetpulsePreodds { + return domain.EnetpulsePreodds{ + PreoddsID: dbP.PreoddsID, + EventFK: dbP.EventFk, + OutcomeTypeFK: dbP.OutcomeTypeFk.Int32, + OutcomeScopeFK: dbP.OutcomeScopeFk.Int32, + OutcomeSubtypeFK: dbP.OutcomeSubtypeFk.Int32, + EventParticipantNumber: dbP.EventParticipantNumber.Int32, + IParam: dbP.Iparam.String, + IParam2: dbP.Iparam2.String, + DParam: dbP.Dparam.String, + DParam2: dbP.Dparam2.String, + SParam: dbP.Sparam.String, + UpdatesCount: dbP.UpdatesCount.Int32, + LastUpdatedAt: dbP.LastUpdatedAt.Time, + CreatedAt: dbP.CreatedAt.Time, + UpdatedAt: dbP.UpdatedAt.Time, + } +} + +func ConvertCreateEnetpulsePreoddsBettingOffer(o domain.CreateEnetpulsePreoddsBettingOffer) dbgen.CreateEnetpulsePreoddsBettingOfferParams { + // Convert float64 to int64 with scale 2 + oddsInt := big.NewInt(int64(math.Round(o.Odds * 100))) + oddsOldInt := big.NewInt(int64(math.Round(o.OddsOld * 100))) + + return dbgen.CreateEnetpulsePreoddsBettingOfferParams{ + BettingofferID: o.BettingOfferID, + PreoddsFk: o.PreoddsFK, + BettingofferStatusFk: pgtype.Int4{Int32: o.BettingOfferStatusFK, Valid: true}, + OddsProviderFk: pgtype.Int4{Int32: o.OddsProviderFK, Valid: true}, + Odds: pgtype.Numeric{ + Int: oddsInt, + Exp: -2, // scale 2 decimal places + Valid: true, + }, + OddsOld: pgtype.Numeric{ + Int: oddsOldInt, + Exp: -2, + Valid: true, + }, + Active: pgtype.Bool{Bool: o.Active == "yes", Valid: true}, + CouponKey: pgtype.Text{ + String: o.CouponKey, + Valid: o.CouponKey != "", + }, + UpdatesCount: pgtype.Int4{Int32: int32(o.UpdatesCount), Valid: true}, + LastUpdatedAt: pgtype.Timestamptz{Time: o.LastUpdatedAt, Valid: !o.LastUpdatedAt.IsZero()}, + } +} + +// Convert DB result to domain struct +func ConvertDBEnetpulsePreoddsBettingOffer(o dbgen.EnetpulsePreoddsBettingoffer) domain.EnetpulsePreoddsBettingOffer { + var odds, oddsOld float64 + if o.Odds.Valid { + odds, _ = o.Odds.Int.Float64() // Convert pgtype.Numeric to float64 + } + if o.OddsOld.Valid { + oddsOld, _ = o.OddsOld.Int.Float64() + } + + active := "no" + if o.Active.Valid && o.Active.Bool { + active = "yes" + } + + return domain.EnetpulsePreoddsBettingOffer{ + ID: o.ID, + BettingOfferID: o.BettingofferID, + PreoddsFK: o.PreoddsFk, + BettingOfferStatusFK: o.BettingofferStatusFk.Int32, + OddsProviderFK: o.OddsProviderFk.Int32, + Odds: odds, + OddsOld: oddsOld, + Active: active, + CouponKey: o.CouponKey.String, + UpdatesCount: int(o.UpdatesCount.Int32), + LastUpdatedAt: o.LastUpdatedAt.Time, + CreatedAt: o.CreatedAt.Time, + UpdatedAt: o.UpdatedAt.Time, + } +} + +func (s *Store) CreateEnetpulseResultParticipant( + ctx context.Context, + participant domain.CreateEnetpulseResultParticipant, +) (domain.EnetpulseResultParticipant, error) { + dbParticipant, err := s.queries.CreateEnetpulseResultParticipant( + ctx, + ConvertCreateEnetpulseResultParticipant(participant), + ) + if err != nil { + return domain.EnetpulseResultParticipant{}, err + } + + return ConvertDBEnetpulseResultParticipant(dbParticipant), nil +} + +func (s *Store) GetEnetpulseResultParticipantsByResultFK( + ctx context.Context, + resultFK string, +) ([]domain.EnetpulseResultParticipant, error) { + dbParticipants, err := s.queries.GetEnetpulseResultParticipantsByResultFK(ctx, resultFK) + if err != nil { + return nil, err + } + + participants := make([]domain.EnetpulseResultParticipant, 0, len(dbParticipants)) + for _, dbp := range dbParticipants { + participants = append(participants, ConvertDBEnetpulseResultParticipant(dbp)) + } + + return participants, nil +} + +func (s *Store) CreateEnetpulseResultReferee( + ctx context.Context, + referee domain.CreateEnetpulseResultReferee, +) (domain.EnetpulseResultReferee, error) { + dbReferee, err := s.queries.CreateEnetpulseResultReferee( + ctx, + ConvertCreateEnetpulseResultReferee(referee), + ) + if err != nil { + return domain.EnetpulseResultReferee{}, err + } + + return ConvertDBEnetpulseResultReferee(dbReferee), nil +} + +func (s *Store) GetEnetpulseResultRefereesByResultFK( + ctx context.Context, + resultFK string, +) ([]domain.EnetpulseResultReferee, error) { + dbReferees, err := s.queries.GetEnetpulseResultRefereesByResultFK(ctx, resultFK) + if err != nil { + return nil, err + } + + referees := make([]domain.EnetpulseResultReferee, 0, len(dbReferees)) + for _, dbr := range dbReferees { + referees = append(referees, ConvertDBEnetpulseResultReferee(dbr)) + } + + return referees, nil +} + +func ConvertCreateEnetpulseResultParticipant(p domain.CreateEnetpulseResultParticipant) dbgen.CreateEnetpulseResultParticipantParams { + return dbgen.CreateEnetpulseResultParticipantParams{ + ParticipantMapID: p.ParticipantMapID, + ResultFk: p.ResultFk, + ParticipantFk: p.ParticipantFk, + Number: pgtype.Int4{Int32: p.Number}, + Name: pgtype.Text{String: p.Name}, + Gender: pgtype.Text{String: p.Gender}, + Type: pgtype.Text{String: p.Type}, + CountryFk: pgtype.Text{String: p.CountryFk}, + CountryName: pgtype.Text{String: p.CountryName}, + OrdinaryTime: pgtype.Text{String: p.OrdinaryTime}, + RunningScore: pgtype.Text{String: p.RunningScore}, + Halftime: pgtype.Text{String: p.Halftime}, + FinalResult: pgtype.Text{String: p.FinalResult}, + LastUpdatedAt: pgtype.Timestamptz{Time: p.LastUpdatedAt, Valid: !p.LastUpdatedAt.IsZero()}, + } +} + +func ConvertDBEnetpulseResultParticipant(p dbgen.EnetpulseResultParticipant) domain.EnetpulseResultParticipant { + return domain.EnetpulseResultParticipant{ + ID: p.ID, + ParticipantMapID: p.ParticipantMapID, + ResultFk: p.ResultFk, + ParticipantFk: p.ParticipantFk, + Number: p.Number.Int32, + Name: p.Name.String, + Gender: p.Gender.String, + Type: p.Type.String, + CountryFk: p.CountryFk.String, + CountryName: p.CountryName.String, + OrdinaryTime: p.OrdinaryTime.String, + RunningScore: p.RunningScore.String, + Halftime: p.Halftime.String, + FinalResult: p.FinalResult.String, + LastUpdatedAt: p.LastUpdatedAt.Time, + CreatedAt: p.CreatedAt.Time, + } +} + +func ConvertCreateEnetpulseResultReferee(r domain.CreateEnetpulseResultReferee) dbgen.CreateEnetpulseResultRefereeParams { + return dbgen.CreateEnetpulseResultRefereeParams{ + ResultFk: r.ResultFk, + RefereeFk: pgtype.Text{String: r.RefereeFk}, + Assistant1RefereeFk: pgtype.Text{String: r.Assistant1RefereeFk}, + Assistant2RefereeFk: pgtype.Text{String: r.Assistant2RefereeFk}, + FourthRefereeFk: pgtype.Text{String: r.FourthRefereeFk}, + Var1RefereeFk: pgtype.Text{String: r.Var1RefereeFk}, + Var2RefereeFk: pgtype.Text{String: r.Var2RefereeFk}, + LastUpdatedAt: pgtype.Timestamptz{Time: r.LastUpdatedAt}, + } +} + +func ConvertDBEnetpulseResultReferee(r dbgen.EnetpulseResultReferee) domain.EnetpulseResultReferee { + return domain.EnetpulseResultReferee{ + ID: r.ID, + ResultFk: r.ResultFk, + RefereeFk: r.RefereeFk.String, + Assistant1RefereeFk: r.Assistant1RefereeFk.String, + Assistant2RefereeFk: r.Assistant2RefereeFk.String, + FourthRefereeFk: r.FourthRefereeFk.String, + Var1RefereeFk: r.Var1RefereeFk.String, + Var2RefereeFk: r.Var2RefereeFk.String, + LastUpdatedAt: r.LastUpdatedAt.Time, + CreatedAt: r.CreatedAt.Time, + } +} diff --git a/internal/repository/virtual_game.go b/internal/repository/virtual_game.go index c792801..e7befef 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -4,6 +4,8 @@ import ( "context" "database/sql" "errors" + "fmt" + "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -19,8 +21,9 @@ type VirtualGameRepository interface { ListVirtualGameProviders(ctx context.Context, limit, offset int32) ([]dbgen.VirtualGameProvider, error) UpdateVirtualGameProviderEnabled(ctx context.Context, providerID string, enabled bool) (dbgen.VirtualGameProvider, error) CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error + GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (*domain.VirtualGameSession, error) GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error) - UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error + // UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error @@ -36,6 +39,12 @@ type VirtualGameRepository interface { CreateVirtualGame(ctx context.Context, arg dbgen.CreateVirtualGameParams) (dbgen.VirtualGame, error) ListAllVirtualGames(ctx context.Context, arg dbgen.GetAllVirtualGamesParams) ([]dbgen.GetAllVirtualGamesRow, error) RemoveAllVirtualGames(ctx context.Context) error + CreateVirtualGameProviderReport(ctx context.Context, report domain.CreateVirtualGameProviderReport) (domain.VirtualGameProviderReport, error) + CreateVirtualGameReport(ctx context.Context, report domain.CreateVirtualGameReport) (domain.VirtualGameReport, error) + GetVirtualGameProviderReportByProviderAndDate(ctx context.Context, providerID string, createdAt time.Time, reportType string) (domain.VirtualGameProviderReport, error) + UpdateVirtualGameProviderReportByDate(ctx context.Context, providerID string, reportDate time.Time, reportType string, totalGamesPlayed int64, totalBets float64, totalPayouts float64, totalPlayers int64) error + ListVirtualGameProviderReportsByGamesPlayedAsc(ctx context.Context) ([]domain.VirtualGameProviderReport, error) + ListVirtualGameProviderReportsByGamesPlayedDesc(ctx context.Context) ([]domain.VirtualGameProviderReport, error) } type VirtualGameRepo struct { @@ -158,14 +167,33 @@ func (r *VirtualGameRepo) CreateVirtualGameSession(ctx context.Context, session UserID: session.UserID, GameID: session.GameID, SessionToken: session.SessionToken, - Currency: session.Currency, - Status: session.Status, - ExpiresAt: pgtype.Timestamptz{Time: session.ExpiresAt, Valid: true}, + // Currency: session.Currency, + // Status: session.Status, + // ExpiresAt: pgtype.Timestamptz{Time: session.ExpiresAt, Valid: true}, } _, err := r.store.queries.CreateVirtualGameSession(ctx, params) return err } +func (r *VirtualGameRepo) GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (*domain.VirtualGameSession, error) { + dbSession, err := r.store.queries.GetVirtualGameSessionByUserID(ctx, userID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + return &domain.VirtualGameSession{ + ID: dbSession.ID, + UserID: dbSession.UserID, + GameID: dbSession.GameID, + SessionToken: dbSession.SessionToken, + CreatedAt: dbSession.CreatedAt.Time, + UpdatedAt: dbSession.UpdatedAt.Time, + }, nil +} + func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error) { dbSession, err := r.store.queries.GetVirtualGameSessionByToken(ctx, token) if err != nil { @@ -179,20 +207,20 @@ func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, toke UserID: dbSession.UserID, GameID: dbSession.GameID, SessionToken: dbSession.SessionToken, - Currency: dbSession.Currency, - Status: dbSession.Status, - CreatedAt: dbSession.CreatedAt.Time, - UpdatedAt: dbSession.UpdatedAt.Time, - ExpiresAt: dbSession.ExpiresAt.Time, + // Currency: dbSession.Currency, + // Status: dbSession.Status, + CreatedAt: dbSession.CreatedAt.Time, + UpdatedAt: dbSession.UpdatedAt.Time, + // ExpiresAt: dbSession.ExpiresAt.Time, }, nil } -func (r *VirtualGameRepo) UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error { - return r.store.queries.UpdateVirtualGameSessionStatus(ctx, dbgen.UpdateVirtualGameSessionStatusParams{ - ID: id, - Status: status, - }) -} +// func (r *VirtualGameRepo) UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error { +// return r.store.queries.UpdateVirtualGameSessionStatus(ctx, dbgen.UpdateVirtualGameSessionStatusParams{ +// ID: id, +// Status: status, +// }) +// } func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error { params := dbgen.CreateVirtualGameTransactionParams{ @@ -314,3 +342,183 @@ func (r *VirtualGameRepo) ListAllVirtualGames(ctx context.Context, arg dbgen.Get func (r *VirtualGameRepo) RemoveAllVirtualGames(ctx context.Context) error { return r.store.queries.DeleteAllVirtualGames(ctx) } + +func (r *VirtualGameRepo) CreateVirtualGameProviderReport( + ctx context.Context, + report domain.CreateVirtualGameProviderReport, +) (domain.VirtualGameProviderReport, error) { + dbReport, err := r.store.queries.CreateVirtualGameProviderReport( + ctx, + ConvertCreateVirtualGameProviderReport(report), + ) + if err != nil { + return domain.VirtualGameProviderReport{}, err + } + + return ConvertDBVirtualGameProviderReport(dbReport), nil +} + +func (r *VirtualGameRepo) CreateVirtualGameReport( + ctx context.Context, + report domain.CreateVirtualGameReport, +) (domain.VirtualGameReport, error) { + dbReport, err := r.store.queries.CreateVirtualGameReport( + ctx, + ConvertCreateVirtualGameReport(report), + ) + if err != nil { + return domain.VirtualGameReport{}, err + } + + return ConvertDBVirtualGameReport(dbReport), nil +} + +func (r *VirtualGameRepo) GetVirtualGameProviderReportByProviderAndDate( + ctx context.Context, + providerID string, + reportDate time.Time, + reportType string, +) (domain.VirtualGameProviderReport, error) { + arg := dbgen.GetVirtualGameProviderReportByProviderAndDateParams{ + ProviderID: providerID, + ReportDate: pgtype.Date{Time: reportDate, Valid: true}, + ReportType: pgtype.Text{String: reportType, Valid: true}, + } + + dbReport, err := r.store.queries.GetVirtualGameProviderReportByProviderAndDate(ctx, arg) + if err != nil { + return domain.VirtualGameProviderReport{}, err + } + + return ConvertDBVirtualGameProviderReport(dbReport), nil +} + +func (r *VirtualGameRepo) UpdateVirtualGameProviderReportByDate( + ctx context.Context, + providerID string, + reportDate time.Time, + reportType string, + totalGamesPlayed int64, + totalBets float64, + totalPayouts float64, + totalPlayers int64, +) error { + arg := dbgen.UpdateVirtualGameProviderReportByDateParams{ + ProviderID: providerID, + ReportDate: pgtype.Date{Time: reportDate, Valid: true}, + ReportType: pgtype.Text{String: reportType, Valid: true}, + TotalGamesPlayed: pgtype.Int8{Int64: totalGamesPlayed, Valid: true}, + TotalBets: pgtype.Numeric{}, + TotalPayouts: pgtype.Numeric{}, + TotalPlayers: pgtype.Int8{Int64: totalPlayers, Valid: true}, + } + + // Safely convert float64 → pgtype.Numeric + if err := arg.TotalBets.Scan(totalBets); err != nil { + return fmt.Errorf("failed to set total_bets: %w", err) + } + if err := arg.TotalPayouts.Scan(totalPayouts); err != nil { + return fmt.Errorf("failed to set total_payouts: %w", err) + } + + if err := r.store.queries.UpdateVirtualGameProviderReportByDate(ctx, arg); err != nil { + return fmt.Errorf("failed to update provider report for %s: %w", providerID, err) + } + + return nil +} + +func (r *VirtualGameRepo) ListVirtualGameProviderReportsByGamesPlayedAsc( + ctx context.Context, +) ([]domain.VirtualGameProviderReport, error) { + dbReports, err := r.store.queries.ListVirtualGameProviderReportsByGamesPlayedAsc(ctx) + if err != nil { + return nil, err + } + + reports := make([]domain.VirtualGameProviderReport, len(dbReports)) + for i, r := range dbReports { + reports[i] = ConvertDBVirtualGameProviderReport(r) + } + + return reports, nil +} + +func (r *VirtualGameRepo) ListVirtualGameProviderReportsByGamesPlayedDesc( + ctx context.Context, +) ([]domain.VirtualGameProviderReport, error) { + dbReports, err := r.store.queries.ListVirtualGameProviderReportsByGamesPlayedDesc(ctx) + if err != nil { + return nil, err + } + + reports := make([]domain.VirtualGameProviderReport, len(dbReports)) + for i, r := range dbReports { + reports[i] = ConvertDBVirtualGameProviderReport(r) + } + + return reports, nil +} + +func ConvertCreateVirtualGameProviderReport(r domain.CreateVirtualGameProviderReport) dbgen.CreateVirtualGameProviderReportParams { + // var totalBets, totalPayouts pgtype.Numeric + + // _ = r.TotalBets. + // _ = totalPayouts.Set(r.TotalPayouts) + + return dbgen.CreateVirtualGameProviderReportParams{ + ProviderID: r.ProviderID, + ReportDate: pgtype.Date{Time: r.ReportDate, Valid: true}, + TotalGamesPlayed: pgtype.Int8{Int64: r.TotalGamesPlayed, Valid: true}, + TotalBets: pgtype.Numeric{Exp: int32(r.TotalBets)}, + TotalPayouts: pgtype.Numeric{Exp: int32(r.TotalPayouts)}, + TotalPlayers: pgtype.Int8{Int64: r.TotalPlayers, Valid: true}, + Column7: pgtype.Text{String: r.ReportType, Valid: true}, + } +} + +func ConvertDBVirtualGameProviderReport(db dbgen.VirtualGameProviderReport) domain.VirtualGameProviderReport { + return domain.VirtualGameProviderReport{ + ID: db.ID, + ProviderID: db.ProviderID, + ReportDate: db.ReportDate.Time, + TotalGamesPlayed: db.TotalGamesPlayed.Int64, + TotalBets: float64(db.TotalBets.Exp), + TotalPayouts: float64(db.TotalPayouts.Exp), + TotalProfit: float64(db.TotalProfit.Exp), + TotalPlayers: db.TotalPlayers.Int64, + ReportType: db.ReportType.String, + CreatedAt: db.CreatedAt.Time, + UpdatedAt: db.UpdatedAt.Time, + } +} + +func ConvertCreateVirtualGameReport(r domain.CreateVirtualGameReport) dbgen.CreateVirtualGameReportParams { + return dbgen.CreateVirtualGameReportParams{ + GameID: r.GameID, + ProviderID: r.ProviderID, + ReportDate: pgtype.Date{Time: r.ReportDate}, + TotalRounds: pgtype.Int8{Int64: r.TotalRounds}, + TotalBets: pgtype.Numeric{Exp: int32(r.TotalBets)}, + TotalPayouts: pgtype.Numeric{Exp: int32(r.TotalPayouts)}, + TotalPlayers: pgtype.Int8{Int64: r.TotalPlayers}, + Column8: r.ReportType, + } +} + +func ConvertDBVirtualGameReport(db dbgen.VirtualGameReport) domain.VirtualGameReport { + return domain.VirtualGameReport{ + ID: db.ID, + GameID: db.GameID, + ProviderID: db.ProviderID, + ReportDate: db.ReportDate.Time, + TotalRounds: db.TotalRounds.Int64, + TotalBets: float64(db.TotalBets.Exp), + TotalPayouts: float64(db.TotalPayouts.Exp), + TotalProfit: float64(db.TotalProfit.Exp), + TotalPlayers: db.TotalPlayers.Int64, + ReportType: db.ReportType.String, + CreatedAt: db.CreatedAt.Time, + UpdatedAt: db.UpdatedAt.Time, + } +} diff --git a/internal/services/arifpay/service.go b/internal/services/arifpay/service.go index 80e5c2e..dc42eea 100644 --- a/internal/services/arifpay/service.go +++ b/internal/services/arifpay/service.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -32,15 +33,15 @@ func NewArifpayService(cfg *config.Config, transferStore ports.TransferStore, wa } } -func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool) (map[string]any, error) { +func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool, userId int64) (map[string]any, error) { // Generate unique nonce nonce := uuid.NewString() var NotifyURL string - if isDeposit{ + if isDeposit { NotifyURL = s.cfg.ARIFPAY.C2BNotifyUrl - }else{ + } else { NotifyURL = s.cfg.ARIFPAY.B2CNotifyUrl } @@ -130,6 +131,10 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR ReferenceNumber: nonce, SessionID: fmt.Sprintf("%v", data["sessionId"]), Status: string(domain.PaymentStatusPending), + CashierID: domain.ValidInt64{ + Value: userId, + Valid: true, + }, } if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil { @@ -139,7 +144,7 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR return data, nil } -func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID string) (*domain.CancelCheckoutSessionResponse, error) { +func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID string) (any, error) { // Build the cancel URL url := fmt.Sprintf("%s/api/sandbox/checkout/session/%s", s.cfg.ARIFPAY.BaseURL, sessionID) @@ -177,17 +182,19 @@ func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID st return nil, fmt.Errorf("failed to unmarshal cancel response: %w", err) } - return &cancelResp, nil + return cancelResp.Data, nil } -func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRequest, userId int64, isDepost bool) error { +func (s *ArifpayService) ProcessWebhook(ctx context.Context, req domain.WebhookRequest, isDeposit bool) error { // 1. Get transfer by SessionID - transfer, err := s.transferStore.GetTransferByReference(ctx, req.Transaction.TransactionID) + transfer, err := s.transferStore.GetTransferByReference(ctx, req.Nonce) if err != nil { return err } - wallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) + userId := transfer.DepositorID.Value + + wallet, err := s.walletSvc.GetCustomerWallet(ctx, userId) if err != nil { return err } @@ -197,7 +204,7 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe } // 2. Update transfer status - newStatus := req.Transaction.TransactionStatus + newStatus := strings.ToLower(req.Transaction.TransactionStatus) // if req.Transaction.TransactionStatus != "" { // newStatus = req.Transaction.TransactionStatus // } @@ -213,10 +220,10 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe } // 3. If SUCCESS -> update customer wallet balance - if (newStatus == "SUCCESS" && isDepost) || (newStatus == "FAILED" && !isDepost) { - _, err = s.walletSvc.AddToWallet(ctx, wallets[0].ID, domain.Currency(req.TotalAmount), domain.ValidInt64{}, transfer.PaymentMethod, domain.PaymentDetails{ + if (newStatus == "success" && isDeposit) || (newStatus == "failed" && !isDeposit) { + _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.TotalAmount), domain.ValidInt64{}, transfer.PaymentMethod, domain.PaymentDetails{ ReferenceNumber: domain.ValidString{ - Value: req.Transaction.TransactionID, + Value: req.Nonce, Valid: true, }, BankNumber: domain.ValidString{ @@ -232,35 +239,94 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe return nil } -func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { +func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error { // Step 1: Create Session + + userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId) + if err != nil { + return fmt.Errorf("failed to get user wallets: %w", err) + } + // if len(userWallets) == 0 { + // return fmt.Errorf("no wallet found for user %d", userId) + // } + + _, err = s.walletSvc.DeductFromWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } + referenceNum := uuid.NewString() sessionReq := domain.CheckoutSessionClientRequest{ Amount: req.Amount, CustomerEmail: req.CustomerEmail, - CustomerPhone: req.CustomerPhone, + CustomerPhone: "251" + req.CustomerPhone[:9], } - sessionResp, err := s.CreateCheckoutSession(sessionReq, false) + sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId) if err != nil { + _, err = s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } return fmt.Errorf("failed to create session: %w", err) } + sessionRespData := sessionResp["data"].(map[string]any) + // Step 2: Execute Transfer transferURL := fmt.Sprintf("%s/api/Telebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL) reqBody := map[string]any{ - "Sessionid": sessionResp["sessionId"], - "Phonenumber": req.PhoneNumber, + "Sessionid": sessionRespData["sessionId"], + "Phonenumber": "251" + req.CustomerPhone[:9], } payload, err := json.Marshal(reqBody) if err != nil { + _, err = s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } return fmt.Errorf("failed to marshal transfer request: %w", err) } transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) if err != nil { + _, err = s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } return fmt.Errorf("failed to build transfer request: %w", err) } transferReq.Header.Set("Content-Type", "application/json") @@ -268,11 +334,35 @@ func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req dom transferResp, err := s.httpClient.Do(transferReq) if err != nil { + _, err = s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } return fmt.Errorf("failed to execute transfer request: %w", err) } defer transferResp.Body.Close() - if transferResp.StatusCode >= 300 { + if transferResp.StatusCode != http.StatusOK { + _, err = s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if err != nil { + return fmt.Errorf("failed to deduct from wallet: %w", err) + } body, _ := io.ReadAll(transferResp.Body) return fmt.Errorf("transfer failed with status %d: %s", transferResp.StatusCode, string(body)) } @@ -283,109 +373,33 @@ func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req dom Verified: false, Type: domain.WITHDRAW, // B2C = payout ReferenceNumber: referenceNum, - SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]), + SessionID: fmt.Sprintf("%v", sessionRespData["sessionId"]), Status: string(domain.PaymentStatusPending), PaymentMethod: domain.TRANSFER_ARIFPAY, + CashierID: domain.ValidInt64{ + Value: userId, + Valid: true, + }, } if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { return fmt.Errorf("failed to store transfer: %w", err) } // Step 4: Deduct from wallet - userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) - if err != nil { - return fmt.Errorf("failed to get user wallets: %w", err) - } - if len(userWallets) == 0 { - return fmt.Errorf("no wallet found for user %d", userId) - } - - _, err = s.walletSvc.DeductFromWallet( - ctx, - userWallets[0].ID, - domain.Currency(req.Amount), - domain.ValidInt64{}, - domain.TRANSFER_ARIFPAY, - "", - ) - if err != nil { - return fmt.Errorf("failed to deduct from wallet: %w", err) - } return nil } -func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { - // Step 1: Create Session - referenceNum := uuid.NewString() - - sessionReq := domain.CheckoutSessionClientRequest{ - Amount: req.Amount, - CustomerEmail: req.CustomerEmail, - CustomerPhone: req.CustomerPhone, - } - - sessionResp, err := s.CreateCheckoutSession(sessionReq, false) +func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error { + // Step 1: Deduct from user wallet first + userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId) if err != nil { - return fmt.Errorf("cbebirr: failed to create session: %w", err) - } - - // Step 2: Execute Transfer - transferURL := fmt.Sprintf("%s/api/Cbebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL) - reqBody := map[string]any{ - "Sessionid": sessionResp["sessionId"], - "Phonenumber": req.PhoneNumber, - } - - payload, err := json.Marshal(reqBody) - if err != nil { - return fmt.Errorf("cbebirr: failed to marshal transfer request: %w", err) - } - - transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) - if err != nil { - return fmt.Errorf("cbebirr: failed to build transfer request: %w", err) - } - transferReq.Header.Set("Content-Type", "application/json") - transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) - - transferResp, err := s.httpClient.Do(transferReq) - if err != nil { - return fmt.Errorf("cbebirr: failed to execute transfer request: %w", err) - } - defer transferResp.Body.Close() - - if transferResp.StatusCode >= 300 { - body, _ := io.ReadAll(transferResp.Body) - return fmt.Errorf("cbebirr: transfer failed with status %d: %s", transferResp.StatusCode, string(body)) - } - - // Step 3: Store transfer in DB - transfer := domain.CreateTransfer{ - Amount: domain.Currency(req.Amount), - Verified: false, - Type: domain.WITHDRAW, // B2C = payout - ReferenceNumber: referenceNum, - SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]), - Status: string(domain.PaymentStatusPending), - PaymentMethod: domain.TRANSFER_ARIFPAY, - } - if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { - return fmt.Errorf("cbebirr: failed to store transfer: %w", err) - } - - // Step 4: Deduct from user wallet - userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) - if err != nil { - return fmt.Errorf("cbebirr: failed to get user wallets: %w", err) - } - if len(userWallets) == 0 { - return fmt.Errorf("cbebirr: no wallet found for user %d", userId) + return fmt.Errorf("cbebirr: failed to get user wallet: %w", err) } _, err = s.walletSvc.DeductFromWallet( ctx, - userWallets[0].ID, + userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, @@ -395,55 +409,68 @@ func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.A return fmt.Errorf("cbebirr: failed to deduct from wallet: %w", err) } - return nil -} - -func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { - // Step 1: Create Session referenceNum := uuid.NewString() + // Step 2: Create Session sessionReq := domain.CheckoutSessionClientRequest{ Amount: req.Amount, CustomerEmail: req.CustomerEmail, - CustomerPhone: req.CustomerPhone, + CustomerPhone: "251" + req.CustomerPhone[:9], } - sessionResp, err := s.CreateCheckoutSession(sessionReq, false) + sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId) if err != nil { - return fmt.Errorf("Mpesa: failed to create session: %w", err) + // refund wallet if session creation fails + _, refundErr := s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if refundErr != nil { + return fmt.Errorf("cbebirr: refund failed after session creation error: %v", refundErr) + } + return fmt.Errorf("cbebirr: failed to create session: %w", err) } - // Step 2: Execute Transfer - transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL) + // Step 3: Execute Transfer + transferURL := fmt.Sprintf("%s/api/Cbebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL) reqBody := map[string]any{ "Sessionid": sessionResp["sessionId"], - "Phonenumber": req.PhoneNumber, + "Phonenumber": "251" + req.CustomerPhone[:9], } payload, err := json.Marshal(reqBody) if err != nil { - return fmt.Errorf("Mpesa: failed to marshal transfer request: %w", err) + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("cbebirr: failed to marshal transfer request: %w", err) } transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) if err != nil { - return fmt.Errorf("Mpesa: failed to build transfer request: %w", err) + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("cbebirr: failed to build transfer request: %w", err) } transferReq.Header.Set("Content-Type", "application/json") transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) transferResp, err := s.httpClient.Do(transferReq) if err != nil { - return fmt.Errorf("Mpesa: failed to execute transfer request: %w", err) + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("cbebirr: failed to execute transfer request: %w", err) } defer transferResp.Body.Close() - if transferResp.StatusCode >= 300 { + if transferResp.StatusCode != http.StatusOK { body, _ := io.ReadAll(transferResp.Body) - return fmt.Errorf("Mpesa: transfer failed with status %d: %s", transferResp.StatusCode, string(body)) + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("cbebirr: transfer failed with status %d: %s", transferResp.StatusCode, string(body)) } - // Step 3: Store transfer in DB + // Step 4: Store transfer in DB transfer := domain.CreateTransfer{ Amount: domain.Currency(req.Amount), Verified: false, @@ -452,30 +479,116 @@ func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]), Status: string(domain.PaymentStatusPending), PaymentMethod: domain.TRANSFER_ARIFPAY, - } - if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { - return fmt.Errorf("Mpesa: failed to store transfer: %w", err) + CashierID: domain.ValidInt64{ + Value: userId, + Valid: true, + }, } - // Step 4: Deduct from user wallet - userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) - if err != nil { - return fmt.Errorf("Mpesa: failed to get user wallets: %w", err) + if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { + return fmt.Errorf("cbebirr: failed to store transfer: %w", err) } - if len(userWallets) == 0 { - return fmt.Errorf("Mpesa: no wallet found for user %d", userId) + + return nil +} + +func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error { + // Step 1: Deduct from user wallet first + userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId) + if err != nil { + return fmt.Errorf("mpesa: failed to get user wallet: %w", err) } _, err = s.walletSvc.DeductFromWallet( ctx, - userWallets[0].ID, + userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, "", ) if err != nil { - return fmt.Errorf("Mpesa: failed to deduct from wallet: %w", err) + return fmt.Errorf("mpesa: failed to deduct from wallet: %w", err) + } + + referenceNum := uuid.NewString() + + // Step 2: Create Session + sessionReq := domain.CheckoutSessionClientRequest{ + Amount: req.Amount, + CustomerEmail: req.CustomerEmail, + CustomerPhone: "251" + req.CustomerPhone[:9], + } + + sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId) + if err != nil { + // Refund wallet if session creation fails + _, refundErr := s.walletSvc.AddToWallet( + ctx, + userWallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_ARIFPAY, + domain.PaymentDetails{}, + "", + ) + if refundErr != nil { + return fmt.Errorf("mpesa: refund failed after session creation error: %v", refundErr) + } + return fmt.Errorf("mpesa: failed to create session: %w", err) + } + + // Step 3: Execute Transfer + transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL) + reqBody := map[string]any{ + "Sessionid": sessionResp["sessionId"], + "Phonenumber": "251" + req.CustomerPhone[:9], + } + + payload, err := json.Marshal(reqBody) + if err != nil { + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("mpesa: failed to marshal transfer request: %w", err) + } + + transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) + if err != nil { + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("mpesa: failed to build transfer request: %w", err) + } + transferReq.Header.Set("Content-Type", "application/json") + transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) + + transferResp, err := s.httpClient.Do(transferReq) + if err != nil { + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("mpesa: failed to execute transfer request: %w", err) + } + defer transferResp.Body.Close() + + if transferResp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(transferResp.Body) + s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "") + return fmt.Errorf("mpesa: transfer failed with status %d: %s", transferResp.StatusCode, string(body)) + } + + // Step 4: Store transfer in DB + transfer := domain.CreateTransfer{ + Amount: domain.Currency(req.Amount), + Verified: false, + Type: domain.WITHDRAW, // B2C = payout + ReferenceNumber: referenceNum, + SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]), + Status: string(domain.PaymentStatusPending), + PaymentMethod: domain.TRANSFER_ARIFPAY, + CashierID: domain.ValidInt64{ + Value: userId, + Valid: true, + }, + } + + if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { + return fmt.Errorf("mpesa: failed to store transfer: %w", err) } return nil diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 9be7254..3e14fdc 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -933,6 +933,7 @@ func (s *Service) GetBetOutcomeByBetID(ctx context.Context, UserID int64) ([]dom func (s *Service) GetBetOutcomeViewByEventID(ctx context.Context, eventID int64, filter domain.BetOutcomeViewFilter) ([]domain.BetOutcomeViewRes, int64, error) { return s.betStore.GetBetOutcomeViewByEventID(ctx, eventID, filter) } + func (s *Service) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) { return s.betStore.GetBetOutcomeByEventID(ctx, eventID, is_filtered) } diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go index 3beed5b..764c273 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -29,16 +29,17 @@ func NewClient(baseURL, secretKey string) *Client { } } -func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) { +func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error) { payload := map[string]interface{}{ - "amount": fmt.Sprintf("%.2f", float64(req.Amount)), - "currency": req.Currency, - // "email": req.Email, + "amount": fmt.Sprintf("%.2f", float64(req.Amount)), + "currency": req.Currency, + "email": req.Email, "first_name": req.FirstName, "last_name": req.LastName, "tx_ref": req.TxRef, - "callback_url": req.CallbackURL, + // "callback_url": req.CallbackURL, "return_url": req.ReturnURL, + "phone_number": req.PhoneNumber, } fmt.Printf("\n\nChapa Payload: %+v\n\n", payload) @@ -69,9 +70,9 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) // <-- Log it } - if resp.StatusCode != http.StatusOK { - return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } + // if resp.StatusCode != http.StatusOK { + // return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + // } var response struct { Message string `json:"message"` @@ -130,15 +131,16 @@ func (c *Client) VerifyPayment(ctx context.Context, reference string) (domain.Ch }, nil } -func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { +func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaPaymentVerificationResponse, error) { url := fmt.Sprintf("%s/transaction/verify/%s", c.baseURL, txRef) - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.secretKey) + req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { @@ -147,35 +149,27 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) } - var response struct { - Status string `json:"status"` - Amount float64 `json:"amount"` - Currency string `json:"currency"` - } - - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + var verification domain.ChapaPaymentVerificationResponse + if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } - var status domain.PaymentStatus - switch response.Status { - case "success": - status = domain.PaymentStatusCompleted - default: - status = domain.PaymentStatusFailed - } + // Normalize payment status for internal use + // switch strings.ToLower(verification.Data.Status) { + // case "success": + // verification.Status = string(domain.PaymentStatusCompleted) + // default: + // verification.Status = string(domain.PaymentStatusFailed) + // } - return &domain.ChapaVerificationResponse{ - Status: string(status), - Amount: response.Amount, - Currency: response.Currency, - }, nil + return &verification, nil } -func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { +func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaTransferVerificationResponse, error) { url := fmt.Sprintf("%s/transfers/verify/%s", c.baseURL, txRef) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) @@ -213,14 +207,77 @@ func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domai status = domain.PaymentStatusFailed } - return &domain.ChapaVerificationResponse{ - Status: string(status), - Amount: response.Amount, - Currency: response.Currency, + return &domain.ChapaTransferVerificationResponse{ + Status: string(status), + // Amount: response.Amount, + // Currency: response.Currency, }, nil } -func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) { +func (c *Client) GetAllTransactions(ctx context.Context) (domain.ChapaAllTransactionsResponse, error) { + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/transactions", nil) + if err != nil { + return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("failed to create request: %w", err) + } + + httpReq.Header.Set("Authorization", "Bearer "+c.secretKey) + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) + } + + var response domain.ChapaAllTransactionsResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("failed to decode response: %w", err) + } + + return response, nil +} + +func (c *Client) GetTransactionEvents(ctx context.Context, refId string) ([]domain.ChapaTransactionEvent, error) { + url := fmt.Sprintf("%s/transaction/events/%s", c.baseURL, refId) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+c.secretKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) + } + + var response struct { + Message string `json:"message"` + Status string `json:"status"` + Data []domain.ChapaTransactionEvent `json:"data"` + } + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return response.Data, nil +} + +func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.BankData, error) { req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -243,9 +300,9 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) return nil, fmt.Errorf("failed to decode response: %w", err) } - var banks []domain.Bank + var banks []domain.BankData for _, bankData := range bankResponse.Data { - bank := domain.Bank{ + bank := domain.BankData{ ID: bankData.ID, Slug: bankData.Slug, Swift: bankData.Swift, @@ -267,7 +324,7 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) return banks, nil } -func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) { +func (c *Client) InitializeTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) { endpoint := c.baseURL + "/transfers" fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint) @@ -304,7 +361,7 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa return response.Status == string(domain.WithdrawalStatusSuccessful), nil } -func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) { +func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaTransferVerificationResponse, error) { base, err := url.Parse(c.baseURL) if err != nil { return nil, fmt.Errorf("invalid base URL: %w", err) @@ -328,7 +385,7 @@ func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain. return nil, fmt.Errorf("chapa api returned status: %d", resp.StatusCode) } - var verification domain.ChapaVerificationResponse + var verification domain.ChapaTransferVerificationResponse if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } @@ -336,6 +393,62 @@ func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain. return &verification, nil } +func (c *Client) CancelTransaction(ctx context.Context, txRef string) (domain.ChapaCancelResponse, error) { + // Construct URL for the cancel transaction endpoint + url := fmt.Sprintf("%s/transaction/cancel/%s", c.baseURL, txRef) + + // Create HTTP request with context + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, nil) + if err != nil { + return domain.ChapaCancelResponse{}, fmt.Errorf("failed to create request: %w", err) + } + + // Set authorization header + httpReq.Header.Set("Authorization", "Bearer "+c.secretKey) + httpReq.Header.Set("Content-Type", "application/json") + + // Execute the HTTP request + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return domain.ChapaCancelResponse{}, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + // Handle non-OK responses + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return domain.ChapaCancelResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) + } + + // Decode successful response + var response struct { + Message string `json:"message"` + Status string `json:"status"` + Data struct { + TxRef string `json:"tx_ref"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + } `json:"data"` + } + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return domain.ChapaCancelResponse{}, fmt.Errorf("failed to decode response: %w", err) + } + + // Return mapped domain response + return domain.ChapaCancelResponse{ + Message: response.Message, + Status: response.Status, + TxRef: response.Data.TxRef, + Amount: response.Data.Amount, + Currency: response.Data.Currency, + CreatedAt: response.Data.CreatedAt, + UpdatedAt: response.Data.UpdatedAt, + }, nil +} + func (c *Client) setHeaders(req *http.Request) { req.Header.Set("Authorization", "Bearer "+c.secretKey) req.Header.Set("Content-Type", "application/json") diff --git a/internal/services/chapa/port.go b/internal/services/chapa/port.go index 78ae033..1739482 100644 --- a/internal/services/chapa/port.go +++ b/internal/services/chapa/port.go @@ -15,11 +15,14 @@ import ( // } type ChapaStore interface { - InitializePayment(request domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) - // VerifyPayment(reference string) (domain.ChapaDepositVerification, error) - ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) + InitializePayment(request domain.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error) + ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaTransferVerificationResponse, error) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) CreateWithdrawal(userID string, amount float64, accountNumber, bankCode string) (*domain.ChapaWithdrawal, error) - HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error - HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error + HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebhookTransfer) error + HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebhookPayment) error + GetPaymentReceiptURL(ctx context.Context, chapaRef string) (string, error) + GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) + GetAccountBalance(ctx context.Context, currencyCode string) ([]domain.Balance, error) + InitiateSwap(ctx context.Context, amount float64, from, to string) (*domain.SwapResponse, error) } diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 1ecd5f3..afbc99c 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -1,9 +1,16 @@ package chapa import ( + "bytes" "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" "errors" "fmt" + "io" + "net/http" "strconv" "strings" @@ -40,6 +47,31 @@ func NewService( } } +func (s *Service) VerifyWebhookSignature(ctx context.Context, payload []byte, chapaSignature, xChapaSignature string) (bool, error) { + secret := s.cfg.CHAPA_WEBHOOK_SECRET // or os.Getenv("CHAPA_SECRET_KEY") + if secret == "" { + return false, fmt.Errorf("missing Chapa secret key in configuration") + } + + // Compute expected signature using HMAC SHA256 + h := hmac.New(sha256.New, []byte(secret)) + h.Write(payload) + expected := hex.EncodeToString(h.Sum(nil)) + + // Check either header + if chapaSignature == expected || xChapaSignature == expected { + return true, nil + } + + // Optionally log for debugging + var pretty map[string]interface{} + _ = json.Unmarshal(payload, &pretty) + fmt.Printf("[Webhook Verification Failed]\nExpected: %s\nGot chapa-signature: %s\nGot x-chapa-signature: %s\nPayload: %+v\n", + expected, chapaSignature, xChapaSignature, pretty) + + return false, fmt.Errorf("invalid webhook signature") +} + // InitiateDeposit starts a new deposit process func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) { // Validate amount @@ -53,22 +85,22 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma return "", fmt.Errorf("failed to get user: %w", err) } - var senderWallet domain.Wallet + // var senderWallet domain.Wallet // Generate unique reference // reference := uuid.New().String() reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, uuid.New().String()) - senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID) + senderWallet, err := s.walletStore.GetCustomerWallet(ctx, userID) if err != nil { - return "", fmt.Errorf("failed to get sender wallets: %w", err) - } - for _, wallet := range senderWallets { - if wallet.IsWithdraw { - senderWallet = wallet - break - } + return "", fmt.Errorf("failed to get sender wallet: %w", err) } + // for _, wallet := range senderWallets { + // if wallet.IsTransferable { + // senderWallet = wallet + // break + // } + // } // Check if payment with this reference already exists // if transfer, err := s.transferStore.GetTransferByReference(ctx, reference); err == nil { @@ -85,13 +117,20 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma ReferenceNumber: reference, // ReceiverWalletID: 1, SenderWalletID: domain.ValidInt64{ - Value: senderWallet.ID, + Value: senderWallet.RegularID, Valid: true, }, Verified: false, + Status: string(domain.STATUS_PENDING), } - payload := domain.ChapaDepositRequest{ + userPhoneNum := user.PhoneNumber[len(user.PhoneNumber)-9:] + + if len(user.PhoneNumber) >= 9 { + userPhoneNum = "0" + userPhoneNum + } + + payload := domain.ChapaInitDepositRequest{ Amount: amount, Currency: "ETB", Email: user.Email, @@ -100,6 +139,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma TxRef: reference, CallbackURL: s.cfg.CHAPA_CALLBACK_URL, ReturnURL: s.cfg.CHAPA_RETURN_URL, + PhoneNumber: userPhoneNum, } // Initialize payment with Chapa @@ -124,170 +164,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma return response.CheckoutURL, nil } -func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) { - // Parse and validate amount - amount, err := strconv.ParseInt(req.Amount, 10, 64) - if err != nil || amount <= 0 { - return nil, domain.ErrInvalidWithdrawalAmount - } - - // Get user's wallet - wallets, err := s.walletStore.GetWalletsByUser(ctx, userID) - if err != nil { - return nil, fmt.Errorf("failed to get user wallets: %w", err) - } - - var withdrawWallet domain.Wallet - for _, wallet := range wallets { - if wallet.IsWithdraw { - withdrawWallet = wallet - break - } - } - - if withdrawWallet.ID == 0 { - return nil, errors.New("withdrawal wallet not found") - } - // Check balance - if withdrawWallet.Balance < domain.Currency(amount) { - return nil, domain.ErrInsufficientBalance - } - - // Generate unique reference - reference := uuid.New().String() - - createTransfer := domain.CreateTransfer{ - Message: fmt.Sprintf("Withdrawing %d into wallet using chapa. Reference Number %s", amount, reference), - Amount: domain.Currency(amount), - Type: domain.WITHDRAW, - SenderWalletID: domain.ValidInt64{ - Value: withdrawWallet.ID, - Valid: true, - }, - Status: string(domain.PaymentStatusPending), - Verified: false, - ReferenceNumber: reference, - PaymentMethod: domain.TRANSFER_CHAPA, - } - - transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer) - - if err != nil { - return nil, fmt.Errorf("failed to create transfer record: %w", err) - } - // Initiate transfer with Chapa - transferReq := domain.ChapaWithdrawalRequest{ - AccountName: req.AccountName, - AccountNumber: req.AccountNumber, - Amount: fmt.Sprintf("%d", amount), - Currency: req.Currency, - Reference: reference, - // BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName), - BankCode: req.BankCode, - } - - success, err := s.chapaClient.InitiateTransfer(ctx, transferReq) - if err != nil { - _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) - return nil, fmt.Errorf("failed to initiate transfer: %w", err) - } - - if !success { - _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) - return nil, errors.New("chapa rejected the transfer request") - } - - // Update withdrawal status to processing - if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil { - return nil, fmt.Errorf("failed to update withdrawal status: %w", err) - } - // Deduct from wallet (or wait for webhook confirmation depending on your flow) - newBalance := withdrawWallet.Balance - domain.Currency(amount) - if err := s.walletStore.UpdateBalance(ctx, withdrawWallet.ID, newBalance); err != nil { - return nil, fmt.Errorf("failed to update wallet balance: %w", err) - } - - return &transfer, nil -} - -func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) { - banks, err := s.chapaClient.FetchSupportedBanks(ctx) - if err != nil { - return nil, fmt.Errorf("failed to fetch banks: %w", err) - } - return banks, nil -} - -func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { - // Lookup transfer by reference - transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) - if err != nil { - return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err) - } - - if transfer.Verified { - return &domain.ChapaVerificationResponse{ - Status: string(domain.PaymentStatusCompleted), - Amount: float64(transfer.Amount) / 100, - Currency: "ETB", - }, nil - } - - // Validate sender wallet - if !transfer.SenderWalletID.Valid { - return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID) - } - - var verification *domain.ChapaVerificationResponse - - // Decide verification method based on type - switch strings.ToLower(string(transfer.Type)) { - case "deposit": - // Use Chapa Payment Verification - verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef) - if err != nil { - return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err) - } - - if verification.Status == string(domain.PaymentStatusSuccessful) { - // Mark verified - if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { - return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err) - } - - // Credit wallet - _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, - transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{}, - fmt.Sprintf("Added %v to wallet using Chapa", transfer.Amount.Float32())) - if err != nil { - return nil, fmt.Errorf("failed to credit wallet: %w", err) - } - } - - case "withdraw": - // Use Chapa Transfer Verification - verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef) - if err != nil { - return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err) - } - - if verification.Status == string(domain.PaymentStatusSuccessful) { - // Mark verified (withdraw doesn't affect balance) - if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { - return nil, fmt.Errorf("failed to mark withdrawal transfer as verified: %w", err) - } - } - - default: - return nil, fmt.Errorf("unsupported transfer type: %s", transfer.Type) - } - - return verification, nil -} - -func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error { +func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, req domain.ChapaWebhookPayment) error { // Find payment by reference - payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference) + payment, err := s.transferStore.GetTransferByReference(ctx, req.TxRef) if err != nil { return domain.ErrPaymentNotFound } @@ -309,15 +188,19 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai // } // If payment is completed, credit user's wallet - if transfer.Status == string(domain.PaymentStatusSuccessful) { + if req.Status == string(domain.PaymentStatusSuccessful) { if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { + return fmt.Errorf("failed to update is payment verified value: %w", err) + } + + if err := s.transferStore.UpdateTransferStatus(ctx, payment.ID, string(domain.DepositStatusCompleted)); err != nil { return fmt.Errorf("failed to update payment status: %w", err) } if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{ ReferenceNumber: domain.ValidString{ - Value: transfer.Reference, + Value: req.TxRef, }, }, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil { return fmt.Errorf("failed to credit user wallet: %w", err) @@ -327,9 +210,209 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai return nil } -func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error { +func (s *Service) CancelDeposit(ctx context.Context, userID int64, txRef string) (domain.ChapaCancelResponse, error) { + // Validate input + if txRef == "" { + return domain.ChapaCancelResponse{}, fmt.Errorf("transaction reference is required") + } + + // Retrieve user to verify ownership / context (optional but good practice) + user, err := s.userStore.GetUserByID(ctx, userID) + if err != nil { + return domain.ChapaCancelResponse{}, fmt.Errorf("failed to get user: %w", err) + } + + fmt.Printf("\n\nAttempting to cancel Chapa transaction: %s for user %s (%d)\n\n", txRef, user.Email, userID) + + // Call Chapa API to cancel transaction + cancelResp, err := s.chapaClient.CancelTransaction(ctx, txRef) + if err != nil { + return domain.ChapaCancelResponse{}, fmt.Errorf("failed to cancel transaction via Chapa: %w", err) + } + + // Update transfer/payment status locally + transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) + if err != nil { + // Log but do not block cancellation if remote succeeded + fmt.Printf("Warning: unable to find local transfer for txRef %s: %v\n", txRef, err) + } else { + if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.STATUS_CANCELLED)); err != nil { + fmt.Printf("Warning: failed to update transfer status for txRef %s: %v\n", txRef, err) + } + + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, false); err != nil { + fmt.Printf("Warning: failed to update transfer status for txRef %s: %v\n", txRef, err) + } + } + + fmt.Printf("\n\nChapa cancellation response: %+v\n\n", cancelResp) + + return cancelResp, nil +} + +func (s *Service) FetchAllTransactions(ctx context.Context) ([]domain.ChapaTransaction, error) { + // Call Chapa API to get all transactions + resp, err := s.chapaClient.GetAllTransactions(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch transactions from Chapa: %w", err) + } + + if resp.Status != "success" { + return nil, fmt.Errorf("chapa API returned non-success status: %s", resp.Status) + } + + transactions := make([]domain.ChapaTransaction, 0, len(resp.Data.Transactions)) + + // Map API transactions to domain transactions + for _, t := range resp.Data.Transactions { + tx := domain.ChapaTransaction{ + Status: t.Status, + RefID: t.RefID, + Type: t.Type, + CreatedAt: t.CreatedAt, + Currency: t.Currency, + Amount: t.Amount, + Charge: t.Charge, + TransID: t.TransID, + PaymentMethod: t.PaymentMethod, + Customer: domain.ChapaCustomer{ + ID: t.Customer.ID, + Email: t.Customer.Email, + FirstName: t.Customer.FirstName, + LastName: t.Customer.LastName, + Mobile: t.Customer.Mobile, + }, + } + transactions = append(transactions, tx) + } + + return transactions, nil +} + +func (s *Service) FetchTransactionEvents(ctx context.Context, refID string) ([]domain.ChapaTransactionEvent, error) { + if refID == "" { + return nil, fmt.Errorf("transaction reference ID is required") + } + + // Call Chapa client to fetch transaction events + events, err := s.chapaClient.GetTransactionEvents(ctx, refID) + if err != nil { + return nil, fmt.Errorf("failed to fetch transaction events from Chapa: %w", err) + } + + // Optional: Transform or filter events if needed + transformedEvents := make([]domain.ChapaTransactionEvent, 0, len(events)) + for _, e := range events { + transformedEvents = append(transformedEvents, domain.ChapaTransactionEvent{ + Item: e.Item, + Message: e.Message, + Type: e.Type, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + }) + } + + return transformedEvents, nil +} + +func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) { + // Parse and validate amount + amount, err := strconv.ParseFloat(req.Amount, 64) + if err != nil || amount <= 0 { + return nil, domain.ErrInvalidWithdrawalAmount + } + + // Get user's wallet + wallet, err := s.walletStore.GetCustomerWallet(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to get user wallets: %w", err) + } + + // var withdrawWallet domain.Wallet + // for _, wallet := range wallets { + // if wallet.IsWithdraw { + // withdrawWallet = wallet + // break + // } + // } + + // if withdrawWallet.ID == 0 { + // return nil, errors.New("withdrawal wallet not found") + // } + // Check balance + if float64(wallet.RegularBalance) < float64(amount) { + return nil, domain.ErrInsufficientBalance + } + + // Generate unique reference + reference := uuid.New().String() + + createTransfer := domain.CreateTransfer{ + Message: fmt.Sprintf("Withdrawing %f into wallet using chapa. Reference Number %s", amount, reference), + Amount: domain.Currency(amount), + Type: domain.WITHDRAW, + SenderWalletID: domain.ValidInt64{ + Value: wallet.RegularID, + Valid: true, + }, + Status: string(domain.PaymentStatusPending), + Verified: false, + ReferenceNumber: reference, + PaymentMethod: domain.TRANSFER_CHAPA, + } + + transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer) + + if err != nil { + return nil, fmt.Errorf("failed to create transfer record: %w", err) + } + // Initiate transfer with Chapa + transferReq := domain.ChapaWithdrawalRequest{ + AccountName: req.AccountName, + AccountNumber: req.AccountNumber, + Amount: fmt.Sprintf("%f", amount), + Currency: req.Currency, + Reference: reference, + // BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName), + BankCode: req.BankCode, + } + + newBalance := float64(wallet.RegularBalance) - float64(amount) + if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil { + return nil, fmt.Errorf("failed to update wallet balance: %w", err) + } + + success, err := s.chapaClient.InitializeTransfer(ctx, transferReq) + if err != nil { + _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) + newBalance := float64(wallet.RegularBalance) + float64(amount) + if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil { + return nil, fmt.Errorf("failed to update wallet balance: %w", err) + } + return nil, fmt.Errorf("failed to initiate transfer: %w", err) + } + + if !success { + _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) + newBalance := float64(wallet.RegularBalance) + float64(amount) + if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil { + return nil, fmt.Errorf("failed to update wallet balance: %w", err) + } + return nil, errors.New("chapa rejected the transfer request") + } + + // Update withdrawal status to processing + // if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil { + // return nil, fmt.Errorf("failed to update withdrawal status: %w", err) + // } + // Deduct from wallet (or wait for webhook confirmation depending on your flow) + + return &transfer, nil +} + +func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, req domain.ChapaWebhookTransfer) error { // Find payment by reference - transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference) + transfer, err := s.transferStore.GetTransferByReference(ctx, req.Reference) if err != nil { return domain.ErrPaymentNotFound } @@ -350,7 +433,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai // verified = true // } - if payment.Status == string(domain.PaymentStatusSuccessful) { + if req.Status == string(domain.PaymentStatusSuccessful) { if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { return fmt.Errorf("failed to update payment status: %w", err) } // If payment is completed, credit user's walle @@ -365,3 +448,274 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai return nil } + +func (s *Service) GetPaymentReceiptURL(refId string) (string, error) { + if refId == "" { + return "", fmt.Errorf("reference ID cannot be empty") + } + + receiptURL := s.cfg.CHAPA_RECEIPT_URL + refId + return receiptURL, nil +} + +func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (any, error) { + // Lookup transfer by reference + transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) + if err != nil { + return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err) + } + + // If already verified, just return a completed response + if transfer.Verified { + return map[string]any{}, errors.New("transfer already verified") + } + + // Validate sender wallet + if !transfer.SenderWalletID.Valid { + return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID) + } + + var verification any + + switch strings.ToLower(string(transfer.Type)) { + case string(domain.DEPOSIT): + // Verify Chapa payment + verification, err := s.chapaClient.ManualVerifyPayment(ctx, txRef) + if err != nil { + return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err) + } + + if strings.ToLower(verification.Data.Status) == "success" || + verification.Status == string(domain.PaymentStatusCompleted) { + + // Credit wallet + _, err := s.walletStore.AddToWallet(ctx, + transfer.SenderWalletID.Value, + transfer.Amount, + domain.ValidInt64{}, + domain.TRANSFER_CHAPA, + domain.PaymentDetails{}, + fmt.Sprintf("Added %.2f ETB to wallet using Chapa", transfer.Amount.Float32())) + if err != nil { + return nil, fmt.Errorf("failed to credit wallet: %w", err) + } + + // Mark verified in DB + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { + return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err) + } + if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.DepositStatusCompleted)); err != nil { + return nil, fmt.Errorf("failed to update deposit transfer status: %w", err) + } + } + + case string(domain.WITHDRAW): + // Verify Chapa transfer + verification, err := s.chapaClient.ManualVerifyTransfer(ctx, txRef) + if err != nil { + return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err) + } + + if strings.ToLower(verification.Data.Status) == "success" || + verification.Status == string(domain.PaymentStatusCompleted) { + + // Deduct wallet + _, err := s.walletStore.DeductFromWallet(ctx, + transfer.SenderWalletID.Value, + transfer.Amount, + domain.ValidInt64{}, + domain.TRANSFER_CHAPA, + fmt.Sprintf("Deducted %.2f ETB from wallet using Chapa", transfer.Amount.Float32())) + if err != nil { + return nil, fmt.Errorf("failed to debit wallet: %w", err) + } + + // Mark verified in DB + if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { + return nil, fmt.Errorf("failed to mark withdraw transfer as verified: %w", err) + } + if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusCompleted)); err != nil { + return nil, fmt.Errorf("failed to update withdraw transfer status: %w", err) + } + } + + default: + return nil, fmt.Errorf("unsupported transfer type: %s", transfer.Type) + } + + return verification, nil +} + +func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.BankData, error) { + banks, err := s.chapaClient.FetchSupportedBanks(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch banks: %w", err) + } + return banks, nil +} + +func (s *Service) GetAllTransfers(ctx context.Context) (*domain.ChapaTransfersListResponse, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.cfg.CHAPA_BASE_URL+"/transfers", nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY)) + + resp, err := s.chapaClient.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch transfers: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var result domain.ChapaTransfersListResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + // Return the decoded result directly; no intermediate dynamic map needed + return &result, nil +} + +func (s *Service) GetAccountBalance(ctx context.Context, currencyCode string) ([]domain.Balance, error) { + URL := s.cfg.CHAPA_BASE_URL + "/balances" + if currencyCode != "" { + URL = fmt.Sprintf("%s/%s", URL, strings.ToLower(currencyCode)) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create balance request: %w", err) + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY)) + + resp, err := s.chapaClient.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute balance request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes)) + } + + var result struct { + Status string `json:"status"` + Message string `json:"message"` + Data []domain.Balance `json:"data"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode balance response: %w", err) + } + + return result.Data, nil +} + +func (s *Service) SwapCurrency(ctx context.Context, reqBody domain.SwapRequest) (*domain.SwapResponse, error) { + URL := s.cfg.CHAPA_BASE_URL + "/swap" + + // Normalize currency codes + reqBody.From = strings.ToUpper(reqBody.From) + reqBody.To = strings.ToUpper(reqBody.To) + + // Marshal request body + body, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal swap payload: %w", err) + } + + // Create HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL, bytes.NewBuffer(body)) + if err != nil { + return nil, fmt.Errorf("failed to create swap request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY)) + + // Execute request + resp, err := s.chapaClient.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute swap request: %w", err) + } + defer resp.Body.Close() + + // Handle unexpected status + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes)) + } + + // Decode response + var result domain.SwapResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode swap response: %w", err) + } + + return &result, nil +} + +// func (s *Service) InitiateSwap(ctx context.Context, amount float64, from, to string) (*domain.SwapResponse, error) { +// if amount < 1 { +// return nil, fmt.Errorf("amount must be at least 1 USD") +// } +// if strings.ToUpper(from) != "USD" || strings.ToUpper(to) != "ETB" { +// return nil, fmt.Errorf("only USD to ETB swap is supported") +// } + +// payload := domain.SwapRequest{ +// Amount: amount, +// From: strings.ToUpper(from), +// To: strings.ToUpper(to), +// } + +// // payload := map[string]any{ +// // "amount": amount, +// // "from": strings.ToUpper(from), +// // "to": strings.ToUpper(to), +// // } + +// body, err := json.Marshal(payload) +// if err != nil { +// return nil, fmt.Errorf("failed to encode swap payload: %w", err) +// } + +// req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.chapa.co/v1/swap", bytes.NewBuffer(body)) +// if err != nil { +// return nil, fmt.Errorf("failed to create swap request: %w", err) +// } + +// req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY)) +// req.Header.Set("Content-Type", "application/json") + +// resp, err := s.chapaClient.httpClient.Do(req) +// if err != nil { +// return nil, fmt.Errorf("failed to execute swap request: %w", err) +// } +// defer resp.Body.Close() + +// if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { +// bodyBytes, _ := io.ReadAll(resp.Body) +// return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes)) +// } + +// var result struct { +// Message string `json:"message"` +// Status string `json:"status"` +// Data domain.SwapResponse `json:"data"` +// } + +// if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { +// return nil, fmt.Errorf("failed to decode swap response: %w", err) +// } + +// return &result.Data, nil +// } diff --git a/internal/services/enet_pulse/port.go b/internal/services/enet_pulse/port.go index a75ab8e..e9e0b1b 100644 --- a/internal/services/enet_pulse/port.go +++ b/internal/services/enet_pulse/port.go @@ -14,4 +14,5 @@ type EnetPulseService interface { FetchTournamentParticipants(ctx context.Context, tournamentID string) error FetchPreMatchOdds(ctx context.Context, params domain.PreMatchOddsRequest) (*domain.PreMatchOddsResponse, error) FetchCountryFlag(ctx context.Context, countryFK int64) (*domain.ImageResponse, error) + GetAllPreoddsWithBettingOffers(ctx context.Context) ([]domain.EnetpulsePreodds, error) } diff --git a/internal/services/enet_pulse/service.go b/internal/services/enet_pulse/service.go index 0aed297..fd9b7d2 100644 --- a/internal/services/enet_pulse/service.go +++ b/internal/services/enet_pulse/service.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "strconv" + "strings" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" @@ -142,99 +143,105 @@ func (s *Service) FetchAndStoreTournamentTemplates(ctx context.Context) error { } for _, sport := range sports { - // 2️⃣ Compose URL for each sport using its Enetpulse sportFK - url := fmt.Sprintf( - "http://eapi.enetpulse.com/tournament_template/list/?sportFK=%s&username=%s&token=%s", - sport.SportID, // must be Enetpulse sportFK - s.cfg.EnetPulseConfig.UserName, - s.cfg.EnetPulseConfig.Token, - ) - fmt.Println("Fetching tournament templates:", url) - - // 3️⃣ Create HTTP request - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return fmt.Errorf("creating tournament template request for sport %s: %w", sport.SportID, err) - } - - // 4️⃣ Execute request - resp, err := s.httpClient.Do(req) - if err != nil { - return fmt.Errorf("requesting tournament templates for sport %s: %w", sport.SportID, err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("failed to fetch tournament templates for sport %s (status %d): %s", - sport.SportID, resp.StatusCode, string(body)) - } - - // 5️⃣ Decode JSON response flexibly - var raw struct { - TournamentTemplates json.RawMessage `json:"tournament_templates"` - } - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("reading tournament templates response for sport %s: %w", sport.SportID, err) - } - if err := json.Unmarshal(bodyBytes, &raw); err != nil { - return fmt.Errorf("unmarshalling raw tournament templates for sport %s: %w", sport.SportID, err) - } - - // 6️⃣ Parse depending on object or array - templates := map[string]TournamentTemplate{} - if len(raw.TournamentTemplates) > 0 && raw.TournamentTemplates[0] == '{' { - // Object (normal case) - if err := json.Unmarshal(raw.TournamentTemplates, &templates); err != nil { - return fmt.Errorf("decoding tournament templates (object) for sport %s: %w", sport.SportID, err) - } - } else { - // Array or empty → skip safely - fmt.Printf("No tournament templates found for sport %s\n", sport.SportID) + if sport.SportID != "1" { continue - } + } else { + // 2️⃣ Compose URL for each sport using its Enetpulse sportFK + url := fmt.Sprintf( + "http://eapi.enetpulse.com/tournament_template/list/?sportFK=%s&username=%s&token=%s", + sport.SportID, // must be Enetpulse sportFK + s.cfg.EnetPulseConfig.UserName, + s.cfg.EnetPulseConfig.Token, + ) - // 7️⃣ Iterate and store each tournament template - for _, tmpl := range templates { - updatesCount := 0 - if tmpl.N != "" { - if n, err := strconv.Atoi(tmpl.N); err == nil { - updatesCount = n + fmt.Println("Fetching tournament templates:", url) + + // 3️⃣ Create HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("creating tournament template request for sport %s: %w", sport.SportID, err) + } + + // 4️⃣ Execute request + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("requesting tournament templates for sport %s: %w", sport.SportID, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to fetch tournament templates for sport %s (status %d): %s", + sport.SportID, resp.StatusCode, string(body)) + } + + // 5️⃣ Decode JSON response flexibly + var raw struct { + TournamentTemplates json.RawMessage `json:"tournament_templates"` + } + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("reading tournament templates response for sport %s: %w", sport.SportID, err) + } + if err := json.Unmarshal(bodyBytes, &raw); err != nil { + return fmt.Errorf("unmarshalling raw tournament templates for sport %s: %w", sport.SportID, err) + } + + // 6️⃣ Parse depending on object or array + templates := map[string]TournamentTemplate{} + if len(raw.TournamentTemplates) > 0 && raw.TournamentTemplates[0] == '{' { + // Object (normal case) + if err := json.Unmarshal(raw.TournamentTemplates, &templates); err != nil { + return fmt.Errorf("decoding tournament templates (object) for sport %s: %w", sport.SportID, err) + } + } else { + // Array or empty → skip safely + fmt.Printf("No tournament templates found for sport %s\n", sport.SportID) + continue + } + + // 7️⃣ Iterate and store each tournament template + for _, tmpl := range templates { + updatesCount := 0 + if tmpl.N != "" { + if n, err := strconv.Atoi(tmpl.N); err == nil { + updatesCount = n + } + } + + lastUpdatedAt, err := time.Parse(time.RFC3339, tmpl.UT) + if err != nil { + lastUpdatedAt = time.Time{} + } + + // Convert sport.SportID from string to int64 + sportFK, err := strconv.ParseInt(sport.SportID, 10, 64) + if err != nil { + fmt.Printf("failed to convert sport.SportID '%s' to int64: %v\n", sport.SportID, err) + continue + } + + createTemplate := domain.CreateEnetpulseTournamentTemplate{ + TemplateID: tmpl.ID, + Name: tmpl.Name, + SportFK: sportFK, // use DB sport ID internally + Gender: tmpl.Gender, + UpdatesCount: updatesCount, + LastUpdatedAt: lastUpdatedAt, + Status: 1, // default active + } + + if _, err := s.store.CreateEnetpulseTournamentTemplate(ctx, createTemplate); err != nil { + fmt.Printf("failed to store tournament template %s: %v\n", tmpl.ID, err) + continue } } - - lastUpdatedAt, err := time.Parse(time.RFC3339, tmpl.UT) - if err != nil { - lastUpdatedAt = time.Time{} - } - - // Convert sport.SportID from string to int64 - sportFK, err := strconv.ParseInt(sport.SportID, 10, 64) - if err != nil { - fmt.Printf("failed to convert sport.SportID '%s' to int64: %v\n", sport.SportID, err) - continue - } - - createTemplate := domain.CreateEnetpulseTournamentTemplate{ - TemplateID: tmpl.ID, - Name: tmpl.Name, - SportFK: sportFK, // use DB sport ID internally - Gender: tmpl.Gender, - UpdatesCount: updatesCount, - LastUpdatedAt: lastUpdatedAt, - Status: 1, // default active - } - - if _, err := s.store.CreateEnetpulseTournamentTemplate(ctx, createTemplate); err != nil { - fmt.Printf("failed to store tournament template %s: %v\n", tmpl.ID, err) - continue - } + break } } - fmt.Println("✅ Successfully fetched and stored all tournament templates") + // fmt.Println("✅ Successfully fetched and stored all tournament templates") return nil } @@ -360,7 +367,7 @@ func (s *Service) FetchAndStoreTournamentStages(ctx context.Context) error { for _, t := range tournaments { // Compose URL for each tournament url := fmt.Sprintf( - "http://eapi.enetpulse.com/tournament_stage/list/?language_typeFK=3&tz=Europe/Sofia&tournamentFK=%s&username=%s&token=%s", + "https://eapi.enetpulse.com/tournament_stage/list/?language_typeFK=3&tz=Europe/Sofia&tournamentFK=%s&username=%s&token=%s", t.TournamentID, s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, @@ -452,6 +459,691 @@ func (s *Service) GetAllTournamentStages(ctx context.Context) ([]domain.Enetpuls return stages, nil } +func (s *Service) FetchAndStoreFixtures(ctx context.Context, date string) error { + // 1️⃣ Fetch all sports from DB + sports, err := s.store.GetAllEnetpulseSports(ctx) + if err != nil { + return fmt.Errorf("failed to fetch sports from DB: %w", err) + } + + // Define API fixture struct + type Fixture struct { + FixtureID string `json:"id"` + Name string `json:"name"` + SportFK string `json:"sportFK"` + TournamentFK string `json:"tournamentFK"` + TournamentTemplateFK string `json:"tournament_templateFK"` + TournamentStageFK string `json:"tournament_stageFK"` + TournamentStageName string `json:"tournament_stage_name"` + TournamentName string `json:"tournament_name"` + TournamentTemplateName string `json:"tournament_template_name"` + SportName string `json:"sport_name"` + Gender string `json:"gender"` + StartDate string `json:"startdate"` // ISO 8601 + StatusType string `json:"status_type"` + StatusDescFK string `json:"status_descFK"` + RoundTypeFK string `json:"round_typeFK"` + UpdatesCount string `json:"n"` // convert to int + LastUpdatedAt string `json:"ut"` // parse to time.Time + } + + // 2️⃣ Loop through each sport + for _, sport := range sports { + if sport.SportID != "1" { + continue + } + + url := fmt.Sprintf( + "https://eapi.enetpulse.com/event/fixtures/?username=%s&token=%s&sportFK=%s&language_typeFK=3&date=%s", + s.cfg.EnetPulseConfig.UserName, + s.cfg.EnetPulseConfig.Token, + sport.SportID, + date, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + fmt.Printf("creating request for sport %s: %v\n", sport.SportID, err) + continue + } + + resp, err := s.httpClient.Do(req) + if err != nil { + fmt.Printf("requesting fixtures for sport %s: %v\n", sport.SportID, err) + continue + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + fmt.Printf("failed to fetch fixtures for sport %s (status %d): %s\n", + sport.SportID, resp.StatusCode, string(body)) + continue + } + + // 3️⃣ Decode API response + var fixturesResp struct { + Events map[string]Fixture `json:"events"` + } + if err := json.NewDecoder(resp.Body).Decode(&fixturesResp); err != nil { + fmt.Printf("decoding fixtures for sport %s: %v\n", sport.SportID, err) + continue + } + + // 4️⃣ Iterate and upsert fixtures + for _, fx := range fixturesResp.Events { + // Parse StartDate and LastUpdatedAt + startDate, err := time.Parse(time.RFC3339, fx.StartDate) + if err != nil { + fmt.Printf("invalid startDate for fixture %s: %v\n", fx.FixtureID, err) + continue + } + lastUpdated, err := time.Parse(time.RFC3339, fx.LastUpdatedAt) + if err != nil { + fmt.Printf("invalid lastUpdatedAt for fixture %s: %v\n", fx.FixtureID, err) + continue + } + + // Convert UpdatesCount + updatesCount, err := strconv.Atoi(fx.UpdatesCount) + if err != nil { + fmt.Printf("invalid updatesCount for fixture %s: %v\n", fx.FixtureID, err) + updatesCount = 0 + } + + fixture := domain.CreateEnetpulseFixture{ + FixtureID: fx.FixtureID, + Name: fx.Name, + SportFK: fx.SportFK, + TournamentFK: fx.TournamentFK, + TournamentTemplateFK: fx.TournamentTemplateFK, + TournamentStageFK: fx.TournamentStageFK, + TournamentStageName: fx.TournamentStageName, + TournamentName: fx.TournamentName, + TournamentTemplateName: fx.TournamentTemplateName, + SportName: fx.SportName, + Gender: fx.Gender, + StartDate: startDate, + StatusType: fx.StatusType, + StatusDescFK: fx.StatusDescFK, + RoundTypeFK: fx.RoundTypeFK, + UpdatesCount: updatesCount, + LastUpdatedAt: lastUpdated, + } + + // 5️⃣ Save fixture using UPSERT repository method + if _, err := s.store.CreateEnetpulseFixture(ctx, fixture); err != nil { + fmt.Printf("failed upserting fixture %s: %v\n", fx.FixtureID, err) + continue + } + } + + fmt.Printf("✅ Successfully fetched and stored fixtures for sport %s\n", sport.SportID) + break + } + + return nil +} + +func (s *Service) GetAllFixtures(ctx context.Context) ([]domain.EnetpulseFixture, error) { + // 1️⃣ Fetch all from store + fixtures, err := s.store.GetAllEnetpulseFixtures(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch fixtures from DB: %w", err) + } + return fixtures, nil +} + +func (s *Service) FetchAndStoreResults(ctx context.Context) error { + sports, err := s.store.GetAllEnetpulseSports(ctx) + if err != nil { + return fmt.Errorf("failed to fetch sports from DB: %w", err) + } + + for _, sport := range sports { + if sport.SportID != "1" { + continue + } + + today := time.Now().Format("2006-01-02") + url := fmt.Sprintf( + "http://eapi.enetpulse.com/event/results/?sportFK=%s&date=%s&username=%s&token=%s", + sport.SportID, + today, + s.cfg.EnetPulseConfig.UserName, + s.cfg.EnetPulseConfig.Token, + ) + fmt.Println("Fetching results:", url) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("creating results request for sport %s: %w", sport.SportID, err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("requesting results for sport %s: %w", sport.SportID, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to fetch results for sport %s (status %d): %s", + sport.SportID, resp.StatusCode, string(body)) + } + + var data struct { + Events []struct { + ID string `json:"id"` + Name string `json:"name"` + SportFK string `json:"sportFK"` + TournamentFK string `json:"tournamentFK"` + TournamentTemplateFK string `json:"tournament_templateFK"` + TournamentStageFK string `json:"tournament_stageFK"` + TournamentStageName string `json:"tournament_stage_name"` + TournamentName string `json:"tournament_name"` + TournamentTemplateName string `json:"tournament_template_name"` + SportName string `json:"sport_name"` + StartDate string `json:"startdate"` + StatusType string `json:"status_type"` + StatusDescFK string `json:"status_descFK"` + RoundTypeFK string `json:"round_typeFK"` + N string `json:"n"` + UT string `json:"ut"` + + Property map[string]struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Value string `json:"value"` + N string `json:"n"` + UT string `json:"ut"` + } `json:"property"` + + EventParticipants map[string]struct { + ID string `json:"id"` + Number string `json:"number"` + ParticipantFK string `json:"participantFK"` + EventFK string `json:"eventFK"` + + Result map[string]struct { + ID string `json:"id"` + EventParticipantsFK string `json:"event_participantsFK"` + ResultTypeFK string `json:"result_typeFK"` + ResultCode string `json:"result_code"` + Value string `json:"value"` + N string `json:"n"` + UT string `json:"ut"` + } `json:"result"` + + Participant struct { + ID string `json:"id"` + Name string `json:"name"` + Gender string `json:"gender"` + Type string `json:"type"` + CountryFK string `json:"countryFK"` + CountryName string `json:"country_name"` + } `json:"participant"` + } `json:"event_participants"` + } `json:"events"` + } + + bodyBytes, _ := io.ReadAll(resp.Body) + if err := json.Unmarshal(bodyBytes, &data); err != nil { + return fmt.Errorf("decoding results failed: %w", err) + } + + for _, event := range data.Events { + // 1️⃣ Create result record + lastUpdatedAt, _ := time.Parse(time.RFC3339, event.UT) + startDate, _ := time.Parse(time.RFC3339, event.StartDate) + + createResult := domain.CreateEnetpulseResult{ + ResultID: event.ID, + Name: event.Name, + SportFK: event.SportFK, + TournamentFK: event.TournamentFK, + TournamentTemplateFK: event.TournamentTemplateFK, + TournamentStageName: event.TournamentStageName, + TournamentName: event.TournamentName, + TournamentTemplateName: event.TournamentTemplateName, + SportName: event.SportName, + StartDate: startDate, + StatusType: event.StatusType, + StatusDescFK: event.StatusDescFK, + RoundTypeFK: event.RoundTypeFK, + LastUpdatedAt: lastUpdatedAt, + } + + if _, err := s.store.CreateEnetpulseResult(ctx, createResult); err != nil { + fmt.Printf("❌ failed to store result %s: %v\n", event.ID, err) + continue + } + + // 2️⃣ Create referees (type == "ref:participant") + for _, prop := range event.Property { + if strings.HasPrefix(prop.Type, "ref:participant") { + refCreatedAt, _ := time.Parse(time.RFC3339, prop.UT) + ref := domain.CreateEnetpulseResultReferee{ + ResultFk: event.ID, + RefereeFk: prop.Value, + LastUpdatedAt: refCreatedAt, + } + if _, err := s.store.CreateEnetpulseResultReferee(ctx, ref); err != nil { + fmt.Printf("⚠️ failed to create referee %s: %v\n", prop.Name, err) + } + } + } + + // 3️⃣ Create participants + their results + for _, ep := range event.EventParticipants { + p := domain.CreateEnetpulseResultParticipant{ + ParticipantMapID: ep.ID, + ResultFk: ep.EventFK, + ParticipantFk: ep.ParticipantFK, + Name: ep.Participant.Name, + CountryFk: ep.Participant.CountryFK, + CountryName: ep.Participant.CountryName, + } + if _, err := s.store.CreateEnetpulseResultParticipant(ctx, p); err != nil { + fmt.Printf("⚠️ failed to create participant %s: %v\n", ep.Participant.Name, err) + continue + } + } + } + + break // stop after the first sport (football) + } + + fmt.Println("✅ Successfully fetched and stored EnetPulse results + participants + referees") + return nil +} + +func (s *Service) GetAllResults(ctx context.Context) ([]domain.EnetpulseResult, error) { + results, err := s.store.GetAllEnetpulseResults(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch results from DB: %w", err) + } + + fmt.Printf("✅ Retrieved %d results from DB\n", len(results)) + return results, nil +} + +// FetchAndStoreOutcomeTypes fetches outcome types from EnetPulse API and stores them in the DB. +func (s *Service) FetchAndStoreOutcomeTypes(ctx context.Context) error { + // 1️⃣ Compose EnetPulse API URL + url := fmt.Sprintf( + "http://eapi.enetpulse.com/static/outcome_type/?language_typeFK=3&username=%s&token=%s", + s.cfg.EnetPulseConfig.UserName, + s.cfg.EnetPulseConfig.Token, + ) + + // 2️⃣ Create HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("failed to create outcome types request: %w", err) + } + + // 3️⃣ Execute request + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to call EnetPulse outcome_type API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("unexpected status %d fetching outcome types: %s", resp.StatusCode, string(body)) + } + + // 4️⃣ Decode JSON response + var outcomeResp struct { + OutcomeTypes map[string]struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + N string `json:"n"` + UT string `json:"ut"` + } `json:"outcome_type"` + } + + if err := json.NewDecoder(resp.Body).Decode(&outcomeResp); err != nil { + return fmt.Errorf("failed to decode outcome types JSON: %w", err) + } + + // 5️⃣ Iterate and store each outcome type + for _, ot := range outcomeResp.OutcomeTypes { + updatesCount := 0 + if ot.N != "" { + if n, err := strconv.Atoi(ot.N); err == nil { + updatesCount = n + } + } + + lastUpdatedAt, err := time.Parse(time.RFC3339, ot.UT) + if err != nil { + lastUpdatedAt = time.Time{} + } + + createOutcome := domain.CreateEnetpulseOutcomeType{ + OutcomeTypeID: ot.ID, + Name: ot.Name, + Description: ot.Description, + UpdatesCount: int32(updatesCount), + LastUpdatedAt: lastUpdatedAt, + } + + // 6️⃣ Save to DB (upsert) + if _, err := s.store.CreateEnetpulseOutcomeType(ctx, createOutcome); err != nil { + // Optionally log the failure, continue to next + continue + } + } + + // s.logger.Info("✅ Successfully fetched and stored all EnetPulse outcome types") + return nil +} + +// GetAllOutcomeTypes retrieves all stored outcome types from the DB. +func (s *Service) GetAllOutcomeTypes(ctx context.Context) ([]domain.EnetpulseOutcomeType, error) { + outcomes, err := s.store.GetAllEnetpulseOutcomeTypes(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch outcome types from DB: %w", err) + } + + // s.logger.Info("✅ Fetched outcome types from DB", zap.Int("count", len(outcomes))) + return outcomes, nil +} + +func (s *Service) FetchAndStorePreodds(ctx context.Context) error { + // 1️⃣ Fetch all fixtures + fixtures, err := s.store.GetAllEnetpulseFixtures(ctx) + if err != nil { + return fmt.Errorf("failed to fetch fixtures: %w", err) + } + + // 2️⃣ Fetch all outcome types + outcomeTypes, err := s.store.GetAllEnetpulseOutcomeTypes(ctx) + if err != nil { + return fmt.Errorf("failed to fetch outcome types: %w", err) + } + + // 3️⃣ Loop through each fixture + for _, fixture := range fixtures { + // 4️⃣ Loop through each outcome type + for _, outcome := range outcomeTypes { + + url := fmt.Sprintf( + "http://eapi.enetpulse.com/preodds/event/?objectFK=%s&odds_providerFK=%s&outcome_typeFK=%s&username=%s&token=%s", + fixture.FixtureID, + s.cfg.EnetPulseConfig.ProviderID, + outcome.OutcomeTypeID, + s.cfg.EnetPulseConfig.UserName, + s.cfg.EnetPulseConfig.Token, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + continue + } + + resp, err := s.httpClient.Do(req) + if err != nil { + continue + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + continue + } + + // Struct adjusted exactly to match JSON structure + var preoddsResp struct { + Preodds map[string]struct { + ID string `json:"id"` + OutcomeTypeFK string `json:"outcome_typeFK"` + OutcomeScopeFK string `json:"outcome_scopeFK"` + OutcomeSubtypeFK string `json:"outcome_subtypeFK"` + EventParticipantNumber string `json:"event_participant_number"` + Iparam string `json:"iparam"` + Iparam2 string `json:"iparam2"` + Dparam string `json:"dparam"` + Dparam2 string `json:"dparam2"` + Sparam string `json:"sparam"` + N string `json:"n"` + UT string `json:"ut"` + + PreoddsBettingOffers map[string]struct { + ID string `json:"id"` + BettingOfferStatusFK string `json:"bettingoffer_statusFK"` + OddsProviderFK string `json:"odds_providerFK"` + Odds string `json:"odds"` + OddsOld string `json:"odds_old"` + Active string `json:"active"` + CouponKey string `json:"couponKey"` + N string `json:"n"` + UT string `json:"ut"` + } `json:"preodds_bettingoffers"` + } `json:"preodds"` + } + + if err := json.NewDecoder(resp.Body).Decode(&preoddsResp); err != nil { + continue + } + + for _, p := range preoddsResp.Preodds { + // Convert numeric/string fields safely + updatesCount, _ := strconv.Atoi(defaultIfEmpty(p.N, "0")) + eventParticipantNumber, _ := strconv.Atoi(defaultIfEmpty(p.EventParticipantNumber, "0")) + lastUpdatedAt := parseTimeOrNow(p.UT) + + createPreodds := domain.CreateEnetpulsePreodds{ + PreoddsID: p.ID, + EventFK: fixture.FixtureID, + OutcomeTypeFK: p.OutcomeTypeFK, + OutcomeScopeFK: p.OutcomeScopeFK, + OutcomeSubtypeFK: p.OutcomeSubtypeFK, + EventParticipantNumber: eventParticipantNumber, + IParam: p.Iparam, + IParam2: p.Iparam2, + DParam: p.Dparam, + DParam2: p.Dparam2, + SParam: p.Sparam, + UpdatesCount: updatesCount, + LastUpdatedAt: lastUpdatedAt, + } + + // Store preodds in DB + _, err := s.store.CreateEnetpulsePreodds(ctx, createPreodds) + if err != nil { + continue + } + + // 5️⃣ Loop through betting offers map + for _, o := range p.PreoddsBettingOffers { + bettingUpdates, _ := strconv.Atoi(defaultIfEmpty(o.N, "0")) + bettingLastUpdatedAt := parseTimeOrNow(o.UT) + + odds, _ := strconv.ParseFloat(defaultIfEmpty(o.Odds, "0"), 64) + oddsOld, _ := strconv.ParseFloat(defaultIfEmpty(o.OddsOld, "0"), 64) + bettingOfferStatusFK, _ := strconv.Atoi(defaultIfEmpty(o.BettingOfferStatusFK, "0")) + oddsProviderFK, _ := strconv.Atoi(defaultIfEmpty(o.OddsProviderFK, "0")) + + createOffer := domain.CreateEnetpulsePreoddsBettingOffer{ + BettingOfferID: o.ID, + PreoddsFK: createPreodds.PreoddsID, + BettingOfferStatusFK: int32(bettingOfferStatusFK), + OddsProviderFK: int32(oddsProviderFK), + Odds: odds, + OddsOld: oddsOld, + Active: o.Active, + CouponKey: o.CouponKey, + UpdatesCount: bettingUpdates, + LastUpdatedAt: bettingLastUpdatedAt, + } + + _, _ = s.store.CreateEnetpulsePreoddsBettingOffer(ctx, createOffer) + } + } + } + } + + return nil +} + +// Utility helpers +func defaultIfEmpty(val, def string) string { + if val == "" { + return def + } + return val +} + +func parseTimeOrNow(t string) time.Time { + parsed, err := time.Parse(time.RFC3339, t) + if err != nil { + return time.Now().UTC() + } + return parsed +} + + +// helper function to parse string to int32 safely +func ParseStringToInt32(s string) int32 { + if s == "" { + return 0 + } + i, _ := strconv.Atoi(s) + return int32(i) +} + +func (s *Service) GetAllPreodds(ctx context.Context) ([]domain.EnetpulsePreodds, error) { + preodds, err := s.store.GetAllEnetpulsePreodds(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch preodds from DB: %w", err) + } + fmt.Printf("\n\nFetched Preodds are:%v\n\n", preodds) + return preodds, nil +} + +// FetchAndStoreBettingOffers fetches betting offers from EnetPulse API and stores them in the DB. +func (s *Service) StoreBettingOffers(ctx context.Context, preoddsID string, oddsProviderIDs []int32) error { + // 1️⃣ Compose API URL + providers := make([]string, len(oddsProviderIDs)) + for i, p := range oddsProviderIDs { + providers[i] = strconv.Itoa(int(p)) + } + url := fmt.Sprintf( + "http://eapi.enetpulse.com/preodds_bettingoffer/?preoddsFK=%s&odds_providerFK=%s&username=%s&token=%s", + preoddsID, + strings.Join(providers, ","), + s.cfg.EnetPulseConfig.UserName, + s.cfg.EnetPulseConfig.Token, + ) + + // 2️⃣ Create HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("failed to create betting offer request: %w", err) + } + + // 3️⃣ Execute request + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to call EnetPulse betting offer API: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("unexpected status %d fetching betting offers: %s", resp.StatusCode, string(body)) + } + + // 4️⃣ Decode JSON response + var offerResp struct { + BettingOffers map[string]struct { + ID string `json:"id"` + PreoddsFK string `json:"preodds_fk"` + BettingOfferStatusFK int32 `json:"bettingoffer_status_fk"` + OddsProviderFK int32 `json:"odds_provider_fk"` + Odds float64 `json:"odds"` + OddsOld float64 `json:"odds_old"` + Active string `json:"active"` + CouponKey string `json:"coupon_key"` + N string `json:"n"` + UT string `json:"ut"` + } `json:"bettingoffer"` + } + + if err := json.NewDecoder(resp.Body).Decode(&offerResp); err != nil { + return fmt.Errorf("failed to decode betting offers JSON: %w", err) + } + + // 5️⃣ Iterate and store each betting offer + for _, o := range offerResp.BettingOffers { + updatesCount := 0 + if o.N != "" { + if n, err := strconv.Atoi(o.N); err == nil { + updatesCount = n + } + } + + lastUpdatedAt, err := time.Parse(time.RFC3339, o.UT) + if err != nil { + lastUpdatedAt = time.Time{} + } + + createOffer := domain.CreateEnetpulsePreoddsBettingOffer{ + BettingOfferID: o.ID, + PreoddsFK: preoddsID, + BettingOfferStatusFK: o.BettingOfferStatusFK, + OddsProviderFK: o.OddsProviderFK, + Odds: o.Odds, + OddsOld: o.OddsOld, + Active: o.Active, + CouponKey: o.CouponKey, + UpdatesCount: int(updatesCount), + LastUpdatedAt: lastUpdatedAt, + } + + // 6️⃣ Save to DB + if _, err := s.store.CreateEnetpulsePreoddsBettingOffer(ctx, createOffer); err != nil { + // optionally log the failure and continue + continue + } + } + + return nil +} + +// GetAllBettingOffers retrieves all stored betting offers from the DB. +func (s *Service) GetAllBettingOffers(ctx context.Context) ([]domain.EnetpulsePreoddsBettingOffer, error) { + offers, err := s.store.GetAllEnetpulsePreoddsBettingOffers(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch betting offers from DB: %w", err) + } + return offers, nil +} + +func (s *Service) GetAllPreoddsWithBettingOffers(ctx context.Context) ([]domain.EnetpulsePreodds, error) { + preodds, err := s.store.GetAllEnetpulsePreoddsWithBettingOffers(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch preodds with betting offers from DB: %w", err) + } + + return preodds, nil +} + +func (s *Service) GetFixturesWithPreodds(ctx context.Context) ([]domain.EnetpulseFixtureWithPreodds, error) { + // 1️⃣ Fetch fixtures and their associated preodds from the repository + fixtures, err := s.store.GetFixturesWithPreodds(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch fixtures with preodds from DB: %w", err) + } + + return fixtures, nil +} + func (s *Service) FetchTournamentTemplates(ctx context.Context) (*domain.TournamentTemplatesResponse, error) { url := fmt.Sprintf( "http://eapi.enetpulse.com/tournamenttemplate/list/?username=%s&token=%s", @@ -701,9 +1393,9 @@ func (s *Service) FetchDailyEvents(ctx context.Context, req domain.DailyEventsRe if req.TournamentTemplateFK != 0 { query += fmt.Sprintf("&tournament_templateFK=%d", req.TournamentTemplateFK) } - if req.TournamentStageFK != 0 { - query += fmt.Sprintf("&tournament_stageFK=%d", req.TournamentStageFK) - } + // if req.TournamentStageFK != 0 { + // query += fmt.Sprintf("&tournament_stageFK=%d", req.TournamentStageFK) + // } // Optionals if req.Date != "" { @@ -757,73 +1449,6 @@ func (s *Service) FetchDailyEvents(ctx context.Context, req domain.DailyEventsRe return &dailyResp, nil } -func (s *Service) FetchFixtures(ctx context.Context, params domain.FixturesRequest) (*domain.FixturesResponse, error) { - // Build base URL - url := fmt.Sprintf("http://eapi.enetpulse.com/event/fixtures/?username=%s&token=%s", - s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) - - // Required filter: one of sportFK | tournament_templateFK | tournament_stageFK - if params.SportFK != 0 { - url += fmt.Sprintf("&sportFK=%d", params.SportFK) - } - if params.TournamentTemplateFK != 0 { - url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK) - } - if params.TournamentStageFK != 0 { - url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) - } - - // Optional filters - if params.LanguageTypeFK != 0 { - url += fmt.Sprintf("&language_typeFK=%d", params.LanguageTypeFK) - } else { - url += "&language_typeFK=3" // default to English - } - if params.Date != "" { - url += fmt.Sprintf("&date=%s", params.Date) - } - if params.Live != "" { - url += fmt.Sprintf("&live=%s", params.Live) - } - if params.IncludeVenue { - url += "&includeVenue=yes" - } - if !params.IncludeEventProperties { - url += "&includeEventProperties=no" - } - if params.IncludeCountryCodes { - url += "&includeCountryCodes=yes" - } - if params.IncludeFirstLastName { - url += "&includeFirstLastName=yes" - } - - // Make request - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, fmt.Errorf("creating fixtures request: %w", err) - } - - resp, err := s.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("requesting fixtures: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) - } - - // Decode response - var fixturesResp domain.FixturesResponse - if err := json.NewDecoder(resp.Body).Decode(&fixturesResp); err != nil { - return nil, fmt.Errorf("decoding fixtures response: %w", err) - } - - return &fixturesResp, nil -} - func (s *Service) FetchResults(ctx context.Context, params domain.ResultsRequest) (*domain.ResultsResponse, error) { // Build base URL url := fmt.Sprintf("http://eapi.enetpulse.com/event/results/?username=%s&token=%s", @@ -836,9 +1461,9 @@ func (s *Service) FetchResults(ctx context.Context, params domain.ResultsRequest if params.TournamentTemplateFK != 0 { url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK) } - if params.TournamentStageFK != 0 { - url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) - } + // if params.TournamentStageFK != 0 { + // url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) + // } // Optional filters if params.LanguageTypeFK != 0 { @@ -974,7 +1599,7 @@ func (s *Service) FetchEventDetails(ctx context.Context, params domain.EventDeta func (s *Service) FetchEventList(ctx context.Context, params domain.EventListRequest) (*domain.EventListResponse, error) { // You must provide either TournamentFK or TournamentStageFK - if params.TournamentFK == 0 && params.TournamentStageFK == 0 { + if params.TournamentFK == 0 { return nil, fmt.Errorf("either TournamentFK or TournamentStageFK is required") } @@ -986,9 +1611,9 @@ func (s *Service) FetchEventList(ctx context.Context, params domain.EventListReq if params.TournamentFK != 0 { url += fmt.Sprintf("&tournamentFK=%d", params.TournamentFK) } - if params.TournamentStageFK != 0 { - url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) - } + // if params.TournamentStageFK != 0 { + // url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) + // } // Optional parameters if !params.IncludeEventProperties { @@ -1059,9 +1684,9 @@ func (s *Service) FetchParticipantFixtures(ctx context.Context, params domain.Pa if params.TournamentTemplateFK != 0 { url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK) } - if params.TournamentStageFK != 0 { - url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) - } + // if params.TournamentStageFK != 0 { + // url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) + // } if params.Date != "" { url += "&date=" + params.Date } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 24b72da..0b5b822 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -87,9 +87,9 @@ func (s *ServiceImpl) ProcessBet365Odds(ctx context.Context) error { Value: domain.STATUS_PENDING, Valid: true, }, - Source: domain.ValidEventSource{ - Value: domain.EVENT_SOURCE_BET365, - }, + // Source: domain.ValidEventSource{ + // Value: domain.EVENT_SOURCE_BET365, + // }, }) if err != nil { s.mongoLogger.Error( diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 50a1550..9586667 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -237,10 +237,10 @@ func (s *Service) FetchB365ResultAndUpdateBets(ctx context.Context) error { Value: time.Now(), Valid: true, }, - Source: domain.ValidEventSource{ - Value: domain.EVENT_SOURCE_BET365, - Valid: true, - }, + // Source: domain.ValidEventSource{ + // Value: domain.EVENT_SOURCE_BET365, + // Valid: true, + // }, }) if err != nil { @@ -463,10 +463,10 @@ func (s *Service) CheckAndUpdateExpiredB365Events(ctx context.Context) (int64, e Value: time.Now(), Valid: true, }, - Source: domain.ValidEventSource{ - Value: domain.EVENT_SOURCE_BET365, - Valid: true, - }, + // Source: domain.ValidEventSource{ + // Value: domain.EVENT_SOURCE_BET365, + // Valid: true, + // }, }) if err != nil { s.mongoLogger.Error( @@ -685,7 +685,7 @@ func (s *Service) GetBet365ResultForEvent(ctx context.Context, b365EventID strin zap.String("b365EventID", b365EventID), zap.Error(err), ) - return json.RawMessage{}, nil, fmt.Errorf("invalid API response for event %d", b365EventID) + return json.RawMessage{}, nil, fmt.Errorf("invalid API response for event %s", b365EventID) } var commonResp domain.CommonResultResponse diff --git a/internal/services/santimpay/service.go b/internal/services/santimpay/service.go index d5e87d1..de58b2e 100644 --- a/internal/services/santimpay/service.go +++ b/internal/services/santimpay/service.go @@ -135,16 +135,16 @@ func (s *SantimPayService) ProcessCallback(ctx context.Context, payload domain.S return fmt.Errorf("invalid ThirdPartyId '%s': %w", payload.ThirdPartyId, err) } - wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) + wallet, err := s.walletSvc.GetCustomerWallet(ctx, userID) if err != nil { - return fmt.Errorf("failed to get wallets for user %d: %w", userID, err) + return fmt.Errorf("failed to get wallets for customer %d: %w", userID, err) } // Optionally, credit user wallet if transfer.Type == domain.DEPOSIT { if _, err := s.walletSvc.AddToWallet( ctx, - wallets[0].ID, + wallet.RegularID, domain.Currency(amount), domain.ValidInt64{}, domain.TRANSFER_SANTIMPAY, diff --git a/internal/services/virtualGame/Alea/service.go b/internal/services/virtualGame/Alea/service.go index e30a61e..5c7b367 100644 --- a/internal/services/virtualGame/Alea/service.go +++ b/internal/services/virtualGame/Alea/service.go @@ -102,13 +102,13 @@ func (s *AleaPlayService) HandleCallback(ctx context.Context, callback *domain.A } // Update session status using the proper repository method - if callback.Type == "SESSION_END" { - if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil { - s.logger.Error("failed to update session status", - "sessionID", session.ID, - "error", err) - } - } + // if callback.Type == "SESSION_END" { + // if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil { + // s.logger.Error("failed to update session status", + // "sessionID", session.ID, + // "error", err) + // } + // } return nil } diff --git a/internal/services/virtualGame/atlas/client.go b/internal/services/virtualGame/atlas/client.go index 70d9713..c504e9c 100644 --- a/internal/services/virtualGame/atlas/client.go +++ b/internal/services/virtualGame/atlas/client.go @@ -52,7 +52,6 @@ func (c *Client) generateHash(body []byte, timestamp string) string { func (c *Client) post(ctx context.Context, path string, body map[string]any, result any) error { // Add timestamp first timestamp := nowTimestamp() - body["timestamp"] = timestamp // Marshal without hash first tmp, _ := json.Marshal(body) @@ -61,12 +60,14 @@ func (c *Client) post(ctx context.Context, path string, body map[string]any, res hash := c.generateHash(tmp, timestamp) body["hash"] = hash + body["timestamp"] = timestamp + fmt.Printf("atlasPost: %v \n", body) // Marshal final body data, _ := json.Marshal(body) req, _ := http.NewRequestWithContext(ctx, "POST", c.BaseURL+path, bytes.NewReader(data)) - req.Header.Set("Content-Type", "text/javascript") + req.Header.Set("Content-Type", "application/json") // Debug fmt.Println("Request URL:", c.BaseURL+path) diff --git a/internal/services/virtualGame/atlas/service.go b/internal/services/virtualGame/atlas/service.go index 39a1c7b..854fe89 100644 --- a/internal/services/virtualGame/atlas/service.go +++ b/internal/services/virtualGame/atlas/service.go @@ -117,7 +117,7 @@ func (s *Service) ProcessBet(ctx context.Context, req domain.AtlasBetRequest) (* } // 6. Deduct amount from wallet (record transaction) - _, err = s.walletSvc.DeductFromWallet(ctx, wallet.ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), "") + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)-req.Amount)) if err != nil { return nil, fmt.Errorf("failed to debit wallet: %w", err) } @@ -158,13 +158,13 @@ func (s *Service) ProcessBetWin(ctx context.Context, req domain.AtlasBetWinReque } // 6. Deduct amount from wallet (record transaction) - _, err = s.walletSvc.DeductFromWallet(ctx, wallet.ID, domain.Currency(req.BetAmount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), "") + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)-req.BetAmount)) if err != nil { return nil, fmt.Errorf("failed to debit wallet: %w", err) } if req.WinAmount > 0 { - _, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(req.WinAmount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+req.WinAmount)) if err != nil { return nil, fmt.Errorf("failed to credit wallet: %w", err) } @@ -173,7 +173,7 @@ func (s *Service) ProcessBetWin(ctx context.Context, req domain.AtlasBetWinReque // 8. Build response res := &domain.AtlasBetWinResponse{ PlayerID: req.PlayerID, - Balance: float64(wallet.RegularBalance) - req.BetAmount + req.WinAmount, + Balance: float64(wallet.RegularBalance), } return res, nil @@ -197,7 +197,7 @@ func (s *Service) ProcessRoundResult(ctx context.Context, req domain.RoundResult return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) } - _, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+req.Amount)) if err != nil { return nil, fmt.Errorf("failed to credit wallet: %w", err) } @@ -229,7 +229,7 @@ func (s *Service) ProcessRollBack(ctx context.Context, req domain.RollbackReques return nil, fmt.Errorf("failed to fetch transfer for reference %s: %w", req.BetTransactionID, err) } - _, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(transfer.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+float64(transfer.Amount))) if err != nil { return nil, fmt.Errorf("failed to credit wallet: %w", err) } @@ -284,7 +284,7 @@ func (s *Service) ProcessFreeSpinResult(ctx context.Context, req domain.FreeSpin return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) } - _, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+req.Amount)) if err != nil { return nil, fmt.Errorf("failed to credit wallet: %w", err) } @@ -313,7 +313,7 @@ func (s *Service) ProcessJackPot(ctx context.Context, req domain.JackpotRequest) return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) } - _, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") + _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") if err != nil { return nil, fmt.Errorf("failed to credit wallet: %w", err) } diff --git a/internal/services/virtualGame/game_orchestration.go b/internal/services/virtualGame/game_orchestration.go deleted file mode 100644 index 73972b0..0000000 --- a/internal/services/virtualGame/game_orchestration.go +++ /dev/null @@ -1,81 +0,0 @@ -package virtualgameservice - -import ( - "context" - "time" - - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" -) - -// Remove a provider by provider_id -func (s *service) RemoveProvider(ctx context.Context, providerID string) error { - return s.repo.DeleteVirtualGameProvider(ctx, providerID) -} - -// Fetch provider by provider_id -func (s *service) GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error) { - return s.repo.GetVirtualGameProviderByID(ctx, providerID) -} - -// List providers with pagination -func (s *service) ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error) { - providers, err := s.repo.ListVirtualGameProviders(ctx, limit, offset) - if err != nil { - return nil, 0, err - } - - total, err := s.repo.CountVirtualGameProviders(ctx) - if err != nil { - return nil, 0, err - } - - // Convert []dbgen.VirtualGameProvider to []domain.VirtualGameProvider - domainProviders := make([]domain.VirtualGameProvider, len(providers)) - for i, p := range providers { - var logoDark *string - if p.LogoDark.Valid { - logoDark = &p.LogoDark.String - } - - var logoLight *string - if p.LogoLight.Valid { - logoLight = &p.LogoLight.String - } - - domainProviders[i] = domain.VirtualGameProvider{ - ProviderID: p.ProviderID, - ProviderName: p.ProviderName, - Enabled: p.Enabled, - LogoDark: logoDark, - LogoLight: logoLight, - CreatedAt: p.CreatedAt.Time, - UpdatedAt: &p.UpdatedAt.Time, - // Add other fields as needed - } - } - - return domainProviders, total, nil -} - -// Enable/Disable a provider -func (s *service) SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error) { - provider, err := s.repo.UpdateVirtualGameProviderEnabled(ctx, providerID, enabled) - if err != nil { - s.logger.Error("Failed to update provider enabled status", "provider_id", providerID, "enabled", enabled, "error", err) - return nil, err - } - now := time.Now() - provider.UpdatedAt.Time = now - - domainProvider := &domain.VirtualGameProvider{ - ProviderID: provider.ProviderID, - ProviderName: provider.ProviderName, - Enabled: provider.Enabled, - CreatedAt: provider.CreatedAt.Time, - UpdatedAt: &provider.UpdatedAt.Time, - // Add other fields as needed - } - - return domainProvider, nil -} diff --git a/internal/services/virtualGame/orchestration/port.go b/internal/services/virtualGame/orchestration/port.go new file mode 100644 index 0000000..d44ccd7 --- /dev/null +++ b/internal/services/virtualGame/orchestration/port.go @@ -0,0 +1,14 @@ +package orchestration + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type OrchestrationService interface { + FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) + GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) + AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) +} diff --git a/internal/services/virtualGame/orchestration/service.go b/internal/services/virtualGame/orchestration/service.go new file mode 100644 index 0000000..d237f11 --- /dev/null +++ b/internal/services/virtualGame/orchestration/service.go @@ -0,0 +1,526 @@ +package orchestration + +import ( + "context" + "fmt" + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" + "github.com/jackc/pgx/v5/pgtype" +) + +type Service struct { + virtualGameSvc virtualgameservice.VirtualGameService + veliVirtualGameSvc veli.VeliVirtualGameService + repo repository.VirtualGameRepository + cfg *config.Config + client *veli.Client +} + +func New(virtualGameSvc virtualgameservice.VirtualGameService, repo repository.VirtualGameRepository, cfg *config.Config, client *veli.Client) *Service { + return &Service{ + virtualGameSvc: virtualGameSvc, + repo: repo, + cfg: cfg, + client: client, + } +} + +func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) { + + // logger := s.mongoLogger.With(zap.String("service", "AddProviders"), zap.Any("ProviderRequest", req)) + + // 0. Remove all existing providers first + if err := s.repo.DeleteAllVirtualGameProviders(ctx); err != nil { + // logger.Error("failed to delete all virtual game providers", zap.Error(err)) + return nil, fmt.Errorf("failed to clear existing providers: %w", err) + } + + // 1. Prepare signature parameters + sigParams := map[string]any{ + "brandId": req.BrandID, + } + + // Optional fields + sigParams["extraData"] = fmt.Sprintf("%t", req.ExtraData) // false is still included + if req.Size > 0 { + sigParams["size"] = fmt.Sprintf("%d", req.Size) + } else { + sigParams["size"] = "" + } + + if req.Page > 0 { + sigParams["page"] = fmt.Sprintf("%d", req.Page) + } else { + sigParams["page"] = "" + } + + // 2. Call external API + var res domain.ProviderResponse + if err := s.client.Post(ctx, "/game-lists/public/providers", req, sigParams, &res); err != nil { + return nil, fmt.Errorf("failed to fetch providers: %w", err) + } + + // 3. Loop through fetched providers and insert into DB + for _, p := range res.Items { + createParams := dbgen.CreateVirtualGameProviderParams{ + ProviderID: p.ProviderID, + ProviderName: p.ProviderName, + LogoDark: pgtype.Text{String: p.LogoForDark, Valid: p.LogoForDark != ""}, + LogoLight: pgtype.Text{String: p.LogoForLight, Valid: p.LogoForLight != ""}, + Enabled: true, + } + + if _, err := s.repo.CreateVirtualGameProvider(ctx, createParams); err != nil { + // logger.Error("failed to add provider", zap.Error(err)) + return nil, fmt.Errorf("failed to add provider %s: %w", p.ProviderID, err) + } + } + + // 4. Always add "popok" provider manually + popokParams := dbgen.CreateVirtualGameProviderParams{ + ProviderID: "popok", + ProviderName: "Popok Gaming", + LogoDark: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-dark.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed + LogoLight: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-light.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed + Enabled: true, + } + + atlasParams := dbgen.CreateVirtualGameProviderParams{ + ProviderID: "atlas", + ProviderName: "Atlas Gaming", + LogoDark: pgtype.Text{String: "/static/logos/atlas-dark.png", Valid: true}, // adjust as needed + LogoLight: pgtype.Text{String: "/static/logos/atlas-light.png", Valid: true}, // adjust as needed + Enabled: true, + } + + if _, err := s.repo.CreateVirtualGameProvider(ctx, popokParams); err != nil { + // logger.Error("failed to add popok provider", zap.Any("popokParams", popokParams), zap.Error(err)) + return nil, fmt.Errorf("failed to add popok provider: %w", err) + } + + if _, err := s.repo.CreateVirtualGameProvider(ctx, atlasParams); err != nil { + return nil, fmt.Errorf("failed to add atlas provider: %w", err) + } + + // Optionally also append it to the response for consistency + // res.Items = append(res.Items, domain.VirtualGameProvider{ + // ProviderID: uuid.New().String(), + // ProviderName: "Popok Gaming", + // LogoForDark: "/static/logos/popok-dark.png", + // LogoForLight: "/static/logos/popok-light.png", + // }) + + return &res, nil +} + +func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) { + // Build params for repo call + // logger := s.mongoLogger.With(zap.String("service", "GetAllVirtualGames"), zap.Any("params", params)) + rows, err := s.repo.ListAllVirtualGames(ctx, params) + if err != nil { + // logger.Error("[GetAllVirtualGames] Failed to fetch virtual games", zap.Error(err)) + return nil, fmt.Errorf("failed to fetch virtual games: %w", err) + } + + var allGames []domain.UnifiedGame + for _, r := range rows { + // --- Convert nullable Rtp to *float64 --- + var rtpPtr *float64 + if r.Rtp.Valid { + rtpFloat, err := r.Rtp.Float64Value() + if err == nil { + rtpPtr = new(float64) + *rtpPtr = rtpFloat.Float64 + } + } + var betsFloat64 []float64 + for _, bet := range r.Bets { + if bet.Valid { + betFloat, err := bet.Float64Value() + if err == nil { + betsFloat64 = append(betsFloat64, betFloat.Float64) + } + } + } + + allGames = append(allGames, domain.UnifiedGame{ + GameID: r.GameID, + ProviderID: r.ProviderID, + Provider: r.ProviderName, + Name: r.Name, + Category: r.Category.String, + DeviceType: r.DeviceType.String, + Volatility: r.Volatility.String, + RTP: rtpPtr, + HasDemo: r.HasDemo.Bool, + HasFreeBets: r.HasFreeBets.Bool, + Bets: betsFloat64, + Thumbnail: r.Thumbnail.String, + Status: int(r.Status.Int32), // nullable status + }) + } + + return allGames, nil +} + +func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) { + // logger := s.mongoLogger.With( + // zap.String("service", "FetchAndStoreAllVirtualGames"), + // zap.Any("ProviderRequest", req), + // ) + + // This is necessary since the provider is a foreign key + _, err := s.AddProviders(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to add providers to database: %w", err) + } + + var allGames []domain.UnifiedGame + + // --- 1. Existing providers (Veli Games) --- + providersRes, err := s.veliVirtualGameSvc.GetProviders(ctx, req) + if err != nil { + // logger.Error("Failed to fetch provider", zap.Error(err)) + return nil, fmt.Errorf("failed to fetch providers: %w", err) + } + + // --- 2. Fetch games for each provider (Veli Games) --- + for _, p := range providersRes.Items { + games, err := s.veliVirtualGameSvc.GetGames(ctx, domain.GameListRequest{ + BrandID: s.cfg.VeliGames.BrandID, + ProviderID: p.ProviderID, + Page: req.Page, + Size: req.Size, + }) + if err != nil { + // logger.Error("failed to get veli games", zap.String("ProviderID", p.ProviderID), zap.Error(err)) + continue // skip failing provider but continue others + } + + for _, g := range games { + unified := domain.UnifiedGame{ + GameID: g.GameID, + ProviderID: g.ProviderID, + Provider: p.ProviderName, + Name: g.Name, + Category: g.Category, + DeviceType: g.DeviceType, + HasDemo: g.HasDemoMode, + HasFreeBets: g.HasFreeBets, + } + allGames = append(allGames, unified) + + // Save to DB + _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + GameID: g.GameID, + ProviderID: g.ProviderID, + Name: g.Name, + Category: pgtype.Text{ + String: g.Category, + Valid: g.Category != "", + }, + DeviceType: pgtype.Text{ + String: g.DeviceType, + Valid: g.DeviceType != "", + }, + HasDemo: pgtype.Bool{ + Bool: g.HasDemoMode, + Valid: true, + }, + HasFreeBets: pgtype.Bool{ + Bool: g.HasFreeBets, + Valid: true, + }, + }) + if err != nil { + // logger.Error("failed to create virtual game", zap.Error(err)) + } + } + } + + // --- 3. Fetch Atlas-V games --- + atlasGames, err := s.veliVirtualGameSvc.GetAtlasVGames(ctx) + if err != nil { + // logger.Error("failed to fetch Atlas-V games", zap.Error(err)) + } else { + for _, g := range atlasGames { + unified := domain.UnifiedGame{ + GameID: g.GameID, + ProviderID: "atlasv", + Provider: "Atlas-V Gaming", // "Atlas-V" + Name: g.Name, + Category: g.Category, // using Type as Category + Thumbnail: g.Thumbnail, + HasDemo: true, + DemoURL: g.DemoURL, + } + allGames = append(allGames, unified) + + // Save to DB + _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + GameID: g.GameID, + ProviderID: "atlasv", + Name: g.Name, + Category: pgtype.Text{ + String: g.Category, + Valid: g.Category != "", + }, + Thumbnail: pgtype.Text{ + String: g.Thumbnail, + Valid: g.Thumbnail != "", + }, + HasDemo: pgtype.Bool{ + Bool: g.HasDemoMode, + Valid: true, + }, + }) + if err != nil { + // logger.Error("failed to create Atlas-V virtual game", zap.Error(err)) + } + } + } + + // --- 4. Handle PopOK separately --- + popokGames, err := s.virtualGameSvc.ListGames(ctx, currency) + if err != nil { + // logger.Error("failed to fetch PopOk games", zap.Error(err)) + return nil, fmt.Errorf("failed to fetch PopOK games: %w", err) + } + + for _, g := range popokGames { + unified := domain.UnifiedGame{ + GameID: fmt.Sprintf("%d", g.ID), + ProviderID: "popok", + Provider: "PopOK", + Name: g.GameName, + Category: "Crash", + Bets: g.Bets, + Thumbnail: g.Thumbnail, + Status: g.Status, + } + allGames = append(allGames, unified) + + // Convert []float64 to []pgtype.Numeric + var betsNumeric []pgtype.Numeric + for _, bet := range g.Bets { + var num pgtype.Numeric + _ = num.Scan(bet) + betsNumeric = append(betsNumeric, num) + } + + // Save to DB + _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + GameID: fmt.Sprintf("%d", g.ID), + ProviderID: "popok", + Name: g.GameName, + Bets: betsNumeric, + Thumbnail: pgtype.Text{ + String: g.Thumbnail, + Valid: g.Thumbnail != "", + }, + Status: pgtype.Int4{ + Int32: int32(g.Status), + Valid: true, + }, + HasDemo: pgtype.Bool{ + Bool: true, + Valid: true, + }, + Category: pgtype.Text{ + String: "Crash", + Valid: true, + }, + }) + if err != nil { + // logger.Error("failed to create PopOK virtual game", zap.Error(err)) + } + } + + return allGames, nil +} + +func (s *Service) RemoveProvider(ctx context.Context, providerID string) error { + return s.repo.DeleteVirtualGameProvider(ctx, providerID) +} + +// Fetch provider by provider_id +func (s *Service) GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error) { + return s.repo.GetVirtualGameProviderByID(ctx, providerID) +} + +// List providers with pagination +func (s *Service) ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error) { + providers, err := s.repo.ListVirtualGameProviders(ctx, limit, offset) + if err != nil { + return nil, 0, err + } + + total, err := s.repo.CountVirtualGameProviders(ctx) + if err != nil { + return nil, 0, err + } + + // Convert []dbgen.VirtualGameProvider to []domain.VirtualGameProvider + domainProviders := make([]domain.VirtualGameProvider, len(providers)) + for i, p := range providers { + var logoDark *string + if p.LogoDark.Valid { + logoDark = &p.LogoDark.String + } + + var logoLight *string + if p.LogoLight.Valid { + logoLight = &p.LogoLight.String + } + + domainProviders[i] = domain.VirtualGameProvider{ + ProviderID: p.ProviderID, + ProviderName: p.ProviderName, + Enabled: p.Enabled, + LogoDark: logoDark, + LogoLight: logoLight, + CreatedAt: p.CreatedAt.Time, + UpdatedAt: &p.UpdatedAt.Time, + // Add other fields as needed + } + } + + return domainProviders, total, nil +} + +// Enable/Disable a provider +func (s *Service) SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error) { + provider, err := s.repo.UpdateVirtualGameProviderEnabled(ctx, providerID, enabled) + if err != nil { + //s.logger.Error("Failed to update provider enabled status", "provider_id", providerID, "enabled", enabled, "error", err) + return nil, err + } + now := time.Now() + provider.UpdatedAt.Time = now + + domainProvider := &domain.VirtualGameProvider{ + ProviderID: provider.ProviderID, + ProviderName: provider.ProviderName, + Enabled: provider.Enabled, + CreatedAt: provider.CreatedAt.Time, + UpdatedAt: &provider.UpdatedAt.Time, + // Add other fields as needed + } + + return domainProvider, nil +} + +func (s *Service) CreateVirtualGameProviderReport(ctx context.Context, report domain.CreateVirtualGameProviderReport) (domain.VirtualGameProviderReport, error) { + // Example: logger := s.mongoLogger.With(zap.String("service", "CreateVirtualGameProviderReport"), zap.Any("Report", report)) + + // Call repository to create the report + created, err := s.repo.CreateVirtualGameProviderReport(ctx, report) + if err != nil { + // logger.Error("failed to create provider report", zap.Error(err), zap.Any("report", report)) + return domain.VirtualGameProviderReport{}, fmt.Errorf("failed to create provider report for provider %s: %w", report.ProviderID, err) + } + + // Return the created report + return created, nil +} + +func (s *Service) CreateVirtualGameReport(ctx context.Context, report domain.CreateVirtualGameReport) (domain.VirtualGameReport, error) { + // Example: logger := s.mongoLogger.With(zap.String("service", "CreateVirtualGameReport"), zap.Any("Report", report)) + + // Call repository to create the report + created, err := s.repo.CreateVirtualGameReport(ctx, report) + if err != nil { + // logger.Error("failed to create game report", zap.Error(err), zap.Any("report", report)) + return domain.VirtualGameReport{}, fmt.Errorf("failed to create game report for game %s: %w", report.GameID, err) + } + + // Return the created report + return created, nil +} + +func (s *Service) GetVirtualGameProviderReportByProviderAndDate( + ctx context.Context, + providerID string, + reportDate time.Time, + reportType string, +) (domain.VirtualGameProviderReport, error) { + // Example logger if needed + // logger := s.mongoLogger.With(zap.String("service", "GetVirtualGameProviderReportByProviderAndDate"), + // zap.String("provider_id", providerID), + // zap.Time("report_date", reportDate), + // zap.String("report_type", reportType), + // ) + + report, err := s.repo.GetVirtualGameProviderReportByProviderAndDate(ctx, providerID, reportDate, reportType) + if err != nil { + // logger.Error("failed to retrieve virtual game provider report", zap.Error(err)) + return domain.VirtualGameProviderReport{}, fmt.Errorf( + "failed to retrieve provider report for provider %s on %s (%s): %w", + providerID, reportDate.Format("2006-01-02"), reportType, err, + ) + } + + return report, nil +} + +func (s *Service) UpdateVirtualGameProviderReportByDate( + ctx context.Context, + providerID string, + reportDate time.Time, + reportType string, + totalGamesPlayed int64, + totalBets float64, + totalPayouts float64, + totalPlayers int64, +) error { + // Optionally log or trace the update + // Example: s.mongoLogger.Info("Updating virtual game provider report", + // zap.String("provider_id", providerID), + // zap.Time("report_date", reportDate), + // zap.String("report_type", reportType), + // ) + + err := s.repo.UpdateVirtualGameProviderReportByDate( + ctx, + providerID, + reportDate, + reportType, + totalGamesPlayed, + totalBets, + totalPayouts, + totalPlayers, + ) + if err != nil { + return fmt.Errorf("failed to update provider report for provider %s on %s (%s): %w", + providerID, + reportDate.Format("2006-01-02"), + reportType, + err, + ) + } + + return nil +} + +func (s *Service) ListVirtualGameProviderReportsByGamesPlayedAsc(ctx context.Context) ([]domain.VirtualGameProviderReport, error) { + reports, err := s.repo.ListVirtualGameProviderReportsByGamesPlayedAsc(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch virtual game provider reports ascending: %w", err) + } + return reports, nil +} + +// ListVirtualGameProviderReportsByGamesPlayedDesc fetches all reports sorted by total_games_played descending. +func (s *Service) ListVirtualGameProviderReportsByGamesPlayedDesc(ctx context.Context) ([]domain.VirtualGameProviderReport, error) { + reports, err := s.repo.ListVirtualGameProviderReportsByGamesPlayedDesc(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch virtual game provider reports descending: %w", err) + } + return reports, nil +} diff --git a/internal/services/virtualGame/port.go b/internal/services/virtualGame/port.go index 8121ea1..035d34b 100644 --- a/internal/services/virtualGame/port.go +++ b/internal/services/virtualGame/port.go @@ -3,16 +3,15 @@ package virtualgameservice import ( "context" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) type VirtualGameService interface { // AddProvider(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) - RemoveProvider(ctx context.Context, providerID string) error - GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error) - ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error) - SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error) + // RemoveProvider(ctx context.Context, providerID string) error + // GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error) + // ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error) + // SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index 46ddb66..c9d4e50 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -224,16 +224,16 @@ func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfo return nil, fmt.Errorf("invalid token") } - wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - if err != nil || len(wallets) == 0 { + wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + if err != nil { s.logger.Error("No wallets found for user", "userID", claims.UserID) - return nil, fmt.Errorf("no wallet found") + return nil, err } return &domain.PopOKPlayerInfoResponse{ Country: "ET", Currency: claims.Currency, - Balance: float64(wallets[0].Balance), // Convert cents to currency + Balance: float64(wallet.RegularBalance), // Convert cents to currency PlayerID: fmt.Sprintf("%d", claims.UserID), }, nil } @@ -246,17 +246,17 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) ( } // Convert amount to cents (assuming wallet uses cents) - amountCents := int64(req.Amount) + // amount := int64(req.Amount) // Deduct from wallet - userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) if err != nil { return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets") } - _, err = s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents), + _, err = s.walletSvc.DeductFromWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, - fmt.Sprintf("Deducted %v amount from wallet by system while placing virtual game bet", amountCents)) + fmt.Sprintf("Deducted %v amount from wallet by system while placing virtual game bet", req.Amount)) if err != nil { return nil, fmt.Errorf("insufficient balance") } @@ -268,7 +268,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) ( Provider: string(domain.PROVIDER_POPOK), GameID: req.GameID, TransactionType: "BET", - Amount: amountCents, // Negative for bets + Amount: int64(req.Amount), // Negative for bets Currency: req.Currency, ExternalTransactionID: req.TransactionID, Status: "COMPLETED", @@ -283,7 +283,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) ( return &domain.PopOKBetResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), // Your internal transaction ID - Balance: float64(userWallets[0].Balance), + Balance: float64(wallet.RegularBalance), }, nil } @@ -319,10 +319,15 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( } // 3. Convert amount to cents - amountCents := int64(req.Amount) + // amountCents := int64(req.Amount) + + wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("Failed to read user wallets") + } // 4. Credit to wallet - _, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, + _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet for winning PopOkBet", req.Amount), ) @@ -331,10 +336,10 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( return nil, fmt.Errorf("wallet credit failed") } - userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - if err != nil { - return &domain.PopOKWinResponse{}, fmt.Errorf("Failed to read user wallets") - } + // userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + // if err != nil { + // return &domain.PopOKWinResponse{}, fmt.Errorf("Failed to read user wallets") + // } // 5. Create transaction record tx := &domain.VirtualGameTransaction{ UserID: claims.UserID, @@ -342,7 +347,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( Provider: string(domain.PROVIDER_POPOK), GameID: req.GameID, TransactionType: "WIN", - Amount: amountCents, + Amount: int64(req.Amount), Currency: req.Currency, ExternalTransactionID: req.TransactionID, Status: "COMPLETED", @@ -354,12 +359,12 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( return nil, fmt.Errorf("transaction recording failed") } - fmt.Printf("\n\n Win balance is:%v\n\n", float64(userWallets[0].Balance)) + fmt.Printf("\n\n Win balance is:%v\n\n", float64(wallet.RegularBalance)) return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), - Balance: float64(userWallets[0].Balance), + Balance: float64(wallet.RegularBalance), }, nil } @@ -371,6 +376,11 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin return nil, fmt.Errorf("invalid token") } + wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("Failed to read user wallets") + } + // 2. Check for duplicate tournament win transaction existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) if err != nil { @@ -379,15 +389,15 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin } if existingTx != nil && existingTx.TransactionType == "TOURNAMENT_WIN" { s.logger.Warn("Duplicate tournament win", "transactionID", req.TransactionID) - wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - balance := 0.0 - if len(wallets) > 0 { - balance = float64(wallets[0].Balance) - } + // wallet, _ := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + // balance := 0.0 + // if len(wallets) > 0 { + // balance = float64(wallets[0].Balance) + // } return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", existingTx.ID), - Balance: balance, + Balance: float64(wallet.RegularBalance), }, nil } @@ -395,7 +405,7 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin amountCents := int64(req.Amount) // 4. Credit user wallet - _, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, + _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet for winning Popok Tournament", req.Amount)) if err != nil { s.logger.Error("Failed to credit wallet for tournament", "userID", claims.UserID, "error", err) @@ -419,15 +429,15 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin } // 6. Fetch updated balance - wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - if err != nil { - return nil, fmt.Errorf("Failed to get wallet balance") - } + // wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + // if err != nil { + // return nil, fmt.Errorf("Failed to get wallet balance") + // } return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), - Balance: float64(wallets[0].Balance), + Balance: float64(wallet.RegularBalance), }, nil } @@ -438,6 +448,11 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque return nil, fmt.Errorf("invalid token") } + wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("Failed to read user wallets") + } + existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) if err != nil { s.logger.Error("Failed to check existing promo transaction", "error", err) @@ -445,20 +460,20 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque } if existingTx != nil && existingTx.TransactionType == "PROMO_WIN" { s.logger.Warn("Duplicate promo win", "transactionID", req.TransactionID) - wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - balance := 0.0 - if len(wallets) > 0 { - balance = float64(wallets[0].Balance) - } + // wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + // balance := 0.0 + // if len(wallets) > 0 { + // balance = float64(wallets[0].Balance) + // } return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", existingTx.ID), - Balance: balance, + Balance: float64(wallet.RegularBalance), }, nil } - amountCents := int64(req.Amount * 100) - _, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, + // amountCents := int64(req.Amount * 100) + _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet for winning PopOk Promo Win", req.Amount)) if err != nil { s.logger.Error("Failed to credit wallet for promo", "userID", claims.UserID, "error", err) @@ -468,7 +483,7 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque tx := &domain.VirtualGameTransaction{ UserID: claims.UserID, TransactionType: "PROMO_WIN", - Amount: amountCents, + Amount: int64(wallet.RegularBalance), Currency: req.Currency, ExternalTransactionID: req.TransactionID, Status: "COMPLETED", @@ -480,15 +495,15 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque return nil, fmt.Errorf("transaction recording failed") } - wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - if err != nil { - return nil, fmt.Errorf("failed to read wallets") - } + // wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + // if err != nil { + // return nil, fmt.Errorf("failed to read wallets") + // } return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), - Balance: float64(wallets[0].Balance), + Balance: float64(wallet.RegularBalance), }, nil } @@ -535,6 +550,11 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ // return nil, fmt.Errorf("invalid token") // } + wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("Failed to read user wallets") + } + // 2. Find the original bet transaction originalBet, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) if err != nil { @@ -551,21 +571,21 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ // 4. Check if already cancelled if originalBet.Status == "CANCELLED" { s.logger.Warn("Transaction already cancelled", "transactionID", req.TransactionID) - wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - balance := 0.0 - if len(wallets) > 0 { - balance = float64(wallets[0].Balance) - } + // wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + // balance := 0.0 + // if len(wallets) > 0 { + // balance = float64(wallets[0].Balance) + // } return &domain.PopOKCancelResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", originalBet.ID), - Balance: balance, + Balance: float64(wallet.RegularBalance), }, nil } // 5. Refund the bet amount (absolute value since bet amount is negative) refundAmount := -originalBet.Amount - _, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, + _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(refundAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet as refund for cancelling PopOk bet", refundAmount), ) if err != nil { @@ -573,10 +593,10 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ return nil, fmt.Errorf("refund failed") } - userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - if err != nil { - return &domain.PopOKCancelResponse{}, fmt.Errorf("Failed to read user wallets") - } + // userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) + // if err != nil { + // return &domain.PopOKCancelResponse{}, fmt.Errorf("Failed to read user wallets") + // } // 6. Mark original bet as cancelled and create cancel record cancelTx := &domain.VirtualGameTransaction{ @@ -615,7 +635,7 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ return &domain.PopOKCancelResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", cancelTx.ID), - Balance: float64(userWallets[0].Balance), + Balance: float64(wallet.RegularBalance), }, nil } @@ -657,10 +677,6 @@ func (s *service) verifySignature(callback *domain.PopOKCallback) bool { return expected == callback.Signature } -// func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) { -// return s.repo.GetGameCounts(ctx, filter) -// } - func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) { now := time.Now().Format("02-01-2006 15:04:05") // dd-mm-yyyy hh:mm:ss diff --git a/internal/services/virtualGame/veli/client.go b/internal/services/virtualGame/veli/client.go index cf899e0..529dd41 100644 --- a/internal/services/virtualGame/veli/client.go +++ b/internal/services/virtualGame/veli/client.go @@ -88,7 +88,7 @@ func (c *Client) generateSignature(params map[string]any) (string, error) { // POST helper -func (c *Client) post(ctx context.Context, path string, body any, sigParams map[string]any, result any) error { +func (c *Client) Post(ctx context.Context, path string, body any, sigParams map[string]any, result any) error { data, _ := json.Marshal(body) sig, err := c.generateSignature(sigParams) if err != nil { diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go deleted file mode 100644 index 2b340d8..0000000 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ /dev/null @@ -1,326 +0,0 @@ -package veli - -import ( - "context" - "fmt" - - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/jackc/pgx/v5/pgtype" - "go.uber.org/zap" -) - -func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) { - - logger := s.mongoLogger.With(zap.String("service", "AddProviders"), zap.Any("ProviderRequest", req)) - - // 0. Remove all existing providers first - if err := s.repo.DeleteAllVirtualGameProviders(ctx); err != nil { - logger.Error("failed to delete all virtual game providers", zap.Error(err)) - return nil, fmt.Errorf("failed to clear existing providers: %w", err) - } - - // 1. Prepare signature parameters - sigParams := map[string]any{ - "brandId": req.BrandID, - } - - // Optional fields - sigParams["extraData"] = fmt.Sprintf("%t", req.ExtraData) // false is still included - if req.Size > 0 { - sigParams["size"] = fmt.Sprintf("%d", req.Size) - } else { - sigParams["size"] = "" - } - - if req.Page > 0 { - sigParams["page"] = fmt.Sprintf("%d", req.Page) - } else { - sigParams["page"] = "" - } - - // 2. Call external API - var res domain.ProviderResponse - if err := s.client.post(ctx, "/game-lists/public/providers", req, sigParams, &res); err != nil { - return nil, fmt.Errorf("failed to fetch providers: %w", err) - } - - // 3. Loop through fetched providers and insert into DB - for _, p := range res.Items { - createParams := dbgen.CreateVirtualGameProviderParams{ - ProviderID: p.ProviderID, - ProviderName: p.ProviderName, - LogoDark: pgtype.Text{String: p.LogoForDark, Valid: p.LogoForDark != ""}, - LogoLight: pgtype.Text{String: p.LogoForLight, Valid: p.LogoForLight != ""}, - Enabled: true, - } - - if _, err := s.repo.CreateVirtualGameProvider(ctx, createParams); err != nil { - logger.Error("failed to add provider", zap.Error(err)) - return nil, fmt.Errorf("failed to add provider %s: %w", p.ProviderID, err) - } - } - - // 4. Always add "popok" provider manually - popokParams := dbgen.CreateVirtualGameProviderParams{ - ProviderID: "popok", - ProviderName: "Popok Gaming", - LogoDark: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-dark.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed - LogoLight: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-light.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed - Enabled: true, - } - - atlasParams := dbgen.CreateVirtualGameProviderParams{ - ProviderID: "atlas", - ProviderName: "Atlas Gaming", - LogoDark: pgtype.Text{String: "/static/logos/atlas-dark.png", Valid: true}, // adjust as needed - LogoLight: pgtype.Text{String: "/static/logos/atlas-light.png", Valid: true}, // adjust as needed - Enabled: true, - } - - if _, err := s.repo.CreateVirtualGameProvider(ctx, popokParams); err != nil { - logger.Error("failed to add popok provider", zap.Any("popokParams", popokParams), zap.Error(err)) - return nil, fmt.Errorf("failed to add popok provider: %w", err) - } - - if _, err := s.repo.CreateVirtualGameProvider(ctx, atlasParams); err != nil { - return nil, fmt.Errorf("failed to add atlas provider: %w", err) - } - - // Optionally also append it to the response for consistency - // res.Items = append(res.Items, domain.VirtualGameProvider{ - // ProviderID: uuid.New().String(), - // ProviderName: "Popok Gaming", - // LogoForDark: "/static/logos/popok-dark.png", - // LogoForLight: "/static/logos/popok-light.png", - // }) - - return &res, nil -} - -func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) { - // Build params for repo call - logger := s.mongoLogger.With(zap.String("service", "GetAllVirtualGames"), zap.Any("params", params)) - rows, err := s.repo.ListAllVirtualGames(ctx, params) - if err != nil { - logger.Error("[GetAllVirtualGames] Failed to fetch virtual games", zap.Error(err)) - return nil, fmt.Errorf("failed to fetch virtual games: %w", err) - } - - var allGames []domain.UnifiedGame - for _, r := range rows { - // --- Convert nullable Rtp to *float64 --- - var rtpPtr *float64 - if r.Rtp.Valid { - rtpFloat, err := r.Rtp.Float64Value() - if err == nil { - rtpPtr = new(float64) - *rtpPtr = rtpFloat.Float64 - } - } - var betsFloat64 []float64 - for _, bet := range r.Bets { - if bet.Valid { - betFloat, err := bet.Float64Value() - if err == nil { - betsFloat64 = append(betsFloat64, betFloat.Float64) - } - } - } - - allGames = append(allGames, domain.UnifiedGame{ - GameID: r.GameID, - ProviderID: r.ProviderID, - Provider: r.ProviderName, - Name: r.Name, - Category: r.Category.String, - DeviceType: r.DeviceType.String, - Volatility: r.Volatility.String, - RTP: rtpPtr, - HasDemo: r.HasDemo.Bool, - HasFreeBets: r.HasFreeBets.Bool, - Bets: betsFloat64, - Thumbnail: r.Thumbnail.String, - Status: int(r.Status.Int32), // nullable status - }) - } - - return allGames, nil -} - -func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) { - logger := s.mongoLogger.With( - zap.String("service", "FetchAndStoreAllVirtualGames"), - zap.Any("ProviderRequest", req), - ) - - // This is necessary since the provider is a foreign key - _, err := s.AddProviders(ctx, req) - if err != nil { - return nil, fmt.Errorf("failed to add providers to database: %w", err) - } - - var allGames []domain.UnifiedGame - - // --- 1. Existing providers (Veli Games) --- - providersRes, err := s.GetProviders(ctx, req) - if err != nil { - logger.Error("Failed to fetch provider", zap.Error(err)) - return nil, fmt.Errorf("failed to fetch providers: %w", err) - } - - // --- 2. Fetch games for each provider (Veli Games) --- - for _, p := range providersRes.Items { - games, err := s.GetGames(ctx, domain.GameListRequest{ - BrandID: s.cfg.VeliGames.BrandID, - ProviderID: p.ProviderID, - Page: req.Page, - Size: req.Size, - }) - if err != nil { - logger.Error("failed to get veli games", zap.String("ProviderID", p.ProviderID), zap.Error(err)) - continue // skip failing provider but continue others - } - - for _, g := range games { - unified := domain.UnifiedGame{ - GameID: g.GameID, - ProviderID: g.ProviderID, - Provider: p.ProviderName, - Name: g.Name, - Category: g.Category, - DeviceType: g.DeviceType, - HasDemo: g.HasDemoMode, - HasFreeBets: g.HasFreeBets, - } - allGames = append(allGames, unified) - - // Save to DB - _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ - GameID: g.GameID, - ProviderID: g.ProviderID, - Name: g.Name, - Category: pgtype.Text{ - String: g.Category, - Valid: g.Category != "", - }, - DeviceType: pgtype.Text{ - String: g.DeviceType, - Valid: g.DeviceType != "", - }, - HasDemo: pgtype.Bool{ - Bool: g.HasDemoMode, - Valid: true, - }, - HasFreeBets: pgtype.Bool{ - Bool: g.HasFreeBets, - Valid: true, - }, - }) - if err != nil { - logger.Error("failed to create virtual game", zap.Error(err)) - } - } - } - - // --- 3. Fetch Atlas-V games --- - atlasGames, err := s.GetAtlasVGames(ctx) - if err != nil { - logger.Error("failed to fetch Atlas-V games", zap.Error(err)) - } else { - for _, g := range atlasGames { - unified := domain.UnifiedGame{ - GameID: g.GameID, - ProviderID: "atlasv", - Provider: "Atlas-V Gaming", // "Atlas-V" - Name: g.Name, - Category: g.Category, // using Type as Category - Thumbnail: g.Thumbnail, - HasDemo: true, - DemoURL: g.DemoURL, - } - allGames = append(allGames, unified) - - // Save to DB - _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ - GameID: g.GameID, - ProviderID: "atlasv", - Name: g.Name, - Category: pgtype.Text{ - String: g.Category, - Valid: g.Category != "", - }, - Thumbnail: pgtype.Text{ - String: g.Thumbnail, - Valid: g.Thumbnail != "", - }, - HasDemo: pgtype.Bool{ - Bool: g.HasDemoMode, - Valid: true, - }, - }) - if err != nil { - logger.Error("failed to create Atlas-V virtual game", zap.Error(err)) - } - } - } - - // --- 4. Handle PopOK separately --- - popokGames, err := s.virtualGameSvc.ListGames(ctx, currency) - if err != nil { - logger.Error("failed to fetch PopOk games", zap.Error(err)) - return nil, fmt.Errorf("failed to fetch PopOK games: %w", err) - } - - for _, g := range popokGames { - unified := domain.UnifiedGame{ - GameID: fmt.Sprintf("%d", g.ID), - ProviderID: "popok", - Provider: "PopOK", - Name: g.GameName, - Category: "Crash", - Bets: g.Bets, - Thumbnail: g.Thumbnail, - Status: g.Status, - } - allGames = append(allGames, unified) - - // Convert []float64 to []pgtype.Numeric - var betsNumeric []pgtype.Numeric - for _, bet := range g.Bets { - var num pgtype.Numeric - _ = num.Scan(bet) - betsNumeric = append(betsNumeric, num) - } - - // Save to DB - _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ - GameID: fmt.Sprintf("%d", g.ID), - ProviderID: "popok", - Name: g.GameName, - Bets: betsNumeric, - Thumbnail: pgtype.Text{ - String: g.Thumbnail, - Valid: g.Thumbnail != "", - }, - Status: pgtype.Int4{ - Int32: int32(g.Status), - Valid: true, - }, - HasDemo: pgtype.Bool{ - Bool: true, - Valid: true, - }, - Category: pgtype.Text{ - String: "Crash", - Valid: true, - }, - - }) - if err != nil { - logger.Error("failed to create PopOK virtual game", zap.Error(err)) - } - } - - return allGames, nil -} diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index fb2ece4..d344896 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -16,6 +16,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "github.com/google/uuid" "go.uber.org/zap" ) @@ -112,7 +113,7 @@ func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest) } var res domain.ProviderResponse - err := s.client.post(ctx, "/game-lists/public/providers", req, sigParams, &res) + err := s.client.Post(ctx, "/game-lists/public/providers", req, sigParams, &res) return &res, err } @@ -140,7 +141,7 @@ func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]d var res struct { Items []domain.GameEntity `json:"items"` } - if err := s.client.post(ctx, "/game-lists/public/games", req, sigParams, &res); err != nil { + if err := s.client.Post(ctx, "/game-lists/public/games", req, sigParams, &res); err != nil { return nil, fmt.Errorf("failed to fetch games for provider %s: %w", req.ProviderID, err) } @@ -168,17 +169,32 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (* "playerId": req.PlayerID, "currency": req.Currency, "deviceType": req.DeviceType, - "country": "US", + "country": req.Country, "ip": req.IP, "brandId": req.BrandID, } // 3. Call external API var res domain.GameStartResponse - if err := s.client.post(ctx, "/unified-api/public/start-game", req, sigParams, &res); err != nil { + if err := s.client.Post(ctx, "/unified-api/public/start-game", req, sigParams, &res); err != nil { return nil, fmt.Errorf("failed to start game with provider %s: %w", req.ProviderID, err) } + playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid PlayerID: %w", err) + } + + session := &domain.VirtualGameSession{ + UserID: playerIDInt64, + GameID: req.GameID, + SessionToken: uuid.NewString(), + } + + if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil { + return nil, fmt.Errorf("failed to create virtual game session: %w", err) + } + return &res, nil } @@ -206,7 +222,7 @@ func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) // 3. Call external API var res domain.GameStartResponse - if err := s.client.post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res); err != nil { + if err := s.client.Post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res); err != nil { return nil, fmt.Errorf("failed to start demo game with provider %s: %w", req.ProviderID, err) } @@ -219,23 +235,24 @@ func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*d if err != nil { return nil, fmt.Errorf("invalid PlayerID: %w", err) } - playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + // playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + // if err != nil { + // return nil, fmt.Errorf("failed to get real balance: %w", err) + // } + // if len(playerWallets) == 0 { + // return nil, fmt.Errorf("PLAYER_NOT_FOUND: no wallet found for player %s", req.PlayerID) + // } + + wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64) if err != nil { - return nil, fmt.Errorf("failed to get real balance: %w", err) - } - if len(playerWallets) == 0 { - return nil, fmt.Errorf("PLAYER_NOT_FOUND: no wallet found for player %s", req.PlayerID) + return nil, fmt.Errorf("failed to read user wallets") } - realBalance := playerWallets[0].Balance + // realBalance := playerWallets[0].Balance // Retrieve bonus balance if applicable - var bonusBalance float64 - if len(playerWallets) > 1 { - bonusBalance = float64(playerWallets[1].Balance) - } else { - bonusBalance = 0 - } + // var bonusBalance float64 + // bonusBalance := float64(wallet.StaticBalance) // Build the response res := &domain.BalanceResponse{ @@ -244,19 +261,19 @@ func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*d Amount float64 `json:"amount"` }{ Currency: req.Currency, - Amount: float64(realBalance), + Amount: float64(wallet.RegularBalance), }, } - if bonusBalance > 0 { - res.Bonus = &struct { - Currency string `json:"currency"` - Amount float64 `json:"amount"` - }{ - Currency: req.Currency, - Amount: bonusBalance, - } - } + // if bonusBalance > 0 { + // res.Bonus = &struct { + // Currency string `json:"currency"` + // Amount float64 `json:"amount"` + // }{ + // Currency: req.Currency, + // Amount: bonusBalance, + // } + // } return res, nil } @@ -281,91 +298,64 @@ func (s *Service) ProcessBet(ctx context.Context, req domain.BetRequest) (*domai // } // --- 3. Get player wallets --- - playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + // playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + // if err != nil { + // return nil, fmt.Errorf("failed to get real balance: %w", err) + // } + // if len(playerWallets) == 0 { + // return nil, fmt.Errorf("no wallets found for player %s", req.PlayerID) + // } + + wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64) if err != nil { - return nil, fmt.Errorf("failed to get real balance: %w", err) - } - if len(playerWallets) == 0 { - return nil, fmt.Errorf("no wallets found for player %s", req.PlayerID) + return nil, fmt.Errorf("failed to read user wallets") } - realWallet := playerWallets[0] - realBalance := float64(realWallet.Balance) + // realWallet := playerWallets[0] + // realBalance := float64(realWallet.Balance) - var bonusBalance float64 - if len(playerWallets) > 1 { - bonusBalance = float64(playerWallets[1].Balance) - } + // var bonusBalance float64 + // if len(playerWallets) > 1 { + // bonusBalance = float64(playerWallets[1].Balance) + // } + + bonusBalance := float64(wallet.StaticBalance) // --- 4. Check sufficient balance --- - totalBalance := realBalance + bonusBalance - if totalBalance < req.Amount.Amount { + // totalBalance := float64(wallet.RegularBalance) + bonusBalance + if float64(wallet.RegularBalance) < req.Amount.Amount { return nil, fmt.Errorf("INSUFFICIENT_BALANCE") } // --- 5. Deduct funds (bonus first, then real) --- remaining := req.Amount.Amount - var usedBonus, usedReal float64 + // var usedBonus, usedReal float64 - if bonusBalance > 0 { - if bonusBalance >= remaining { - // fully cover from bonus - usedBonus = remaining - bonusBalance -= remaining - remaining = 0 - } else { - // partially cover from bonus - usedBonus = bonusBalance - remaining -= bonusBalance - bonusBalance = 0 - } - } - - if remaining > 0 { - if realBalance >= remaining { - usedReal = remaining - realBalance -= remaining - remaining = 0 - } else { - // should never happen because of totalBalance check - return nil, fmt.Errorf("INSUFFICIENT_BALANCE") - } + if remaining > float64(wallet.RegularBalance) { + return nil, fmt.Errorf("INSUFFICIENT_BALANCE") } // --- 6. Persist wallet deductions --- - if usedBonus > 0 && len(playerWallets) > 1 { - _, err = s.walletSvc.DeductFromWallet(ctx, playerWallets[1].ID, - domain.Currency(usedBonus), - domain.ValidInt64{}, - domain.TRANSFER_DIRECT, - fmt.Sprintf("Deduct bonus %.2f for bet %s", usedBonus, req.TransactionID), - ) - if err != nil { - return nil, fmt.Errorf("bonus deduction failed: %w", err) - } - } - if usedReal > 0 { - _, err = s.walletSvc.DeductFromWallet(ctx, realWallet.ID, - domain.Currency(usedReal), - domain.ValidInt64{}, - domain.TRANSFER_DIRECT, - fmt.Sprintf("Deduct real %.2f for bet %s", usedReal, req.TransactionID), - ) - if err != nil { - return nil, fmt.Errorf("real deduction failed: %w", err) - } + _, err = s.walletSvc.DeductFromWallet(ctx, wallet.RegularID, + domain.Currency(req.Amount.Amount), + domain.ValidInt64{}, + domain.TRANSFER_DIRECT, + fmt.Sprintf("Deduct amount %.2f for bet %s", req.Amount.Amount, req.TransactionID), + ) + if err != nil { + return nil, fmt.Errorf("bonus deduction failed: %w", err) } // --- 7. Build response --- res := &domain.BetResponse{ Real: domain.BalanceDetail{ Currency: "ETB", - Amount: realBalance, + Amount: float64(wallet.RegularBalance), }, WalletTransactionID: req.TransactionID, - UsedRealAmount: usedReal, - UsedBonusAmount: usedBonus, + UsedRealAmount: req.Amount.Amount, + UsedBonusAmount: 0, } if bonusBalance > 0 { @@ -386,21 +376,19 @@ func (s *Service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domai } // --- 2. Get player wallets --- - playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64) if err != nil { - return nil, fmt.Errorf("failed to get wallets: %w", err) - } - if len(playerWallets) == 0 { - return nil, fmt.Errorf("PLAYER_NOT_FOUND: no wallets for player %s", req.PlayerID) + return nil, fmt.Errorf("failed to read user wallets") } - realWallet := playerWallets[0] - realBalance := float64(realWallet.Balance) + // realWallet := playerWallets[0] + realBalance := float64(wallet.RegularBalance) - var bonusBalance float64 - if len(playerWallets) > 1 { - bonusBalance = float64(playerWallets[1].Balance) - } + // var bonusBalance float64 + // if len(playerWallets) > 1 { + // bonusBalance = float64(playerWallets[1].Balance) + // } + bonusBalance := float64(wallet.StaticBalance) // --- 3. Apply winnings (for now, everything goes to real wallet) --- winAmount := req.Amount.Amount @@ -412,7 +400,7 @@ func (s *Service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domai _, err = s.walletSvc.AddToWallet( ctx, - realWallet.ID, + wallet.RegularID, domain.Currency(winAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, @@ -423,18 +411,18 @@ func (s *Service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domai return nil, fmt.Errorf("failed to credit real wallet: %w", err) } - // --- 4. Reload balances after credit --- - updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) - if err != nil { - return nil, fmt.Errorf("failed to reload balances: %w", err) - } + // // --- 4. Reload balances after credit --- + // updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + // if err != nil { + // return nil, fmt.Errorf("failed to reload balances: %w", err) + // } - updatedReal := updatedWallets[0] - realBalance = float64(updatedReal.Balance) + // updatedReal := updatedWallets[0] + // realBalance = float64(wallet.RegularBalance) - if len(updatedWallets) > 1 { - bonusBalance = float64(updatedWallets[1].Balance) - } + // if len(updatedWallets) > 1 { + // bonusBalance = float64(updatedWallets[1].Balance) + // } // --- 5. Build response --- res := &domain.WinResponse{ @@ -465,21 +453,18 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( } // --- 2. Get player wallets --- - playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64) if err != nil { - return nil, fmt.Errorf("failed to get wallets: %w", err) - } - if len(playerWallets) == 0 { - return nil, fmt.Errorf("no wallets for player %s", req.PlayerID) + return nil, fmt.Errorf("failed to read user wallets") } - realWallet := playerWallets[0] - realBalance := float64(realWallet.Balance) + // realWallet := playerWallets[0] + realBalance := float64(wallet.RegularBalance) - var bonusBalance float64 - if len(playerWallets) > 1 { - bonusBalance = float64(playerWallets[1].Balance) - } + // var bonusBalance float64 + // if len(playerWallets) > 1 { + bonusBalance := float64(wallet.StaticBalance) + // } // --- 3. Determine refund amount based on IsAdjustment --- var refundAmount float64 @@ -503,7 +488,7 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( _, err = s.walletSvc.AddToWallet( ctx, - realWallet.ID, + wallet.RegularID, domain.Currency(refundAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, @@ -521,23 +506,23 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( } // --- 5. Reload balances after refund --- - updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) - if err != nil { - return nil, fmt.Errorf("failed to reload balances: %w", err) - } + // updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) + // if err != nil { + // return nil, fmt.Errorf("failed to reload balances: %w", err) + // } - updatedReal := updatedWallets[0] - realBalance = float64(updatedReal.Balance) + // updatedReal := updatedWallets[0] + // realBalance = float64(wallet.RegularBalance) - if len(updatedWallets) > 1 { - bonusBalance = float64(updatedWallets[1].Balance) - } + // if len(updatedWallets) > 1 { + // bonusBalance = float64(updatedWallets[1].Balance) + // } // --- 6. Build response --- res := &domain.CancelResponse{ WalletTransactionID: req.TransactionID, Real: domain.BalanceDetail{ - Currency: "ETB", + Currency: req.AdjustmentRefund.Currency, Amount: realBalance, }, UsedRealAmount: usedReal, @@ -546,7 +531,7 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( if bonusBalance > 0 { res.Bonus = &domain.BalanceDetail{ - Currency: "ETB", + Currency: req.AdjustmentRefund.Currency, Amount: bonusBalance, } } @@ -554,12 +539,6 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( return res, nil } -// Example helper to fetch original bet -// func (s *Service) getOriginalBet(ctx context.Context, transactionID string) (*domain.BetRecord, error) { -// // TODO: implement actual lookup -// return &domain.BetRecord{Amount: 50}, nil -// } - func (s *Service) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) { // --- Signature Params (flattened strings for signing) --- sigParams := map[string]any{ @@ -599,7 +578,7 @@ func (s *Service) GetGamingActivity(ctx context.Context, req domain.GamingActivi // --- Actual API Call --- var res domain.GamingActivityResponse - err := s.client.post(ctx, "/report-api/public/gaming-activity", req, sigParams, &res) + err := s.client.Post(ctx, "/report-api/public/gaming-activity", req, sigParams, &res) if err != nil { return nil, err } @@ -640,7 +619,7 @@ func (s *Service) GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) ( // --- Actual API Call --- var res domain.HugeWinsResponse - err := s.client.post(ctx, "/report-api/public/gaming-activity/huge-wins", req, sigParams, &res) + err := s.client.Post(ctx, "/report-api/public/gaming-activity/huge-wins", req, sigParams, &res) if err != nil { return nil, err } @@ -663,7 +642,7 @@ func (s *Service) GetCreditBalances(ctx context.Context, brandID string) ([]doma Credits []domain.CreditBalance `json:"credits"` } - if err := s.client.post(ctx, "/report-api/public/credit/balances", body, nil, &res); err != nil { + if err := s.client.Post(ctx, "/report-api/public/credit/balances", body, nil, &res); err != nil { return nil, fmt.Errorf("failed to fetch credit balances: %w", err) } diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 776f580..288ffde 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -34,6 +34,7 @@ import ( virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/orchestration" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" @@ -49,7 +50,8 @@ import ( type App struct { enetPulseSvc *enetpulse.Service atlasVirtualGameService atlas.AtlasVirtualGameService - veliVirtualGameService veli.VeliVirtualGameService + veliVirtualGameService *veli.Service + orchestrationSvc *orchestration.Service telebirrSvc *telebirr.TelebirrService arifpaySvc *arifpay.ArifpayService santimpaySvc *santimpay.SantimPayService @@ -92,7 +94,8 @@ type App struct { func NewApp( enetPulseSvc *enetpulse.Service, atlasVirtualGameService atlas.AtlasVirtualGameService, - veliVirtualGameService veli.VeliVirtualGameService, + veliVirtualGameService *veli.Service, + orchestrationSvc *orchestration.Service, telebirrSvc *telebirr.TelebirrService, arifpaySvc *arifpay.ArifpayService, santimpaySvc *santimpay.SantimPayService, @@ -149,6 +152,7 @@ func NewApp( enetPulseSvc: enetPulseSvc, atlasVirtualGameService: atlasVirtualGameService, veliVirtualGameService: veliVirtualGameService, + orchestrationSvc: orchestrationSvc, telebirrSvc: telebirrSvc, arifpaySvc: arifpaySvc, santimpaySvc: santimpaySvc, diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index e5b9cfc..2969daa 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -20,7 +20,7 @@ import ( resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/stats" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/orchestration" "github.com/robfig/cron/v3" "go.uber.org/zap" ) @@ -347,21 +347,18 @@ func StartReportCrons(reportService report.ReportService, mongoLogger *zap.Logge func SetupReportandVirtualGameCronJobs( ctx context.Context, reportService report.ReportService, - virtualGameService *veli.Service, // inject your virtual game service + virtualGameOrchestrationService *orchestration.Service, outputDir string, ) { - c := cron.New(cron.WithSeconds()) // use WithSeconds for testing + c := cron.New(cron.WithSeconds()) // WithSeconds for testing, remove in prod schedule := []struct { spec string period string }{ - // { - // spec: "*/60 * * * * *", // Every 1 minute for testing - // period: "test", - // }, + // { spec: "*/60 * * * * *", period: "test" }, // every 60 seconds for testing { - spec: "0 0 0 * * *", // Daily at midnight + spec: "0 0 0 * * *", // daily at midnight period: "daily", }, } @@ -370,7 +367,7 @@ func SetupReportandVirtualGameCronJobs( period := job.period if _, err := c.AddFunc(job.spec, func() { - log.Printf("[%s] Running virtual game fetch & store job...", period) + log.Printf("[%s] Running virtual game & provider report job...", period) brandID := os.Getenv("VELI_BRAND_ID") if brandID == "" { @@ -378,6 +375,7 @@ func SetupReportandVirtualGameCronJobs( return } + // Step 1. Fetch and store all virtual games req := domain.ProviderRequest{ BrandID: brandID, ExtraData: true, @@ -385,7 +383,7 @@ func SetupReportandVirtualGameCronJobs( Page: 1, } - allGames, err := virtualGameService.FetchAndStoreAllVirtualGames(ctx, req, "ETB") + allGames, err := virtualGameOrchestrationService.FetchAndStoreAllVirtualGames(ctx, req, "ETB") if err != nil { log.Printf("[%s] Error fetching/storing virtual games: %v", period, err) return @@ -393,19 +391,42 @@ func SetupReportandVirtualGameCronJobs( log.Printf("[%s] Successfully fetched & stored %d virtual games", period, len(allGames)) - // --- Generate reports only for daily runs --- - // if period == "daily" { - // now := time.Now() - // from := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()) - // to := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()) + // Step 2. Fetch all providers + providers, total, err := virtualGameOrchestrationService.ListProviders(ctx, 1000, 0) + if err != nil { + log.Printf("[%s] Failed to list providers: %v", period, err) + return + } else if total == 0 { + log.Printf("[%s] No providers found, skipping report generation", period) + return + } + + log.Printf("[%s] Found %d total providers", period, total) + + // Step 3. Create provider-level daily report entries + reportDate := time.Now().UTC().Truncate(24 * time.Hour) + for _, p := range providers { + createReq := domain.CreateVirtualGameProviderReport{ + ProviderID: p.ProviderID, + ReportDate: reportDate, + TotalGamesPlayed: 0, + TotalBets: 0, + TotalPayouts: 0, + TotalPlayers: 0, + ReportType: period, // "daily" + } + + _, err := virtualGameOrchestrationService.CreateVirtualGameProviderReport(ctx, createReq) + if err != nil { + log.Printf("[%s] Failed to create report for provider %s: %v", period, p.ProviderID, err) + continue + } + + log.Printf("[%s] Created daily report row for provider: %s", period, p.ProviderID) + } + + log.Printf("[%s] Daily provider reports created successfully", period) - // log.Printf("Running daily report for period %s -> %s", from.Format(time.RFC3339), to.Format(time.RFC3339)) - // if err := reportService.GenerateReport(ctx, from, to); err != nil { - // log.Printf("Error generating daily report: %v", err) - // } else { - // log.Printf("Successfully generated daily report") - // } - // } }); err != nil { log.Fatalf("Failed to schedule %s cron job: %v", period, err) } @@ -453,60 +474,89 @@ func StartEnetPulseCron(enetPulseSvc *enetpulse.Service, mongoLogger *zap.Logger task func() }{ { - spec: "0 0,10,20,30,40,50 * * * *", // Every 10 minutes + spec: "0 0 */2 * * *", // Every 2 hours task: func() { + ctx := context.Background() + + // 1️⃣ Sports mongoLogger.Info("Began fetching and storing sports cron task") - if err := enetPulseSvc.FetchAndStoreSports(context.Background()); err != nil { - mongoLogger.Error("Failed to fetch and store sports", - zap.Error(err), - ) + if err := enetPulseSvc.FetchAndStoreSports(ctx); err != nil { + mongoLogger.Error("Failed to fetch and store sports", zap.Error(err)) } else { - mongoLogger.Info("Completed fetching and storing sports without errors") + mongoLogger.Info("\n\n✅ Completed fetching and storing sports\n\n") } + // 2️⃣ Tournament Templates mongoLogger.Info("Began fetching and storing tournament templates cron task") - if err := enetPulseSvc.FetchAndStoreTournamentTemplates(context.Background()); err != nil { - mongoLogger.Error("Failed to fetch and store tournament templates", - zap.Error(err), - ) + if err := enetPulseSvc.FetchAndStoreTournamentTemplates(ctx); err != nil { + mongoLogger.Error("Failed to fetch and store tournament templates", zap.Error(err)) } else { - mongoLogger.Info("Completed fetching and storing tournament templates without errors") + mongoLogger.Info("\n\n✅ Completed fetching and storing tournament templates\n\n") } + // 3️⃣ Tournaments mongoLogger.Info("Began fetching and storing tournaments cron task") - if err := enetPulseSvc.FetchAndStoreTournaments(context.Background()); err != nil { - mongoLogger.Error("Failed to fetch and store tournaments", - zap.Error(err), - ) + if err := enetPulseSvc.FetchAndStoreTournaments(ctx); err != nil { + mongoLogger.Error("Failed to fetch and store tournaments", zap.Error(err)) } else { - mongoLogger.Info("Completed fetching and storing tournaments without errors") + mongoLogger.Info("\n\n✅ Completed fetching and storing tournaments\n\n") } - mongoLogger.Info("Began fetching and storing tournament stages cron task") - if err := enetPulseSvc.FetchAndStoreTournamentStages(context.Background()); err != nil { - mongoLogger.Error("Failed to fetch and store tournament stages", - zap.Error(err), - ) + // 4️⃣ Tournament Stages + // mongoLogger.Info("Began fetching and storing tournament stages cron task") + // if err := enetPulseSvc.FetchAndStoreTournamentStages(ctx); err != nil { + // mongoLogger.Error("Failed to fetch and store tournament stages", zap.Error(err)) + // } else { + // mongoLogger.Info("✅ \n\nCompleted fetching and storing tournament stages\n\n") + // } + + // // 5️⃣ Fixtures + mongoLogger.Info("Began fetching and storing fixtures cron task") + today := time.Now().Format("2006-01-02") + if err := enetPulseSvc.FetchAndStoreFixtures(ctx, today); err != nil { + mongoLogger.Error("Failed to fetch and store fixtures", zap.Error(err)) } else { - mongoLogger.Info("Completed fetching and storing tournament stages without errors") + mongoLogger.Info("\n\n✅ Completed fetching and storing fixtures\n\n") + } + + // 6️⃣ Results + // mongoLogger.Info("Began fetching and storing results cron task") + // if err := enetPulseSvc.FetchAndStoreResults(ctx); err != nil { + // mongoLogger.Error("Failed to fetch and store results", zap.Error(err)) + // } else { + // mongoLogger.Info("\n\n✅ Completed fetching and storing results\n\n") + // } + + // 7 Outcome Types + mongoLogger.Info("Began fetching and storing outcome_types cron task") + if err := enetPulseSvc.FetchAndStoreOutcomeTypes(ctx); err != nil { + mongoLogger.Error("Failed to fetch and store outcome_types", zap.Error(err)) + } else { + mongoLogger.Info("\n\n✅ Completed fetching and storing outcome_types\n\n") + } + + // 8 Preodds + mongoLogger.Info("Began fetching and storing preodds cron task") + if err := enetPulseSvc.FetchAndStorePreodds(ctx); err != nil { + mongoLogger.Error("Failed to fetch and store preodds", zap.Error(err)) + } else { + mongoLogger.Info("\n\n✅ Completed fetching and storing preodds\n\n") } }, }, } for _, job := range schedule { - // Run the task immediately at startup + // Run immediately at startup job.task() // Schedule the task if _, err := c.AddFunc(job.spec, job.task); err != nil { - mongoLogger.Error("Failed to schedule EnetPulse cron job", - zap.Error(err), - ) + mongoLogger.Error("Failed to schedule EnetPulse cron job", zap.Error(err)) } } c.Start() - log.Println("EnetPulse cron jobs started for sports, tournament templates, tournaments, and tournament stages") - mongoLogger.Info("EnetPulse cron jobs started for sports, tournament templates, tournaments, and tournament stages") + log.Println("EnetPulse cron jobs started for sports, tournament templates, tournaments, tournament stages, fixtures, and results") + mongoLogger.Info("EnetPulse cron jobs started for sports, tournament templates, tournaments, tournament stages, fixtures, and results") } diff --git a/internal/web_server/handlers/arifpay.go b/internal/web_server/handlers/arifpay.go index c61d24e..51e248f 100644 --- a/internal/web_server/handlers/arifpay.go +++ b/internal/web_server/handlers/arifpay.go @@ -18,6 +18,15 @@ import ( // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/arifpay/checkout [post] func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error { + + userId, ok := c.Locals("user_id").(int64) + if !ok { + return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ + Error: "missing user id", + Message: "Unauthorized", + }) + } + var req domain.CheckoutSessionClientRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ @@ -26,7 +35,7 @@ func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error { }) } - data, err := h.arifpaySvc.CreateCheckoutSession(req, true) + data, err := h.arifpaySvc.CreateCheckoutSession(req, true, userId) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -53,7 +62,7 @@ func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error { // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/arifpay/checkout/{sessionId}/cancel [post] +// @Router /api/v1/arifpay/checkout/cancel/{sessionId} [post] func (h *Handler) CancelCheckoutSessionHandler(c *fiber.Ctx) error { sessionID := c.Params("sessionId") if sessionID == "" { @@ -103,15 +112,15 @@ func (h *Handler) HandleArifpayC2BWebhook(c *fiber.Ctx) error { // 🚨 Decide how to get userId: // If you get it from auth context/middleware, extract it here. // For now, let's assume userId comes from your auth claims: - userId, ok := c.Locals("user_id").(int64) - if !ok { - return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ - Error: "missing user id", - Message: "Unauthorized", - }) - } + // userId, ok := c.Locals("user_id").(int64) + // if !ok { + // return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ + // Error: "missing user id", + // Message: "Unauthorized", + // }) + // } - err := h.arifpaySvc.HandleWebhook(c.Context(), req, userId, true) + err := h.arifpaySvc.ProcessWebhook(c.Context(), req, true) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -150,15 +159,15 @@ func (h *Handler) HandleArifpayB2CWebhook(c *fiber.Ctx) error { // 🚨 Decide how to get userId: // If you get it from auth context/middleware, extract it here. // For now, let's assume userId comes from your auth claims: - userId, ok := c.Locals("user_id").(int64) - if !ok { - return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ - Error: "missing user id", - Message: "Unauthorized", - }) - } + // userId, ok := c.Locals("user_id").(int64) + // if !ok { + // return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ + // Error: "missing user id", + // Message: "Unauthorized", + // }) + // } - err := h.arifpaySvc.HandleWebhook(c.Context(), req, userId, false) + err := h.arifpaySvc.ProcessWebhook(c.Context(), req, false) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Error: err.Error(), @@ -253,7 +262,7 @@ func (h *Handler) ArifpayVerifyBySessionIDHandler(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param type query string true "Transfer type (telebirr, cbe, mpesa)" -// @Param request body domain.ArifpayB2CRequest true "Transfer request payload" +// @Param request body domain.CheckoutSessionClientRequest true "Transfer request payload" // @Success 200 {object} map[string]string "message: transfer executed successfully" // @Failure 400 {object} map[string]string "error: invalid request or unsupported transfer type" // @Failure 500 {object} map[string]string "error: internal server error" @@ -275,7 +284,7 @@ func (h *Handler) ExecuteArifpayB2CTransfer(c *fiber.Ctx) error { }) } - var req domain.ArifpayB2CRequest + var req domain.CheckoutSessionClientRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to process your withdrawal request", diff --git a/internal/web_server/handlers/atlas.go b/internal/web_server/handlers/atlas.go index 9690578..1b44b69 100644 --- a/internal/web_server/handlers/atlas.go +++ b/internal/web_server/handlers/atlas.go @@ -7,9 +7,11 @@ import ( "fmt" "log" "strings" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" ) // GetAtlasVGames godoc @@ -52,7 +54,7 @@ func (h *Handler) GetAtlasVGames(c *fiber.Ctx) error { // @Failure 502 {object} domain.ErrorResponse // @Router /api/v1/atlas/init-game [post] func (h *Handler) InitAtlasGame(c *fiber.Ctx) error { - // Retrieve user ID from context + // 1️⃣ Retrieve user ID from context userId, ok := c.Locals("user_id").(int64) if !ok { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ @@ -61,6 +63,7 @@ func (h *Handler) InitAtlasGame(c *fiber.Ctx) error { }) } + // 2️⃣ Parse request body var req domain.AtlasGameInitRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ @@ -69,30 +72,53 @@ func (h *Handler) InitAtlasGame(c *fiber.Ctx) error { }) } - // Attach user ID to request + // 3️⃣ Attach user ID to request req.PlayerID = fmt.Sprintf("%d", userId) - // Default language if not provided + // 4️⃣ Set defaults if not provided if req.Language == "" { req.Language = "en" } - - // Default currency if not provided if req.Currency == "" { req.Currency = "USD" } - // Call the service + // 5️⃣ Call the Atlas service res, err := h.atlasVirtualGameSvc.InitGame(context.Background(), req) if err != nil { log.Println("InitAtlasGame error:", err) - return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to initialize Atlas game", Error: err.Error(), }) } + // 6️⃣ Update provider report: increment total_games_played + go func() { + ctx := context.Background() + reportDate := time.Now().Truncate(24 * time.Hour) + reportType := "daily" + providerID := "atlas" // all Atlas games belong to this provider + + err := h.orchestrationSvc.UpdateVirtualGameProviderReportByDate( + ctx, + providerID, + reportDate, + reportType, + 1, // increment total_games_played by 1 + 0, // total_bets (no change) + 0, // total_payouts (no change) + 1, // total_players (no change) + ) + if err != nil { + h.InternalServerErrorLogger().Error("Failed to update total_games_played for Atlas game", + zap.String("provider_id", providerID), + zap.Error(err), + ) + } + }() + + // 7️⃣ Return response to user return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Game initialized successfully", Data: res, diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 9a2a7c2..4af65bc 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -1,7 +1,9 @@ package handlers import ( + "encoding/json" "fmt" + "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" @@ -39,7 +41,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { }) } - amount := domain.Currency(req.Amount * 100) + amount := domain.Currency(req.Amount) fmt.Println("We are here init Chapa payment") @@ -51,40 +53,6 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { }) } - // get static wallet of user - // wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID) - // if err != nil { - // return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - // Error: err.Error(), - // Message: "Failed to initiate Chapa deposit", - // }) - // } - - // var multiplier float32 = 1 - // bonusMultiplier, err := h.bonusSvc.GetBonusMultiplier(c.Context()) - // if err == nil { - // multiplier = bonusMultiplier[0].Multiplier - // } - - // var balanceCap int64 = 0 - // bonusBalanceCap, err := h.bonusSvc.GetBonusBalanceCap(c.Context()) - // if err == nil { - // balanceCap = bonusBalanceCap[0].BalanceCap - // } - - // capedBalanceAmount := domain.Currency((math.Min(req.Amount, float64(balanceCap)) * float64(multiplier)) * 100) - - // _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, capedBalanceAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - // fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", capedBalanceAmount, multiplier), - // ) - // if err != nil { - // h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err) - // return err - // } - - // if err := h.bonusSvc.ProcessWelcomeBonus(c.Context(), domain.ToCurrency(float32(req.Amount)), 0, userID); err != nil { - // return err - // } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Chapa deposit process initiated successfully", Data: checkoutURL, @@ -95,69 +63,180 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { // WebhookCallback godoc // @Summary Chapa payment webhook callback (used by Chapa) -// @Description Handles payment notifications from Chapa +// @Description Handles payment and transfer notifications from Chapa // @Tags Chapa // @Accept json // @Produce json -// @Param request body domain.ChapaWebhookPayload true "Webhook payload" -// @Success 200 {object} map[string]interface{} +// @Param request body domain.ChapaWebhookPayment true "Webhook payload" +// @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/chapa/payments/webhook/verify [post] func (h *Handler) WebhookCallback(c *fiber.Ctx) error { + body := c.Body() - chapaTransactionType := new(domain.ChapaTransactionType) + // Retrieve signature headers + chapaSignature := c.Get("chapa-signature") + xChapaSignature := c.Get("x-chapa-signature") - if parseTypeErr := c.BodyParser(chapaTransactionType); parseTypeErr != nil { - return domain.UnProcessableEntityResponse(c) + // Verify webhook signature + valid, err := h.chapaSvc.VerifyWebhookSignature(c.Context(), body, chapaSignature, xChapaSignature) + if err != nil || !valid { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid Chapa webhook signature", + Error: err.Error(), + }) } - switch chapaTransactionType.Type { - case h.Cfg.CHAPA_PAYMENT_TYPE: - chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer) + // Try parsing as transfer webhook first + var transfer domain.ChapaWebhookTransfer + if err := json.Unmarshal(body, &transfer); err == nil && + strings.EqualFold(transfer.Type, "payout") { - if err := c.BodyParser(chapaTransferVerificationRequest); err != nil { - return domain.UnProcessableEntityResponse(c) - } - - err := h.chapaSvc.HandleVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to verify Chapa depposit", - Error: err.Error(), - }) - } - - return c.Status(fiber.StatusOK).JSON(domain.Response{ - StatusCode: 200, - Message: "Chapa deposit transaction verified successfully", - Data: chapaTransferVerificationRequest, - Success: true, - }) - case h.Cfg.CHAPA_TRANSFER_TYPE: - chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment) - if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil { - return domain.UnProcessableEntityResponse(c) - } - - err := h.chapaSvc.HandleVerifyWithdrawWebhook(c.Context(), *chapaPaymentVerificationRequest) - if err != nil { + if err := h.chapaSvc.ProcessVerifyWithdrawWebhook(c.Context(), transfer); err != nil { return domain.UnExpectedErrorResponse(c) } return c.Status(fiber.StatusOK).JSON(domain.Response{ StatusCode: 200, - Message: "Chapa withdrawal transaction verified successfully", - Data: chapaPaymentVerificationRequest, + Message: "Chapa withdrawal webhook processed successfully", + // Data: transfer, Success: true, }) - } - // Return a 400 Bad Request if the type does not match any known case - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Message: "Invalid Chapa webhook type", - Error: "Unknown transaction type", + // Otherwise, try as payment webhook + var payment domain.ChapaWebhookPayment + if err := json.Unmarshal(body, &payment); err != nil { + return domain.UnProcessableEntityResponse(c) + } + + if err := h.chapaSvc.ProcessVerifyDepositWebhook(c.Context(), payment); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to verify Chapa deposit", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + StatusCode: 200, + Message: "Chapa deposit webhook processed successfully", + // Data: payment, + Success: true, + }) +} + +// CancelDeposit godoc +// @Summary Cancel a Chapa deposit transaction +// @Description Cancels an active Chapa transaction using its transaction reference +// @Tags Chapa +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param tx_ref path string true "Transaction Reference" +// @Success 200 {object} domain.ChapaCancelResponse +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/chapa/transaction/cancel/{tx_ref} [put] +func (h *Handler) CancelDeposit(c *fiber.Ctx) error { + // Get user ID from context (set by your auth middleware) + userID, ok := c.Locals("user_id").(int64) + if !ok { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: "invalid user ID", + Message: "User ID is required to cancel a deposit", + }) + } + + // Extract tx_ref from URL path + txRef := c.Params("tx_ref") + if txRef == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Error: "missing transaction reference", + Message: "Transaction reference is required in the path", + }) + } + + fmt.Printf("\n\nReceived request to cancel Chapa transaction: %s (User ID: %d)\n\n", txRef, userID) + + // Call the service layer to cancel deposit + cancelResp, err := h.chapaSvc.CancelDeposit(c.Context(), userID, txRef) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to cancel Chapa deposit", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Chapa transaction cancelled successfully", + Data: cancelResp, + StatusCode: 200, + Success: true, + }) +} + +// FetchAllTransactions godoc +// @Summary Get all Chapa transactions +// @Description Retrieves all transactions from Chapa payment gateway +// @Tags Chapa +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Success 200 {array} domain.ChapaTransaction +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/chapa/transactions [get] +func (h *Handler) FetchAllTransactions(c *fiber.Ctx) error { + transactions, err := h.chapaSvc.FetchAllTransactions(c.Context()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to fetch Chapa transactions", + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Chapa transactions retrieved successfully", + Data: transactions, + StatusCode: 200, + Success: true, + }) +} + +// GetTransactionEvents godoc +// @Summary Fetch transaction events +// @Description Retrieve the timeline of events for a specific Chapa transaction +// @Tags Chapa +// @Accept json +// @Produce json +// @Param ref_id path string true "Transaction Reference" +// @Success 200 {array} domain.ChapaTransactionEvent +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/chapa/transaction/events/{ref_id} [get] +func (h *Handler) GetTransactionEvents(c *fiber.Ctx) error { + refID := c.Params("ref_id") + if refID == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to fetch transaction events", + Error: "Transaction reference is required", + }) + } + + events, err := h.chapaSvc.FetchTransactionEvents(c.Context(), refID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to fetch transaction events", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Transaction events fetched successfully", + Data: events, + StatusCode: 200, + Success: true, }) } @@ -168,10 +247,10 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param tx_ref path string true "Transaction Reference" -// @Success 200 {object} domain.ChapaVerificationResponse +// @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/chapa/payments/manual/verify/{tx_ref} [get] +// @Router /api/v1/chapa/transaction/manual/verify/{tx_ref} [get] func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error { txRef := c.Params("tx_ref") if txRef == "" { @@ -189,11 +268,11 @@ func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error { }) } - return c.Status(fiber.StatusOK).JSON(domain.ChapaVerificationResponse{ - Status: string(verification.Status), - Amount: verification.Amount, - Currency: verification.Currency, - TxRef: txRef, + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Chapa transaction verified successfully", + Data: verification, + StatusCode: 200, + Success: true, }) } @@ -231,7 +310,7 @@ func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error { // @Produce json // @Security ApiKeyAuth // @Param request body domain.ChapaWithdrawalRequest true "Withdrawal request details" -// @Success 201 {object} domain.Response "Chapa withdrawal process initiated successfully" +// @Success 200 {object} domain.Response "Chapa withdrawal process initiated successfully" // @Failure 400 {object} domain.ErrorResponse "Invalid request body" // @Failure 401 {object} domain.ErrorResponse "Unauthorized" // @Failure 422 {object} domain.ErrorResponse "Unprocessable entity" @@ -256,10 +335,151 @@ func (h *Handler) InitiateWithdrawal(c *fiber.Ctx) error { }) } - return c.Status(fiber.StatusCreated).JSON(domain.Response{ + return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Chapa withdrawal process initiated successfully", - StatusCode: 201, + StatusCode: 200, Success: true, Data: withdrawal, }) } + +// GetPaymentReceipt godoc +// @Summary Get Chapa Payment Receipt URL +// @Description Retrieve the Chapa payment receipt URL using the reference ID +// @Tags Chapa +// @Accept json +// @Produce json +// @Param chapa_ref path string true "Chapa Reference ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/chapa/payments/receipt/{chapa_ref} [get] +func (h *Handler) GetPaymentReceipt(c *fiber.Ctx) error { + chapaRef := c.Params("chapa_ref") + if chapaRef == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to get Chapa payment receipt", + Error: "Chapa reference ID is required", + }) + } + + receiptURL, err := h.chapaSvc.GetPaymentReceiptURL(chapaRef) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to get Chapa payment receipt", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Payment receipt URL generated successfully", + Data: receiptURL, + StatusCode: 200, + Success: true, + }) +} + +// GetAllTransfers godoc +// @Summary Get all Chapa transfers +// @Description Retrieve all transfer records from Chapa +// @Tags Chapa +// @Accept json +// @Produce json +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/chapa/transfers [get] +func (h *Handler) GetAllTransfers(c *fiber.Ctx) error { + transfers, err := h.chapaSvc.GetAllTransfers(c.Context()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to fetch Chapa transfers", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Chapa transfers retrieved successfully", + Data: transfers, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// GetAccountBalance godoc +// @Summary Get Chapa account balance +// @Description Retrieve Chapa account balance, optionally filtered by currency code (e.g., ETB, USD) +// @Tags Chapa +// @Accept json +// @Produce json +// @Param currency_code query string false "Currency code (optional)" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/chapa/balances [get] +func (h *Handler) GetAccountBalance(c *fiber.Ctx) error { + currencyCode := c.Query("currency_code", "") + + balances, err := h.chapaSvc.GetAccountBalance(c.Context(), currencyCode) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to fetch Chapa account balance", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Chapa account balances retrieved successfully", + Data: balances, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// SwapCurrency godoc +// @Summary Swap currency using Chapa API +// @Description Convert an amount from one currency to another using Chapa's currency swap API +// @Tags Chapa +// @Accept json +// @Produce json +// @Param request body domain.SwapRequest true "Swap request payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/chapa/swap [post] +func (h *Handler) SwapCurrency(c *fiber.Ctx) error { + var reqBody domain.SwapRequest + + // Parse request body + if err := c.BodyParser(&reqBody); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request payload", + Error: err.Error(), + }) + } + + // Validate input + if reqBody.From == "" || reqBody.To == "" || reqBody.Amount <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Missing or invalid swap parameters", + Error: "from, to, and amount are required fields", + }) + } + + // Call service + resp, err := h.chapaSvc.SwapCurrency(c.Context(), reqBody) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to perform currency swap", + Error: err.Error(), + }) + } + + // Success response + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Currency swapped successfully", + Data: resp, + StatusCode: fiber.StatusOK, + Success: true, + }) +} diff --git a/internal/web_server/handlers/enet_pulse.go b/internal/web_server/handlers/enet_pulse.go index a54606c..e89c7b2 100644 --- a/internal/web_server/handlers/enet_pulse.go +++ b/internal/web_server/handlers/enet_pulse.go @@ -9,67 +9,10 @@ import ( "github.com/gofiber/fiber/v2" ) -// GetPreMatchOdds godoc -// @Summary Get pre-match odds for an event -// @Description Fetches pre-match odds from EnetPulse for a given event -// @Tags EnetPulse - PreMatch -// @Accept json -// @Produce json -// @Param objectFK query int true "Event ID" -// @Param oddsProviderFK query []int false "Odds provider IDs (comma separated)" -// @Param outcomeTypeFK query int false "Outcome type ID" -// @Param outcomeScopeFK query int false "Outcome scope ID" -// @Param outcomeSubtypeFK query int false "Outcome subtype ID" -// @Param limit query int false "Limit results" -// @Param offset query int false "Offset results" -// @Param languageTypeFK query int false "Language type ID" -// @Success 200 {object} domain.Response{data=domain.PreMatchOddsResponse} -// @Failure 400 {object} domain.ErrorResponse -// @Failure 502 {object} domain.ErrorResponse -// @Router /api/v1/odds/pre-match [get] -func (h *Handler) GetPreMatchOdds(c *fiber.Ctx) error { - // Parse query parameters - objectFK := c.QueryInt("objectFK") - if objectFK == 0 { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Message: "Event ID (objectFK) is required", - Error: "missing or invalid objectFK", - }) - } - - params := domain.PreMatchOddsRequest{ - ObjectFK: int64(objectFK), - OddsProviderFK: intSliceToInt64Slice(parseIntSlice(c.Query("oddsProviderFK"))), // convert []int to []int64 - OutcomeTypeFK: int64(c.QueryInt("outcomeTypeFK")), - OutcomeScopeFK: int64(c.QueryInt("outcomeScopeFK")), - OutcomeSubtypeFK: int64(c.QueryInt("outcomeSubtypeFK")), - Limit: c.QueryInt("limit"), - Offset: c.QueryInt("offset"), - LanguageTypeFK: int64(c.QueryInt("languageTypeFK")), - } - - // Call service - res, err := h.enetPulseSvc.FetchPreMatchOdds(c.Context(), params) - if err != nil { - log.Println("FetchPreMatchOdds error:", err) - return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ - Message: "Failed to fetch pre-match odds", - Error: err.Error(), - }) - } - - return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Pre-match odds fetched successfully", - Data: res, - StatusCode: fiber.StatusOK, - Success: true, - }) -} - // GetAllSports godoc // @Summary Get all sports // @Description Fetches all sports stored in the database -// @Tags EnetPulse - Sports +// @Tags EnetPulse // @Accept json // @Produce json // @Success 200 {object} domain.Response{data=[]domain.EnetpulseSport} @@ -97,7 +40,7 @@ func (h *Handler) GetAllSports(c *fiber.Ctx) error { // GetAllTournamentTemplates godoc // @Summary Get all tournament templates // @Description Fetches all tournament templates stored in the database -// @Tags EnetPulse - Tournament Templates +// @Tags EnetPulse // @Accept json // @Produce json // @Success 200 {object} domain.Response{data=[]domain.EnetpulseTournamentTemplate} @@ -125,7 +68,7 @@ func (h *Handler) GetAllTournamentTemplates(c *fiber.Ctx) error { // GetAllTournaments godoc // @Summary Get all tournaments // @Description Fetches all tournaments stored in the database -// @Tags EnetPulse - Tournaments +// @Tags EnetPulse // @Accept json // @Produce json // @Success 200 {object} domain.Response{data=[]domain.EnetpulseTournament} @@ -153,7 +96,7 @@ func (h *Handler) GetAllTournaments(c *fiber.Ctx) error { // GetAllTournamentStages godoc // @Summary Get all tournament stages // @Description Fetches all tournament stages stored in the database -// @Tags EnetPulse - Tournament Stages +// @Tags EnetPulse // @Accept json // @Produce json // @Success 200 {object} domain.Response{data=[]domain.EnetpulseTournamentStage} @@ -178,8 +121,177 @@ func (h *Handler) GetAllTournamentStages(c *fiber.Ctx) error { }) } +// GetFixturesByDate godoc +// @Summary Get all stored fixtures +// @Description Fetches all fixtures stored in the database +// @Tags EnetPulse +// @Accept json +// @Produce json +// @Success 200 {object} domain.Response{data=[]domain.EnetpulseFixture} +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/enetpulse/fixtures [get] +func (h *Handler) GetFixturesByDate(c *fiber.Ctx) error { + // Call service to get all fixtures from DB + fixtures, err := h.enetPulseSvc.GetAllFixtures(c.Context()) + if err != nil { + log.Println("GetAllFixtures error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch fixtures from database", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Fixtures fetched successfully", + Data: fixtures, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// GetAllResults godoc +// @Summary Get all results +// @Description Fetches all EnetPulse match results stored in the database +// @Tags EnetPulse +// @Accept json +// @Produce json +// @Success 200 {object} domain.Response{data=[]domain.EnetpulseResult} +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/enetpulse/results [get] +func (h *Handler) GetAllResults(c *fiber.Ctx) error { + // Call service + results, err := h.enetPulseSvc.GetAllResults(c.Context()) + if err != nil { + log.Println("GetAllResults error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch EnetPulse results", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "EnetPulse results fetched successfully", + Data: results, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// GetAllPreodds godoc +// @Summary Get all preodds +// @Description Fetches all EnetPulse pre-match odds stored in the database +// @Tags EnetPulse +// @Accept json +// @Produce json +// @Success 200 {object} domain.Response{data=[]domain.EnetpulsePreodds} +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/enetpulse/preodds [get] +func (h *Handler) GetAllPreodds(c *fiber.Ctx) error { + // Call service + preodds, err := h.enetPulseSvc.GetAllPreodds(c.Context()) + if err != nil { + log.Println("GetAllPreodds error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch EnetPulse preodds", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "EnetPulse preodds fetched successfully", + Data: preodds, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// GetAllBettingOffers godoc +// @Summary Get all betting offers +// @Description Fetches all EnetPulse preodds betting offers stored in the database +// @Tags EnetPulse +// @Accept json +// @Produce json +// @Success 200 {object} domain.Response{data=[]domain.EnetpulsePreoddsBettingOffer} +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/enetpulse/betting-offers [get] +func (h *Handler) GetAllBettingOffers(c *fiber.Ctx) error { + // Call service + offers, err := h.enetPulseSvc.GetAllBettingOffers(c.Context()) + if err != nil { + log.Println("GetAllBettingOffers error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch EnetPulse betting offers", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "EnetPulse betting offers fetched successfully", + Data: offers, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// GetAllPreoddsWithBettingOffers godoc +// @Summary Get all preodds with betting offers +// @Description Fetches all EnetPulse pre-match odds along with their associated betting offers stored in the database +// @Tags EnetPulse +// @Accept json +// @Produce json +// @Success 200 {object} domain.Response{data=[]domain.EnetpulsePreodds} +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/enetpulse/preodds-with-offers [get] +func (h *Handler) GetAllPreoddsWithBettingOffers(c *fiber.Ctx) error { + // Call service + preodds, err := h.enetPulseSvc.GetAllPreoddsWithBettingOffers(c.Context()) + if err != nil { + log.Println("GetAllPreoddsWithBettingOffers error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch EnetPulse preodds with betting offers", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "EnetPulse preodds with betting offers fetched successfully", + Data: preodds, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// GetFixturesWithPreodds godoc +// @Summary Get fixtures with preodds +// @Description Fetches all EnetPulse fixtures along with their associated pre-match odds +// @Tags EnetPulse +// @Accept json +// @Produce json +// @Success 200 {object} domain.Response{data=[]domain.EnetpulseFixtureWithPreodds} +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/enetpulse/fixtures/preodds [get] +func (h *Handler) GetFixturesWithPreodds(c *fiber.Ctx) error { + // Call service + fixtures, err := h.enetPulseSvc.GetFixturesWithPreodds(c.Context()) + if err != nil { + log.Println("GetFixturesWithPreodds error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch EnetPulse fixtures with preodds", + Error: err.Error(), + }) + } + + // Return success response + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "EnetPulse fixtures with preodds fetched successfully", + Data: fixtures, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + // Helper: parse comma-separated string into []int -func parseIntSlice(input string) []int { +func ParseIntSlice(input string) []int { if input == "" { return nil } @@ -194,7 +306,7 @@ func parseIntSlice(input string) []int { } // Helper: convert []int to []int64 -func intSliceToInt64Slice(input []int) []int64 { +func IntSliceToInt64Slice(input []int) []int64 { if input == nil { return nil } diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index eade53c..6bf3104 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -34,6 +34,7 @@ import ( virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/orchestration" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" @@ -42,6 +43,7 @@ import ( ) type Handler struct { + orchestrationSvc *orchestration.Service enetPulseSvc *enetpulse.Service telebirrSvc *telebirr.TelebirrService arifpaySvc *arifpay.ArifpayService @@ -69,7 +71,7 @@ type Handler struct { leagueSvc *league.Service virtualGameSvc virtualgameservice.VirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService - veliVirtualGameSvc veli.VeliVirtualGameService + veliVirtualGameSvc *veli.Service atlasVirtualGameSvc atlas.AtlasVirtualGameService recommendationSvc recommendation.RecommendationService authSvc *authentication.Service @@ -82,6 +84,7 @@ type Handler struct { } func New( + orchestrationSvc *orchestration.Service, enetPulseSvc *enetpulse.Service, telebirrSvc *telebirr.TelebirrService, arifpaySvc *arifpay.ArifpayService, @@ -101,7 +104,7 @@ func New( bonusSvc *bonus.Service, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService, - veliVirtualGameSvc veli.VeliVirtualGameService, + veliVirtualGameSvc *veli.Service, atlasVirtualGameSvc atlas.AtlasVirtualGameService, recommendationSvc recommendation.RecommendationService, userSvc *user.Service, @@ -121,6 +124,7 @@ func New( mongoLoggerSvc *zap.Logger, ) *Handler { return &Handler{ + orchestrationSvc: orchestrationSvc, enetPulseSvc: enetPulseSvc, telebirrSvc: telebirrSvc, arifpaySvc: arifpaySvc, diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index c8adbd2..607ac06 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -3,7 +3,9 @@ package handlers import ( "context" "errors" - "fmt" + "time" + + // "fmt" "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -120,14 +122,6 @@ func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error { // @Failure 502 {object} domain.ErrorResponse // @Router /api/v1/veli/start-game [post] func (h *Handler) StartGame(c *fiber.Ctx) error { - userId, ok := c.Locals("user_id").(int64) - if !ok { - return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ - Error: "missing user id", - Message: "Unauthorized", - }) - } - var req domain.GameStartRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ @@ -135,26 +129,25 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { Error: err.Error(), }) } - - // There needs to be a way to generate a sessionID - - // Attach user ID to request - req.PlayerID = fmt.Sprintf("%d", userId) // Default brand if not provided if req.BrandID == "" { req.BrandID = h.Cfg.VeliGames.BrandID } - req.IP = c.IP() + useId := c.Locals("user_id") + req.IP = c.IP() + req.PlayerID = useId.(string) + + // 1️⃣ Call StartGame service res, err := h.veliVirtualGameSvc.StartGame(context.Background(), req) if err != nil { h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]StartGame", zap.Any("request", req), zap.Error(err), ) - // Handle provider disabled case specifically + if strings.Contains(err.Error(), "is disabled") { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ Message: "Provider is disabled", @@ -162,13 +155,39 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { }) } - // Fallback for other errors return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to start game", Error: err.Error(), }) } + // 2️⃣ Game started successfully → Update total_games_played + go func() { + ctx := context.Background() + reportDate := time.Now().Truncate(24 * time.Hour) + reportType := "daily" + + // Increment total_games_played by 1 + err := h.orchestrationSvc.UpdateVirtualGameProviderReportByDate( + ctx, + req.ProviderID, + reportDate, + reportType, + 1, // increment total_games_played by 1 + 0, + 0, + 1, + ) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to update total_games_played", + zap.String("provider_id", req.ProviderID), + zap.Error(err), + ) + } + }() + + // 3️⃣ Return response to user return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Game started successfully", Data: res, @@ -260,15 +279,13 @@ func (h *Handler) GetBalance(c *fiber.Ctx) error { func (h *Handler) PlaceBet(c *fiber.Ctx) error { var req domain.BetRequest if err := c.BodyParser(&req); err != nil { - // return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } - // Signature check optional here - + // 1️⃣ Process the bet with the external provider res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req) if err != nil { if errors.Is(err, veli.ErrDuplicateTransaction) { @@ -278,10 +295,42 @@ func (h *Handler) PlaceBet(c *fiber.Ctx) error { Message: "Failed to process bet", Error: err.Error(), }) - // return fiber.NewError(fiber.StatusBadRequest, err.Error()) } - return c.JSON(res) + // 2️⃣ If bet successful → update total_bets in the report + go func() { + ctx := context.Background() + reportDate := time.Now().Truncate(24 * time.Hour) + reportType := "daily" + + // Increment total_bets by the bet amount + err := h.orchestrationSvc.UpdateVirtualGameProviderReportByDate( + ctx, + req.ProviderID, + reportDate, + reportType, + 0, // total_games_played (no change) + req.Amount.Amount, // add this bet to total_bets + 0, // total_payouts (no change) + 0, // total_players (no change) + ) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to update total_bets after bet", + zap.String("provider_id", req.ProviderID), + zap.Float64("bet_amount", req.Amount.Amount), + zap.Error(err), + ) + } + }() + + // 3️⃣ Return success response + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Bet processed successfully", + Data: res, + StatusCode: fiber.StatusOK, + Success: true, + }) } func (h *Handler) RegisterWin(c *fiber.Ctx) error { diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index c077906..c03c1a2 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -25,6 +25,54 @@ type launchVirtualGameRes struct { LaunchURL string `json:"launch_url"` } +// ListVirtualGameProviderReportsAscHandler +// @Summary List virtual game provider reports (ascending) +// @Description Retrieves all virtual game provider reports sorted by total_games_played in ascending order +// @Tags VirtualGames - Orchestration +// @Success 200 {array} domain.VirtualGameProviderReport +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/orchestrator/virtual-game/provider-reports/asc [get] +func (h *Handler) ListVirtualGameProviderReportsAscHandler(c *fiber.Ctx) error { + reports, err := h.orchestrationSvc.ListVirtualGameProviderReportsByGamesPlayedAsc(c.Context()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to fetch virtual game provider reports ascending", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Virtual game provider reports retrieved successfully", + Data: reports, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// ListVirtualGameProviderReportsDescHandler +// @Summary List virtual game provider reports (descending) +// @Description Retrieves all virtual game provider reports sorted by total_games_played in descending order +// @Tags VirtualGames - Orchestration +// @Success 200 {array} domain.VirtualGameProviderReport +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/orchestrator/virtual-game/provider-reports/desc [get] +func (h *Handler) ListVirtualGameProviderReportsDescHandler(c *fiber.Ctx) error { + reports, err := h.orchestrationSvc.ListVirtualGameProviderReportsByGamesPlayedDesc(c.Context()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to fetch virtual game provider reports descending", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Virtual game provider reports retrieved successfully", + Data: reports, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + // ListVirtualGames godoc // @Summary List all virtual games // @Description Returns all virtual games with optional filters (category, search, pagination) @@ -77,7 +125,7 @@ func (h *Handler) ListVirtualGames(c *fiber.Ctx) error { } // --- Call service method --- - games, err := h.veliVirtualGameSvc.GetAllVirtualGames(c.Context(), params) + games, err := h.orchestrationSvc.GetAllVirtualGames(c.Context(), params) if err != nil { log.Println("ListVirtualGames error:", err) return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ @@ -105,7 +153,7 @@ func (h *Handler) ListVirtualGames(c *fiber.Ctx) error { // @Router /api/v1/virtual-game/providers/{provider_id} [delete] func (h *Handler) RemoveProvider(c *fiber.Ctx) error { providerID := c.Params("providerID") - if err := h.virtualGameSvc.RemoveProvider(c.Context(), providerID); err != nil { + if err := h.orchestrationSvc.RemoveProvider(c.Context(), providerID); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not remove provider") } return c.SendStatus(fiber.StatusOK) @@ -122,7 +170,7 @@ func (h *Handler) RemoveProvider(c *fiber.Ctx) error { // @Router /api/v1/virtual-game/providers/{provider_id} [get] func (h *Handler) GetProviderByID(c *fiber.Ctx) error { providerID := c.Params("providerID") - provider, err := h.virtualGameSvc.GetProviderByID(c.Context(), providerID) + provider, err := h.orchestrationSvc.GetProviderByID(c.Context(), providerID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not fetch provider") } @@ -143,7 +191,7 @@ func (h *Handler) ListProviders(c *fiber.Ctx) error { limit, _ := strconv.Atoi(c.Query("limit", "20")) offset, _ := strconv.Atoi(c.Query("offset", "0")) - providers, total, err := h.virtualGameSvc.ListProviders(c.Context(), int32(limit), int32(offset)) + providers, total, err := h.orchestrationSvc.ListProviders(c.Context(), int32(limit), int32(offset)) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not list providers") } @@ -168,7 +216,7 @@ func (h *Handler) SetProviderEnabled(c *fiber.Ctx) error { providerID := c.Params("providerID") enabled, _ := strconv.ParseBool(c.Query("enabled", "true")) - provider, err := h.virtualGameSvc.SetProviderEnabled(c.Context(), providerID, enabled) + provider, err := h.orchestrationSvc.SetProviderEnabled(c.Context(), providerID, enabled) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not update provider status") } @@ -264,6 +312,14 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error { } func (h *Handler) HandleBet(c *fiber.Ctx) error { + // userID := c.Locals("user_id") + // fmt.Printf("\n\nBet User ID is%v\n\n",userID) + // if userID == "" { + // return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + // Message: "Failed to process Bet request", + // Error: "Invalid user identification", + // }) + // } // Read the raw body body := c.Body() if len(body) == 0 { @@ -292,6 +348,8 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error { }) } + // req.PlayerID = fmt.Sprintf("%v", userID) + res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req) if err != nil { if errors.Is(err, veli.ErrDuplicateTransaction) { @@ -316,6 +374,8 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error { }) } + // req.PlayerID = fmt.Sprintf("%v", userID) + resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req) if err != nil { code := fiber.StatusInternalServerError @@ -341,6 +401,8 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error { }) } + // req.PlayerID = fmt.Sprintf("%v", userID) + resp, err := h.atlasVirtualGameSvc.ProcessBet(c.Context(), req) if err != nil { // code := fiber.StatusInternalServerError diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index da8e8df..c199700 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -20,6 +20,7 @@ import ( func (a *App) initAppRoutes() { h := handlers.New( + a.orchestrationSvc, a.enetPulseSvc, a.telebirrSvc, a.arifpaySvc, @@ -150,9 +151,9 @@ func (a *App) initAppRoutes() { //Arifpay groupV1.Post("/arifpay/checkout", a.authMiddleware, h.CreateCheckoutSessionHandler) - groupV1.Post("/arifpay/checkout/cancel/:session_id", a.authMiddleware, h.CancelCheckoutSessionHandler) - groupV1.Post("/api/v1/arifpay/c2b-webhook", a.authMiddleware, h.HandleArifpayC2BWebhook) - groupV1.Post("/api/v1/arifpay/b2c-webhook", a.authMiddleware, h.HandleArifpayB2CWebhook) + groupV1.Post("/arifpay/checkout/cancel/:sessionId", a.authMiddleware, h.CancelCheckoutSessionHandler) + groupV1.Post("/api/v1/arifpay/c2b-webhook", h.HandleArifpayC2BWebhook) + groupV1.Post("/api/v1/arifpay/b2c-webhook", h.HandleArifpayB2CWebhook) groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.ExecuteArifpayB2CTransfer) groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) @@ -275,7 +276,7 @@ func (a *App) initAppRoutes() { tenant.Delete("/odds/market-settings", a.authMiddleware, a.CompanyOnly, h.DeleteAllCompanyMarketSettings) tenant.Delete("/odds/market-settings/:id", a.authMiddleware, a.CompanyOnly, h.DeleteCompanyMarketSettings) - groupV1.Get("/events", a.authMiddleware, h.GetAllEvents) + groupV1.Get("/events", h.GetAllEvents) groupV1.Get("/events/:id", a.authMiddleware, h.GetEventByID) groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) groupV1.Patch("/events/:id/is_monitored", a.authMiddleware, a.SuperAdminOnly, h.SetEventIsMonitored) @@ -290,11 +291,16 @@ func (a *App) initAppRoutes() { tenant.Get("/events/:id/bets", a.authMiddleware, a.CompanyOnly, h.GetTenantBetsByEventID) //EnetPulse - groupV1.Get("/odds/pre-match", h.GetPreMatchOdds) + // groupV1.Get("/odds/pre-match", h.GetPreMatchOdds) groupV1.Get("/sports", h.GetAllSports) groupV1.Get("/tournament_templates", h.GetAllTournamentTemplates) - groupV1.Get("/tournaments", h.GetAllTournamentTemplates) + groupV1.Get("/tournaments", h.GetAllTournaments) groupV1.Get("/tournament_stages", h.GetAllTournamentStages) + groupV1.Get("/fixtures", h.GetFixturesByDate) + groupV1.Get("/results", h.GetAllResults) + groupV1.Get("/preodds", h.GetAllPreoddsWithBettingOffers) + groupV1.Get("/bettingoffers", h.GetAllBettingOffers) + groupV1.Get("/fixtures/preodds", h.GetFixturesWithPreodds) // Leagues groupV1.Get("/leagues", a.authMiddleware, a.SuperAdminOnly, h.GetAllLeagues) @@ -381,10 +387,17 @@ func (a *App) initAppRoutes() { //Chapa Routes groupV1.Post("/chapa/payments/webhook/verify", h.WebhookCallback) - groupV1.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyTransaction) + groupV1.Get("/chapa/transaction/manual/verify/:tx_ref", a.authMiddleware, h.ManualVerifyTransaction) + groupV1.Put("/chapa/transaction/cancel/:tx_ref", a.authMiddleware, h.CancelDeposit) + groupV1.Get("/chapa/transactions", a.authMiddleware, h.FetchAllTransactions) + groupV1.Get("/chapa/transaction/events/:ref_id", a.authMiddleware, h.GetTransactionEvents) groupV1.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit) groupV1.Post("/chapa/payments/withdraw", a.authMiddleware, h.InitiateWithdrawal) groupV1.Get("/chapa/banks", h.GetSupportedBanks) + groupV1.Get("/chapa/payments/receipt/:chapa_ref", a.authMiddleware, h.GetPaymentReceipt) + groupV1.Get("/chapa/transfers", a.authMiddleware, h.GetAllTransfers) + groupV1.Get("/chapa/balance", a.authMiddleware, h.GetAccountBalance) + groupV1.Post("/chapa/swap", a.authMiddleware, h.SwapCurrency) // Currencies groupV1.Get("/currencies", h.GetSupportedCurrencies) @@ -414,7 +427,7 @@ func (a *App) initAppRoutes() { groupV1.Post("/veli/credit-balances", a.authMiddleware, h.GetCreditBalances) //Atlas Virtual Game Routes - groupV1.Get("/atlas/games", a.authMiddleware, h.InitAtlasGame) + groupV1.Get("/atlas/games", h.GetAtlasVGames) groupV1.Post("/atlas/init-game", a.authMiddleware, h.InitAtlasGame) a.fiber.Post("/account", h.AtlasGetUserDataCallback) a.fiber.Post("/betwin", h.HandleAtlasBetWin) @@ -469,6 +482,8 @@ func (a *App) initAppRoutes() { groupV1.Delete("/virtual-game/favorites/:gameID", a.authMiddleware, h.RemoveFavorite) groupV1.Get("/virtual-game/favorites", a.authMiddleware, h.ListFavorites) + groupV1.Get("/orchestrator/virtual-game/provider-reports/asc", a.OnlyAdminAndAbove, h.ListVirtualGameProviderReportsAscHandler) + groupV1.Get("/orchestrator/virtual-game/provider-reports/desc", a.OnlyAdminAndAbove, h.ListVirtualGameProviderReportsDescHandler) groupV1.Delete("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.RemoveProvider) groupV1.Get("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.GetProviderByID) groupV1.Get("/virtual-game/orchestrator/games", h.ListVirtualGames) diff --git a/makefile b/makefile index 936aee9..b25835a 100644 --- a/makefile +++ b/makefile @@ -46,45 +46,45 @@ postgres: .PHONY: backup backup: @mkdir -p backup - @docker exec -t fortunebet-backend-postgres-1 pg_dump -U root --data-only --exclude-table=schema_migrations gh | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz + @docker exec -t fortunebet-postgres-1 pg_dump -U root --data-only --exclude-table=schema_migrations gh | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz restore: @echo "Restoring latest backup..." @latest_file=$$(ls -t backup/dump_*.sql.gz | head -n 1); \ echo "Restoring from $$latest_file"; \ - gunzip -c $$latest_file | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh + gunzip -c $$latest_file | docker exec -i fortunebet-postgres-1 psql -U root -d gh restore_file: @echo "Restoring latest backup..." - gunzip -c $(file) | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh + gunzip -c $(file) | docker exec -i fortunebet-postgres-1 psql -U root -d gh .PHONY: seed_data seed_data: @echo "Waiting for PostgreSQL to be ready..." - @until docker exec fortunebet-backend-postgres-1 pg_isready -U root -d gh; do \ + @until docker exec fortunebet-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; \ + cat $$file | docker exec -i fortunebet-postgres-1 psql -U root -d gh; \ done .PHONY: seed_dev_data seed_dev_data: @echo "Waiting for PostgreSQL to be ready..." - @until docker exec fortunebet-backend-postgres-1 pg_isready -U root -d gh; do \ + @until docker exec fortunebet-postgres-1 pg_isready -U root -d gh; do \ echo "PostgreSQL is not ready yet..."; \ sleep 1; \ done - cat db/scripts/fix_autoincrement_desync.sql | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh; + cat db/scripts/fix_autoincrement_desync.sql | docker exec -i fortunebet-postgres-1 psql -U root -d gh; @for file in db/dev_data/*.sql; do \ if [ -f "$$file" ]; then \ echo "Seeding $$file..."; \ - cat $$file | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh; \ + cat $$file | docker exec -i fortunebet-postgres-1 psql -U root -d gh; \ fi \ done postgres_log: - docker logs fortunebet-backend-postgres-1 + docker logs fortunebet-postgres-1 .PHONY: swagger swagger: @swag init -g cmd/main.go @@ -94,7 +94,7 @@ logs: db-up: | logs @mkdir -p logs @docker compose up -d postgres migrate mongo - @docker logs fortunebet-backend-postgres-1 > logs/postgres.log 2>&1 & + @docker logs fortunebet-postgres-1 > logs/postgres.log 2>&1 & .PHONY: db-down db-down: @docker compose down -v