From 9cb008225b85e558010aeab585d30578e8013780 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 14 Oct 2025 14:21:27 +0300 Subject: [PATCH 01/11] Enetpulse fixes --- db/migrations/000011_enet_pulse.down.sql | 2 - db/migrations/000011_enet_pulse.up.sql | 64 -- db/migrations/00008_enet_pulse.down.sql | 5 +- db/migrations/00008_enet_pulse.up.sql | 204 ++++ db/query/enet_pulse.sql | 241 +++++ docker-compose.yml | 13 +- gen/db/enet_pulse.sql.go | 741 ++++++++++++++ gen/db/models.go | 137 +++ internal/config/config.go | 62 +- internal/domain/enet_pulse.go | 206 ++++ internal/repository/enet_pulse.go | 429 ++++++++ internal/services/enet_pulse/service.go | 1054 +++++++++++++++++--- internal/services/odds/service.go | 6 +- internal/services/result/service.go | 18 +- internal/web_server/cron.go | 83 +- internal/web_server/handlers/enet_pulse.go | 57 ++ internal/web_server/routes.go | 4 +- 17 files changed, 3041 insertions(+), 285 deletions(-) delete mode 100644 db/migrations/000011_enet_pulse.down.sql delete mode 100644 db/migrations/000011_enet_pulse.up.sql 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..84c0daa 100644 --- a/db/migrations/00008_enet_pulse.down.sql +++ b/db/migrations/00008_enet_pulse.down.sql @@ -1,2 +1,5 @@ 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; \ 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..faa33a7 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..f4c05bb 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, @@ -130,5 +133,243 @@ 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_stage_fk, + tournament_stage_name, + 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,$16,$17,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_stage_fk = EXCLUDED.tournament_stage_fk, + tournament_stage_name = EXCLUDED.tournament_stage_name, + 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_stage_fk, + tournament_stage_name, + 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 +) 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, $29, $30 +) +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_stage_fk = EXCLUDED.tournament_stage_fk, + tournament_stage_name = EXCLUDED.tournament_stage_name, + 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: 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; + + + + + diff --git a/docker-compose.yml b/docker-compose.yml index b480236..1846a77 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: postgres: image: postgres:16-alpine ports: - - 5422:5432 + - "5422:5432" environment: - POSTGRES_PASSWORD=secret - POSTGRES_USER=root @@ -19,6 +19,7 @@ services: volumes: - postgres_data:/var/lib/postgresql/data - ./exports:/exports + mongo: container_name: fortunebet-mongo image: mongo:7.0.11 @@ -37,6 +38,7 @@ services: interval: 10s timeout: 5s retries: 5 + migrate: image: migrate/migrate volumes: @@ -54,6 +56,7 @@ services: ] networks: - app + redis: image: redis:7-alpine ports: @@ -66,17 +69,17 @@ services: timeout: 5s retries: 5 - zookeeper: image: confluentinc/cp-zookeeper:7.5.0 container_name: zookeeper ports: - - "2181:2181" + - "22181:2181" # remapped host port (Windows-safe) environment: ZOOKEEPER_CLIENT_PORT: 2181 ZOOKEEPER_TICK_TIME: 2000 networks: - app + kafka: image: confluentinc/cp-kafka:7.5.0 depends_on: @@ -88,19 +91,19 @@ services: KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - ports: - "9092:9092" - "29092:29092" networks: - app + app: build: context: . 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/gen/db/enet_pulse.sql.go b/gen/db/enet_pulse.sql.go index a2c131b..03ed49a 100644 --- a/gen/db/enet_pulse.sql.go +++ b/gen/db/enet_pulse.sql.go @@ -11,6 +11,519 @@ 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_stage_fk, + tournament_stage_name, + 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,$16,$17,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_stage_fk = EXCLUDED.tournament_stage_fk, + tournament_stage_name = EXCLUDED.tournament_stage_name, + 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_stage_fk, tournament_stage_name, 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"` + TournamentStageFk pgtype.Text `json:"tournament_stage_fk"` + TournamentStageName pgtype.Text `json:"tournament_stage_name"` + 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.TournamentStageFk, + arg.TournamentStageName, + 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.TournamentStageFk, + &i.TournamentStageName, + &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_stage_fk, + tournament_stage_name, + 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 +) 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, $29, $30 +) +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_stage_fk = EXCLUDED.tournament_stage_fk, + tournament_stage_name = EXCLUDED.tournament_stage_name, + 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_stage_fk, tournament_stage_name, 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"` + TournamentStageFk pgtype.Text `json:"tournament_stage_fk"` + TournamentStageName pgtype.Text `json:"tournament_stage_name"` + 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.TournamentStageFk, + arg.TournamentStageName, + 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.TournamentStageFk, + &i.TournamentStageName, + &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 CreateEnetpulseSport = `-- name: CreateEnetpulseSport :one INSERT INTO enetpulse_sports ( sport_id, @@ -63,6 +576,7 @@ func (q *Queries) CreateEnetpulseSport(ctx context.Context, arg CreateEnetpulseS } const CreateEnetpulseTournament = `-- name: CreateEnetpulseTournament :one + INSERT INTO enetpulse_tournaments ( tournament_id, name, @@ -90,6 +604,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, @@ -250,6 +766,231 @@ 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_stage_fk, tournament_stage_name, 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.TournamentStageFk, + &i.TournamentStageName, + &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 GetAllEnetpulseResults = `-- name: GetAllEnetpulseResults :many +SELECT id, result_id, name, sport_fk, tournament_fk, tournament_template_fk, tournament_stage_fk, tournament_stage_name, 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.TournamentStageFk, + &i.TournamentStageName, + &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, diff --git a/gen/db/models.go b/gen/db/models.go index cff8694..50f63d1 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -256,6 +256,143 @@ type DisabledOdd struct { CreatedAt pgtype.Timestamp `json:"created_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"` + TournamentStageFk pgtype.Text `json:"tournament_stage_fk"` + TournamentStageName pgtype.Text `json:"tournament_stage_name"` + 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"` + TournamentStageFk pgtype.Text `json:"tournament_stage_fk"` + TournamentStageName pgtype.Text `json:"tournament_stage_name"` + 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"` diff --git a/internal/config/config.go b/internal/config/config.go index 23e738b..73031ba 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,7 +35,7 @@ var ( ErrInvalidAtlasBaseUrl = errors.New("Atlas Base URL is invalid") ErrInvalidAtlasOperatorID = errors.New("Atlas operator ID is invalid") ErrInvalidAtlasSecretKey = errors.New("Atlas secret key is invalid") - ErrInvalidAtlasBrandID = errors.New("Atlas brand ID is invalid") + ErrInvalidAtlasBrandID = errors.New("Atlas brand ID is invalid") ErrInvalidAtlasPartnerID = errors.New("Atlas Partner ID is invalid") ErrMissingResendApiKey = errors.New("missing Resend Api key") @@ -46,8 +46,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 { @@ -188,6 +189,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") @@ -427,34 +429,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/enet_pulse.go b/internal/domain/enet_pulse.go index aae67ea..a4c75eb 100644 --- a/internal/domain/enet_pulse.go +++ b/internal/domain/enet_pulse.go @@ -462,3 +462,209 @@ 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 + 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 + CreatedAt time.Time + UpdatedAt 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 EnetpulsePreodds 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"` +} diff --git a/internal/repository/enet_pulse.go b/internal/repository/enet_pulse.go index 0ea921e..208bf5d 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,165 @@ 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 ConvertCreateEnetpulseTournamentStage(stage domain.CreateEnetpulseTournamentStage) dbgen.EnetpulseTournamentStage { // return dbgen.EnetpulseTournamentStage{ // StageID: stage.StageID, @@ -157,6 +319,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, + StatusType: dbF.StatusType.String, + StatusDescFK: dbF.StatusDescFk.String, + RoundTypeFK: dbF.RoundTypeFk.String, + UpdatesCount: int(dbF.UpdatesCount.Int32), + LastUpdatedAt: dbF.LastUpdatedAt.Time, + CreatedAt: dbF.CreatedAt.Time, + UpdatedAt: dbF.UpdatedAt.Time, + } +} + func ConvertCreateEnetpulseTournamentStage(stage domain.CreateEnetpulseTournamentStage) dbgen.CreateEnetpulseTournamentStageParams { return dbgen.CreateEnetpulseTournamentStageParams{ StageID: stage.StageID, @@ -321,3 +531,222 @@ 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: fmt.Sprintf("%v", dbP.EventFk), + OutcomeTypeFK: fmt.Sprintf("%v", dbP.OutcomeTypeFk), + OutcomeScopeFK: fmt.Sprintf("%v", dbP.OutcomeScopeFk), + OutcomeSubtypeFK: fmt.Sprintf("%v", dbP.OutcomeSubtypeFk), + EventParticipantNumber: int(dbP.EventParticipantNumber.Int32), + IParam: dbP.Iparam.String, + IParam2: dbP.Iparam2.String, + DParam: dbP.Dparam.String, + DParam2: dbP.Dparam2.String, + SParam: dbP.Sparam.String, + UpdatesCount: int(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, + } +} diff --git a/internal/services/enet_pulse/service.go b/internal/services/enet_pulse/service.go index 0aed297..f813ad9 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,95 +143,101 @@ 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 } } @@ -452,6 +459,767 @@ 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 the database + sports, err := s.store.GetAllEnetpulseSports(ctx) + if err != nil { + return fmt.Errorf("failed to fetch sports from DB: %w", err) + } + + // Struct for decoding each fixture from API + type Fixture struct { + FixtureID string `json:"id"` + Name string `json:"name"` + SportFK string `json:"sportFK"` + TournamentFK string `json:"tournamentFK"` + TournamentName string `json:"tournament_name"` + StartDate string `json:"startdate"` + StatusType string `json:"status_type"` + HomeTeam string `json:"home_team"` + AwayTeam string `json:"away_team"` + HomeTeamID string `json:"home_team_id"` + AwayTeamID string `json:"away_team_id"` + HomeKitImage string `json:"home_kit_image"` + AwayKitImage string `json:"away_kit_image"` + } + + // 2️⃣ Loop through each sport + for _, sport := range sports { + if sport.SportID != "1" { + continue + } + + url := fmt.Sprintf( + "http://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 fixtures 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 response for sport %s: %v\n", sport.SportID, err) + continue + } + + // 4️⃣ Iterate over fixtures and store as events + for _, fx := range fixturesResp.Events { + // Conversions + sportID, _ := strconv.Atoi(fx.SportFK) + homeTeamID, _ := strconv.ParseInt(fx.HomeTeamID, 10, 64) + awayTeamID, _ := strconv.ParseInt(fx.AwayTeamID, 10, 64) + leagueID, _ := strconv.ParseInt(fx.TournamentFK, 10, 64) + startDate, _ := time.Parse("2006-01-02 15:04:05", fx.StartDate) + + event := domain.CreateEvent{ + SourceEventID: fx.FixtureID, + SportID: int32(sportID), + MatchName: fx.Name, + HomeTeam: fx.HomeTeam, + AwayTeam: fx.AwayTeam, + HomeTeamID: homeTeamID, + AwayTeamID: awayTeamID, + HomeTeamImage: fx.HomeKitImage, + AwayTeamImage: fx.AwayKitImage, + LeagueID: leagueID, + LeagueName: fx.TournamentName, + StartTime: startDate, + IsLive: false, // default, can update later from live feed + Status: domain.STATUS_PENDING, // map to enum if needed + Source: "EnetPulse", // custom enum constant + DefaultWinningUpperLimit: 0, // default, can adjust + } + + // 5️⃣ Save event in DB (UPSERT) + if err := s.store.SaveEvent(ctx, event); err != nil { + fmt.Printf("failed storing event %s: %v\n", fx.FixtureID, err) + continue + } + } + + fmt.Printf("✅ Successfully fetched and stored events for sport %s\n", sport.SportID) + break + } + + fmt.Println("✅ Completed fetching and storing events for all sports") + return nil +} + +func (s *Service) FetchFixtures(ctx context.Context, date string) ([]domain.EnetpulseFixture, error) { + var allFixtures []domain.EnetpulseFixture + + // 1️⃣ Fetch all sports from the database + sports, err := s.store.GetAllEnetpulseSports(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch sports from DB: %w", err) + } + + // Struct for decoding each fixture from API + 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"` + StatusType string `json:"status_type"` + StatusDescFK string `json:"status_descFK"` + RoundTypeFK string `json:"round_typeFK"` + UpdatesCount string `json:"n"` + LastUpdatedAt string `json:"ut"` + } + + // 2️⃣ Loop through each sport + for _, sport := range sports { + // Only fetch for sport "1" (Football) + if sport.SportID != "1" { + continue + } + + url := fmt.Sprintf( + "http://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 fixtures 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 response for sport %s: %v\n", sport.SportID, err) + continue + } + + // 4️⃣ Iterate over fixtures and store them + for _, fx := range fixturesResp.Events { + tournamentFK, _ := strconv.Atoi(fx.TournamentFK) + tournamentTemplateFK, _ := strconv.Atoi(fx.TournamentTemplateFK) + tournamentStageFK, _ := strconv.Atoi(fx.TournamentStageFK) + statusDescFK, _ := strconv.Atoi(fx.StatusDescFK) + roundTypeFK, _ := strconv.Atoi(fx.RoundTypeFK) + updatesCount, _ := strconv.Atoi(fx.UpdatesCount) + + startDate, _ := time.Parse(time.RFC3339, fx.StartDate) + lastUpdatedAt, _ := time.Parse(time.RFC3339, fx.LastUpdatedAt) + + createFixture := domain.CreateEnetpulseFixture{ + FixtureID: fx.FixtureID, + Name: fx.Name, + SportFK: fx.SportFK, + TournamentFK: strconv.Itoa(tournamentFK), + TournamentTemplateFK: strconv.Itoa(tournamentTemplateFK), + TournamentStageFK: strconv.Itoa(tournamentStageFK), + TournamentStageName: fx.TournamentStageName, + TournamentName: fx.TournamentName, + TournamentTemplateName: fx.TournamentTemplateName, + SportName: fx.SportName, + Gender: fx.Gender, + StartDate: startDate, + StatusType: fx.StatusType, + StatusDescFK: strconv.Itoa(statusDescFK), + RoundTypeFK: strconv.Itoa(roundTypeFK), + UpdatesCount: updatesCount, + LastUpdatedAt: lastUpdatedAt, + } + + dbFixture, err := s.store.CreateEnetpulseFixture(ctx, createFixture) + if err != nil { + fmt.Printf("failed storing fixture %s: %v\n", fx.FixtureID, err) + continue + } + + allFixtures = append(allFixtures, dbFixture) + } + + fmt.Printf("✅ Successfully fetched and stored fixtures for sport %s\n", sport.SportID) + break // stop after first relevant sport + } + + fmt.Println("✅ Completed fetching and storing fixtures for all sports") + return allFixtures, 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 { + // 1️⃣ Fetch all sports (if you want to limit to one, adjust the loop as in your template fetcher) + sports, err := s.store.GetAllEnetpulseSports(ctx) + if err != nil { + return fmt.Errorf("failed to fetch sports from DB: %w", err) + } + + type Result 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"` + 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 string `json:"game_started"` + FirstHalfEnded string `json:"first_half_ended"` + SecondHalfStarted string `json:"second_half_started"` + SecondHalfEnded string `json:"second_half_ended"` + GameEnded string `json:"game_ended"` + } + + for _, sport := range sports { + if sport.SportID != "1" { // ⚽ Example: Only Football + continue + } + + url := fmt.Sprintf( + // "https://eapi.enetpulse.com/event/results/?username=kirubelapiusr&token=b1d35ee5fb8371938c6ca1b4fd6c75cc&sportFK=1&language_typeFK=3&date=2025-10-12" + "http://eapi.enetpulse.com/event/results/?sportFK=%s&date=%s&username=%s&token=%s", + sport.SportID, + time.DateOnly, + 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 raw struct { + EventResults json.RawMessage `json:"results"` + } + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("reading results response for sport %s: %w", sport.SportID, err) + } + if err := json.Unmarshal(bodyBytes, &raw); err != nil { + return fmt.Errorf("unmarshalling raw results for sport %s: %w", sport.SportID, err) + } + + results := map[string]Result{} + if len(raw.EventResults) > 0 && raw.EventResults[0] == '{' { + if err := json.Unmarshal(raw.EventResults, &results); err != nil { + return fmt.Errorf("decoding results (object) for sport %s: %w", sport.SportID, err) + } + } else { + fmt.Printf("No results found for sport %s\n", sport.SportID) + continue + } + + for _, r := range results { + updatesCount := 0 + if r.N != "" { + if n, err := strconv.Atoi(r.N); err == nil { + updatesCount = n + } + } + + lastUpdatedAt, _ := time.Parse(time.RFC3339, r.UT) + startDate, _ := time.Parse(time.RFC3339, r.StartDate) + gameStarted, _ := time.Parse(time.RFC3339, r.GameStarted) + firstHalfEnded, _ := time.Parse(time.RFC3339, r.FirstHalfEnded) + secondHalfStarted, _ := time.Parse(time.RFC3339, r.SecondHalfStarted) + secondHalfEnded, _ := time.Parse(time.RFC3339, r.SecondHalfEnded) + gameEnded, _ := time.Parse(time.RFC3339, r.GameEnded) + + createResult := domain.CreateEnetpulseResult{ + ResultID: r.ID, + Name: r.Name, + SportFK: r.SportFK, + TournamentFK: r.TournamentFK, + TournamentTemplateFK: r.TournamentTemplateFK, + TournamentStageFK: r.TournamentStageFK, + TournamentStageName: r.TournamentStageName, + TournamentName: r.TournamentName, + TournamentTemplateName: r.TournamentTemplateName, + SportName: r.SportName, + StartDate: startDate, + StatusType: r.StatusType, + StatusDescFK: r.StatusDescFK, + RoundTypeFK: r.RoundTypeFK, + UpdatesCount: int32(updatesCount), + LastUpdatedAt: lastUpdatedAt, + Round: r.Round, + Live: r.Live, + VenueName: r.VenueName, + LivestatsPlus: r.LivestatsPlus, + LivestatsType: r.LivestatsType, + Commentary: r.Commentary, + LineupConfirmed: r.LineupConfirmed, + Verified: r.Verified, + Spectators: r.Spectators, + GameStarted: &gameStarted, + FirstHalfEnded: &firstHalfEnded, + SecondHalfStarted: &secondHalfStarted, + SecondHalfEnded: &secondHalfEnded, + GameEnded: &gameEnded, + } + + if _, err := s.store.CreateEnetpulseResult(ctx, createResult); err != nil { + fmt.Printf("❌ failed to store result %s: %v\n", r.ID, err) + continue + } + } + + break // limit to one sport if necessary + } + + fmt.Println("✅ Successfully fetched and stored EnetPulse results") + 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 events from DB + fixtures, err := s.store.GetAllEnetpulseFixtures(ctx) + if err != nil { + return fmt.Errorf("failed to fetch fixtures: %w", err) + } + + // providerIDStr := strconv.Itoa(int(s.cfg.EnetPulseConfig.ProviderID)) + + // 2️⃣ Loop through each fixture/event + for _, fixture := range fixtures { + url := fmt.Sprintf( + "http://eapi.enetpulse.com/preodds/event/?objectFK=%s&odds_providerFK=%s&username=%s&token=%s", + fixture.FixtureID, + s.cfg.EnetPulseConfig.ProviderID, + s.cfg.EnetPulseConfig.UserName, + s.cfg.EnetPulseConfig.Token, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + // optionally log error and continue to next fixture + continue + } + + resp, err := s.httpClient.Do(req) + if err != nil { + continue + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + continue + } + + // Decode API response + 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"` + BettingOffers []struct { + ID string `json:"id"` + 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:"bettingoffers"` + } `json:"preodds"` + } + + if err := json.NewDecoder(resp.Body).Decode(&preoddsResp); err != nil { + continue + } + + // Iterate and store preodds and nested betting offers + for _, p := range preoddsResp.Preodds { + updatesCount := 0 + if p.N != "" { + if n, err := strconv.Atoi(p.N); err == nil { + updatesCount = n + } + } + + lastUpdatedAt, _ := time.Parse(time.RFC3339, p.UT) + + eventParticipantNumber := int32(0) + if p.EventParticipantNumber != "" { + if epn, err := strconv.Atoi(p.EventParticipantNumber); err == nil { + eventParticipantNumber = int32(epn) + } + } + + createPreodds := domain.CreateEnetpulsePreodds{ + PreoddsID: p.ID, + EventFK: fixture.FixtureID, + OutcomeTypeFK: string(p.OutcomeTypeFK), + OutcomeScopeFK: string(p.OutcomeScopeFK), + OutcomeSubtypeFK: string(p.OutcomeSubtypeFK), + EventParticipantNumber: int(eventParticipantNumber), + IParam: p.Iparam, + IParam2: p.Iparam2, + DParam: p.Dparam, + DParam2: p.Dparam2, + SParam: p.Sparam, + UpdatesCount: int(updatesCount), + LastUpdatedAt: lastUpdatedAt, + } + + storedPreodds, err := s.store.CreateEnetpulsePreodds(ctx, createPreodds) + if err != nil { + continue + } + + for _, o := range p.BettingOffers { + bettingUpdates := 0 + if o.N != "" { + if n, err := strconv.Atoi(o.N); err == nil { + bettingUpdates = n + } + } + + bettingLastUpdatedAt, _ := time.Parse(time.RFC3339, o.UT) + + createOffer := domain.CreateEnetpulsePreoddsBettingOffer{ + BettingOfferID: o.ID, + PreoddsFK: storedPreodds.PreoddsID, + BettingOfferStatusFK: o.BettingOfferStatusFK, + OddsProviderFK: o.OddsProviderFK, + Odds: o.Odds, + OddsOld: o.OddsOld, + Active: o.Active, + CouponKey: o.CouponKey, + UpdatesCount: int(bettingUpdates), + LastUpdatedAt: bettingLastUpdatedAt, + } + + _, _ = s.store.CreateEnetpulsePreoddsBettingOffer(ctx, createOffer) + } + } + } + + return nil +} + +// 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) + } + 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 +} + +// helper to safely parse string to int32 +// func parseStringToInt32(s string) int32 { +// if s == "" { +// return 0 +// } +// i, err := strconv.Atoi(s) +// if err != nil { +// return 0 +// } +// return int32(i) +// } + func (s *Service) FetchTournamentTemplates(ctx context.Context) (*domain.TournamentTemplatesResponse, error) { url := fmt.Sprintf( "http://eapi.enetpulse.com/tournamenttemplate/list/?username=%s&token=%s", @@ -757,72 +1525,72 @@ 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) +// 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) - } +// // 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" - } +// // 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) - } +// // 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() +// 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)) - } +// 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) - } +// // 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 -} +// return &fixturesResp, nil +// } func (s *Service) FetchResults(ctx context.Context, params domain.ResultsRequest) (*domain.ResultsResponse, error) { // Build base URL diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 0a9f401..dc2700d 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -84,9 +84,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 698e743..9dd144c 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -236,10 +236,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 { @@ -733,10 +733,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( @@ -955,7 +955,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/web_server/cron.go b/internal/web_server/cron.go index f1caaac..e717345 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -252,60 +252,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("✅ Completed fetching and storing sports") } + // 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("✅ Completed fetching and storing tournament templates") } + // 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("✅ Completed fetching and storing tournaments") } + // 4️⃣ Tournament Stages 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), - ) + if err := enetPulseSvc.FetchAndStoreTournamentStages(ctx); err != nil { + mongoLogger.Error("Failed to fetch and store tournament stages", zap.Error(err)) } else { - mongoLogger.Info("Completed fetching and storing tournament stages without errors") + mongoLogger.Info("✅ Completed fetching and storing tournament stages") + } + + // 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 fixtures") + } + + // 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("✅ Completed fetching and storing results") + } + + // 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("✅ Completed fetching and storing outcome_types") + } + + // 8 Outcome Types + 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("✅ Completed fetching and storing preodds") } }, }, } 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/enet_pulse.go b/internal/web_server/handlers/enet_pulse.go index a54606c..05ad993 100644 --- a/internal/web_server/handlers/enet_pulse.go +++ b/internal/web_server/handlers/enet_pulse.go @@ -178,6 +178,63 @@ 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 - Fixtures +// @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 - Results +// @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, + }) +} + + // Helper: parse comma-separated string into []int func parseIntSlice(input string) []int { if input == "" { diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 6b3dedc..61bb47b 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -259,7 +259,7 @@ func (a *App) initAppRoutes() { tenant.Get("/odds/upcoming/:upcoming_id/market/:market_id", h.GetTenantOddsByMarketID) tenant.Post("/odds/settings", a.CompanyOnly, h.SaveOddsSetting) - groupV1.Get("/events", a.authMiddleware, h.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) @@ -275,6 +275,8 @@ func (a *App) initAppRoutes() { groupV1.Get("/tournament_templates", h.GetAllTournamentTemplates) groupV1.Get("/tournaments", h.GetAllTournamentTemplates) groupV1.Get("/tournament_stages", h.GetAllTournamentStages) + groupV1.Get("/fixtures", h.GetFixturesByDate) + groupV1.Get("/results", h.GetAllResults) // Leagues tenant.Get("/leagues", h.GetAllLeagues) From fc69ca3f528017237e9326537dc83fce87fa8f54 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 15 Oct 2025 16:42:00 +0300 Subject: [PATCH 02/11] Enetpulse fixture and preodds --- docs/docs.go | 153 ++------------------- docs/swagger.json | 153 ++------------------- docs/swagger.yaml | 106 ++------------ internal/web_server/handlers/enet_pulse.go | 73 ++-------- 4 files changed, 32 insertions(+), 453 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 47d40df..a34cfd4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3058,7 +3058,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Fixtures" + "EnetPulse" ], "summary": "Get all stored fixtures", "responses": { @@ -3102,7 +3102,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Fixtures" + "EnetPulse" ], "summary": "Get fixtures with preodds", "responses": { @@ -3146,7 +3146,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Preodds" + "EnetPulse" ], "summary": "Get all preodds", "responses": { @@ -3190,7 +3190,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Results" + "EnetPulse" ], "summary": "Get all results", "responses": { @@ -3234,7 +3234,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Sports" + "EnetPulse" ], "summary": "Get all sports", "responses": { @@ -3278,7 +3278,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Tournament Stages" + "EnetPulse" ], "summary": "Get all tournament stages", "responses": { @@ -3322,7 +3322,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Tournament Templates" + "EnetPulse" ], "summary": "Get all tournament templates", "responses": { @@ -3366,7 +3366,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "EnetPulse - Tournaments" + "EnetPulse" ], "summary": "Get all tournaments", "responses": { @@ -4300,108 +4300,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", @@ -12402,41 +12300,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": { diff --git a/docs/swagger.json b/docs/swagger.json index 252532a..c59c2e1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3050,7 +3050,7 @@ "application/json" ], "tags": [ - "EnetPulse - Fixtures" + "EnetPulse" ], "summary": "Get all stored fixtures", "responses": { @@ -3094,7 +3094,7 @@ "application/json" ], "tags": [ - "EnetPulse - Fixtures" + "EnetPulse" ], "summary": "Get fixtures with preodds", "responses": { @@ -3138,7 +3138,7 @@ "application/json" ], "tags": [ - "EnetPulse - Preodds" + "EnetPulse" ], "summary": "Get all preodds", "responses": { @@ -3182,7 +3182,7 @@ "application/json" ], "tags": [ - "EnetPulse - Results" + "EnetPulse" ], "summary": "Get all results", "responses": { @@ -3226,7 +3226,7 @@ "application/json" ], "tags": [ - "EnetPulse - Sports" + "EnetPulse" ], "summary": "Get all sports", "responses": { @@ -3270,7 +3270,7 @@ "application/json" ], "tags": [ - "EnetPulse - Tournament Stages" + "EnetPulse" ], "summary": "Get all tournament stages", "responses": { @@ -3314,7 +3314,7 @@ "application/json" ], "tags": [ - "EnetPulse - Tournament Templates" + "EnetPulse" ], "summary": "Get all tournament templates", "responses": { @@ -3358,7 +3358,7 @@ "application/json" ], "tags": [ - "EnetPulse - Tournaments" + "EnetPulse" ], "summary": "Get all tournaments", "responses": { @@ -4292,108 +4292,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", @@ -12394,41 +12292,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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2fee177..c6c6fce 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1757,31 +1757,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: @@ -6186,7 +6161,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all stored fixtures tags: - - EnetPulse - Fixtures + - EnetPulse /api/v1/enetpulse/fixtures/preodds: get: consumes: @@ -6213,7 +6188,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get fixtures with preodds tags: - - EnetPulse - Fixtures + - EnetPulse /api/v1/enetpulse/preodds: get: consumes: @@ -6239,7 +6214,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all preodds tags: - - EnetPulse - Preodds + - EnetPulse /api/v1/enetpulse/results: get: consumes: @@ -6265,7 +6240,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all results tags: - - EnetPulse - Results + - EnetPulse /api/v1/enetpulse/sports: get: consumes: @@ -6291,7 +6266,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all sports tags: - - EnetPulse - Sports + - EnetPulse /api/v1/enetpulse/tournament-stages: get: consumes: @@ -6317,7 +6292,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all tournament stages tags: - - EnetPulse - Tournament Stages + - EnetPulse /api/v1/enetpulse/tournament-templates: get: consumes: @@ -6343,7 +6318,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all tournament templates tags: - - EnetPulse - Tournament Templates + - EnetPulse /api/v1/enetpulse/tournaments: get: consumes: @@ -6369,7 +6344,7 @@ paths: $ref: '#/definitions/domain.ErrorResponse' summary: Get all tournaments tags: - - EnetPulse - Tournaments + - EnetPulse /api/v1/events: get: consumes: @@ -6966,71 +6941,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: diff --git a/internal/web_server/handlers/enet_pulse.go b/internal/web_server/handlers/enet_pulse.go index 443a677..d3d812c 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} @@ -181,7 +124,7 @@ 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 - Fixtures +// @Tags EnetPulse // @Accept json // @Produce json // @Success 200 {object} domain.Response{data=[]domain.EnetpulseFixture} @@ -209,7 +152,7 @@ func (h *Handler) GetFixturesByDate(c *fiber.Ctx) error { // GetAllResults godoc // @Summary Get all results // @Description Fetches all EnetPulse match results stored in the database -// @Tags EnetPulse - Results +// @Tags EnetPulse // @Accept json // @Produce json // @Success 200 {object} domain.Response{data=[]domain.EnetpulseResult} @@ -237,7 +180,7 @@ func (h *Handler) GetAllResults(c *fiber.Ctx) error { // GetAllPreodds godoc // @Summary Get all preodds // @Description Fetches all EnetPulse pre-match odds stored in the database -// @Tags EnetPulse - Preodds +// @Tags EnetPulse // @Accept json // @Produce json // @Success 200 {object} domain.Response{data=[]domain.EnetpulsePreodds} @@ -265,7 +208,7 @@ func (h *Handler) GetAllPreodds(c *fiber.Ctx) error { // GetFixturesWithPreodds godoc // @Summary Get fixtures with preodds // @Description Fetches all EnetPulse fixtures along with their associated pre-match odds -// @Tags EnetPulse - Fixtures +// @Tags EnetPulse // @Accept json // @Produce json // @Success 200 {object} domain.Response{data=[]domain.EnetpulseFixtureWithPreodds} From e0e4ff4b6486e4e1c91b6120d861167250f39fde Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 17 Oct 2025 11:30:33 +0300 Subject: [PATCH 03/11] Atlas Gaming Fixes --- internal/services/virtualGame/atlas/client.go | 5 ++-- internal/web_server/handlers/enet_pulse.go | 4 ++-- internal/web_server/handlers/veli_games.go | 23 ++++++++++--------- .../handlers/virtual_games_hadlers.go | 14 +++++++++++ internal/web_server/routes.go | 6 ++--- makefile | 20 ++++++++-------- 6 files changed, 44 insertions(+), 28 deletions(-) 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/web_server/handlers/enet_pulse.go b/internal/web_server/handlers/enet_pulse.go index d3d812c..7bd3616 100644 --- a/internal/web_server/handlers/enet_pulse.go +++ b/internal/web_server/handlers/enet_pulse.go @@ -235,7 +235,7 @@ func (h *Handler) GetFixturesWithPreodds(c *fiber.Ctx) error { } // Helper: parse comma-separated string into []int -func parseIntSlice(input string) []int { +func ParseIntSlice(input string) []int { if input == "" { return nil } @@ -250,7 +250,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/veli_games.go b/internal/web_server/handlers/veli_games.go index c8adbd2..8232ee6 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -3,7 +3,7 @@ package handlers import ( "context" "errors" - "fmt" + // "fmt" "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -120,13 +120,14 @@ 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", - }) - } + // userId, ok := c.Locals("user_id").(int64) + // fmt.Printf("\n\nVeli Start Game User ID is %v\n\n", userId) + // 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 { @@ -135,11 +136,11 @@ 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) + // req.PlayerID = fmt.Sprintf("%d", userId) // Default brand if not provided if req.BrandID == "" { diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index c077906..8c1fdd3 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -264,6 +264,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 +300,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 +326,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 +353,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 03a7b8c..aabc03f 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -284,7 +284,7 @@ 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.GetAllTournaments) @@ -400,7 +400,7 @@ func (a *App) initAppRoutes() { //Veli Virtual Game Routes groupV1.Post("/veli/providers", h.GetProviders) groupV1.Post("/veli/games-list", h.GetGamesByProvider) - groupV1.Post("/veli/start-game", a.authMiddleware, h.StartGame) + groupV1.Post("/veli/start-game", h.StartGame) groupV1.Post("/veli/start-demo-game", h.StartDemoGame) a.fiber.Post("/balance", h.GetBalance) groupV1.Post("/veli/gaming-activity", a.authMiddleware, h.GetGamingActivity) @@ -408,7 +408,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) 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 From 4104d7d3715211022531e4297418321200ed3a22 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 20 Oct 2025 15:16:39 +0300 Subject: [PATCH 04/11] Chapa Fixes --- docs/docs.go | 184 ++++++++++++++++++++++- docs/swagger.json | 184 ++++++++++++++++++++++- docs/swagger.yaml | 122 ++++++++++++++- internal/domain/atlas.go | 2 +- internal/domain/chapa.go | 25 +++ internal/services/chapa/port.go | 5 +- internal/services/chapa/service.go | 141 ++++++++++++++++- internal/services/virtualGame/service.go | 4 - internal/web_server/handlers/chapa.go | 129 ++++++++++++++++ internal/web_server/routes.go | 4 + 10 files changed, 790 insertions(+), 10 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index a34cfd4..a057097 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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", @@ -2209,6 +2252,50 @@ const docTemplate = `{ } } }, + "/api/v1/chapa/payments/receipt/{chapa_ref}": { + "get": { + "description": "Retrieve the Chapa payment receipt URL using the reference ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get Chapa Payment Receipt URL", + "parameters": [ + { + "type": "string", + "description": "Chapa Reference ID", + "name": "chapa_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/payments/webhook/verify": { "post": { "description": "Handles payment notifications from Chapa", @@ -2319,6 +2406,87 @@ const docTemplate = `{ } } }, + "/api/v1/chapa/swap": { + "post": { + "description": "Perform a USD to ETB currency swap using Chapa's API", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Initiate a currency swap", + "parameters": [ + { + "description": "Swap Request Payload", + "name": "payload", + "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/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", @@ -9837,7 +10005,7 @@ const docTemplate = `{ "type": "string" }, "round_id": { - "type": "integer" + "type": "string" }, "session_id": { "type": "string" @@ -12934,6 +13102,20 @@ const docTemplate = `{ } } }, + "domain.SwapRequest": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + } + } + }, "domain.TelebirrPaymentCallbackPayload": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index c59c2e1..46c389a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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", @@ -2201,6 +2244,50 @@ } } }, + "/api/v1/chapa/payments/receipt/{chapa_ref}": { + "get": { + "description": "Retrieve the Chapa payment receipt URL using the reference ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Get Chapa Payment Receipt URL", + "parameters": [ + { + "type": "string", + "description": "Chapa Reference ID", + "name": "chapa_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/payments/webhook/verify": { "post": { "description": "Handles payment notifications from Chapa", @@ -2311,6 +2398,87 @@ } } }, + "/api/v1/chapa/swap": { + "post": { + "description": "Perform a USD to ETB currency swap using Chapa's API", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chapa" + ], + "summary": "Initiate a currency swap", + "parameters": [ + { + "description": "Swap Request Payload", + "name": "payload", + "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/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", @@ -9829,7 +9997,7 @@ "type": "string" }, "round_id": { - "type": "integer" + "type": "string" }, "session_id": { "type": "string" @@ -12926,6 +13094,20 @@ } } }, + "domain.SwapRequest": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + } + } + }, "domain.TelebirrPaymentCallbackPayload": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c6c6fce..db66f08 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -75,7 +75,7 @@ definitions: player_id: type: string round_id: - type: integer + type: string session_id: type: string timestamp: @@ -2191,6 +2191,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: @@ -5509,6 +5518,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: @@ -5589,6 +5627,35 @@ paths: summary: Verify a payment manually tags: - Chapa + /api/v1/chapa/payments/receipt/{chapa_ref}: + get: + consumes: + - application/json + description: Retrieve the Chapa payment receipt URL using the reference ID + parameters: + - description: Chapa Reference ID + in: path + name: chapa_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: Get Chapa Payment Receipt URL + tags: + - Chapa /api/v1/chapa/payments/webhook/verify: post: consumes: @@ -5661,6 +5728,59 @@ paths: summary: Initiate a withdrawal tags: - Chapa + /api/v1/chapa/swap: + post: + consumes: + - application/json + description: Perform a USD to ETB currency swap using Chapa's API + parameters: + - description: Swap Request Payload + in: body + name: payload + 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: Initiate a currency swap + 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: 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..bf6b683 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -196,3 +196,28 @@ type ChapaWebHookPayment struct { } `json:"customization"` Meta string `json:"meta"` } + +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 SwapResponse 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"` +} diff --git a/internal/services/chapa/port.go b/internal/services/chapa/port.go index 78ae033..862e3c4 100644 --- a/internal/services/chapa/port.go +++ b/internal/services/chapa/port.go @@ -16,10 +16,13 @@ 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) 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 + 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 5822cb7..03cba51 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -1,9 +1,13 @@ package chapa import ( + "bytes" "context" + "encoding/json" "errors" "fmt" + "io" + "net/http" "strconv" "strings" @@ -63,7 +67,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma return "", fmt.Errorf("failed to get sender wallets: %w", err) } for _, wallet := range senderWallets { - if wallet.IsWithdraw { + if wallet.IsTransferable { senderWallet = wallet break } @@ -364,3 +368,138 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai return nil } + +func (s *Service) GetPaymentReceiptURL(ctx context.Context, chapaRef string) (string, error) { + if chapaRef == "" { + return "", fmt.Errorf("chapa reference ID is required") + } + + receiptURL := fmt.Sprintf("https://chapa.link/payment-receipt/%s", chapaRef) + return receiptURL, nil +} + +func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.chapa.co/v1/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 struct { + Status string `json:"status"` + Message string `json:"message"` + Data []domain.Transfer `json:"data"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return result.Data, nil +} + +func (s *Service) GetAccountBalance(ctx context.Context, currencyCode string) ([]domain.Balance, error) { + baseURL := "https://api.chapa.co/v1/balances" + if currencyCode != "" { + baseURL = fmt.Sprintf("%s/%s", baseURL, strings.ToLower(currencyCode)) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, 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) 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/virtualGame/service.go b/internal/services/virtualGame/service.go index 46ddb66..435e03b 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -657,10 +657,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/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 9a2a7c2..f260bc5 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -263,3 +263,132 @@ func (h *Handler) InitiateWithdrawal(c *fiber.Ctx) error { 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(c.Context(), 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 balance retrieved successfully", + Data: balances, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// InitiateSwap godoc +// @Summary Initiate a currency swap +// @Description Perform a USD to ETB currency swap using Chapa's API +// @Tags Chapa +// @Accept json +// @Produce json +// @Param payload 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) InitiateSwap(c *fiber.Ctx) error { + var req domain.SwapRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request payload", + Error: err.Error(), + }) + } + + swapResult, err := h.chapaSvc.InitiateSwap(c.Context(), req.Amount, req.From, req.To) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to initiate currency swap", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Currency swap initiated successfully", + Data: swapResult, + StatusCode: fiber.StatusOK, + Success: true, + }) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index aabc03f..c1e8146 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -383,6 +383,10 @@ func (a *App) initAppRoutes() { 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", h.GetPaymentReceipt) + groupV1.Get("/chapa/transfers", h.GetAllTransfers) + groupV1.Get("/chapa/balance", h.GetAccountBalance) + groupV1.Post("/chapa/init-swap", h.InitiateSwap) // Currencies groupV1.Get("/currencies", h.GetSupportedCurrencies) From e98477d6cc3aff76148f245b709a3fdf4b468f79 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sat, 25 Oct 2025 17:53:36 +0300 Subject: [PATCH 05/11] virtual game provider report --- cmd/main.go | 10 +- db/migrations/000001_fortune.up.sql | 37 ++ db/query/virtual_games.sql | 85 ++- docs/docs.go | 90 +++ docs/swagger.json | 90 +++ docs/swagger.yaml | 61 ++ gen/db/models.go | 29 + gen/db/virtual_games.sql.go | 272 +++++++++ internal/domain/virtual_game.go | 58 ++ internal/repository/virtual_game.go | 188 +++++++ .../virtualGame/game_orchestration.go | 81 --- .../virtualGame/orchestration/port.go | 14 + .../virtualGame/orchestration/service.go | 526 ++++++++++++++++++ internal/services/virtualGame/port.go | 9 +- internal/services/virtualGame/veli/client.go | 2 +- .../virtualGame/veli/game_orchestration.go | 325 ----------- internal/services/virtualGame/veli/service.go | 14 +- internal/web_server/app.go | 8 +- internal/web_server/cron.go | 65 ++- internal/web_server/handlers/atlas.go | 40 +- internal/web_server/handlers/handlers.go | 12 +- internal/web_server/handlers/veli_games.go | 87 ++- .../handlers/virtual_games_hadlers.go | 58 +- internal/web_server/routes.go | 3 + 24 files changed, 1682 insertions(+), 482 deletions(-) delete mode 100644 internal/services/virtualGame/game_orchestration.go create mode 100644 internal/services/virtualGame/orchestration/port.go create mode 100644 internal/services/virtualGame/orchestration/service.go diff --git a/cmd/main.go b/cmd/main.go index 3f0972b..92f0fb6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -59,6 +59,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" @@ -153,6 +154,12 @@ func main() { aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) veliVirtualGameService := veli.New(virtualGameSvc, vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), domain.MongoDBLogger, cfg) + orchestrationSvc := orchestration.New( + virtualGameSvc, + virtuaGamesRepo, + cfg, + veliCLient, + ) atlasClient := atlas.NewClient(cfg, walletSvc) atlasVirtualGameService := atlas.New(virtualGameSvc, vitualGameRepo, atlasClient, walletSvc, wallet.TransferStore(store), cfg) recommendationSvc := recommendation.NewService(recommendationRepo) @@ -194,7 +201,7 @@ func main() { ) go httpserver.StartEnetPulseCron(enePulseSvc, domain.MongoDBLogger) - go httpserver.SetupReportandVirtualGameCronJobs(context.Background(), reportSvc, veliVirtualGameService, "C:/Users/User/Desktop") + go httpserver.SetupReportandVirtualGameCronJobs(context.Background(), reportSvc, orchestrationSvc, "C:/Users/User/Desktop") go httpserver.ProcessBetCashback(context.TODO(), betSvc) bankRepository := repository.NewBankRepository(store) @@ -259,6 +266,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 4b636ec..e791639 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -36,6 +36,24 @@ CREATE TABLE IF NOT EXISTS virtual_game_providers ( created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ ); + +CREATE TABLE IF NOT EXISTS virtual_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, @@ -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, diff --git a/db/query/virtual_games.sql b/db/query/virtual_games.sql index 46e5061..8bee846 100644 --- a/db/query/virtual_games.sql +++ b/db/query/virtual_games.sql @@ -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/docs/docs.go b/docs/docs.go index a057097..5eddf6c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4627,6 +4627,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)", @@ -13452,6 +13504,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 46c389a..3823958 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4619,6 +4619,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)", @@ -13444,6 +13496,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 db66f08..66ec732 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2431,6 +2431,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: @@ -7167,6 +7192,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: diff --git a/gen/db/models.go b/gen/db/models.go index 65348ad..a3b349a 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1063,6 +1063,35 @@ 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"` diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 5a2809a..a9e8bec 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -281,6 +281,132 @@ 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, @@ -587,6 +713,39 @@ 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, @@ -745,6 +904,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,6 +1080,43 @@ func (q *Queries) UpdateVirtualGameProviderEnabled(ctx context.Context, arg Upda return i, err } +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 + provider_id = $1 + AND report_date = $2 + AND report_type = $3 +` + +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) 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 +} + const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec UPDATE virtual_game_sessions SET status = $2, 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/virtual_game.go b/internal/repository/virtual_game.go index c792801..f970a9d 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" @@ -36,6 +38,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 { @@ -314,3 +322,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/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/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 index 2b340d8..6082148 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -1,326 +1 @@ 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 cea093e..3e2992c 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -111,7 +111,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 } @@ -139,7 +139,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) } @@ -174,7 +174,7 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (* // 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) } @@ -205,7 +205,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) } @@ -598,7 +598,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 } @@ -639,7 +639,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 } @@ -662,7 +662,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 a836706..cf3d6fe 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -33,6 +33,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" @@ -48,7 +49,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 @@ -90,7 +92,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, @@ -146,6 +149,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 99ad807..51f225f 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -16,7 +16,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/orchestration" "github.com/robfig/cron/v3" "go.uber.org/zap" ) @@ -160,21 +160,18 @@ func StartCleanupCrons(ticketService ticket.Service, notificationSvc *notificati func SetupReportandVirtualGameCronJobs( ctx context.Context, reportService *report.Service, - 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", }, } @@ -183,7 +180,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 == "" { @@ -191,6 +188,7 @@ func SetupReportandVirtualGameCronJobs( return } + // Step 1. Fetch and store all virtual games req := domain.ProviderRequest{ BrandID: brandID, ExtraData: true, @@ -198,7 +196,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 @@ -206,19 +204,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()) - - 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") - } + // 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) + }); err != nil { log.Fatalf("Failed to schedule %s cron job: %v", period, err) } 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/handlers.go b/internal/web_server/handlers/handlers.go index 48f60f7..70ecc44 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -33,6 +33,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" @@ -41,6 +42,7 @@ import ( ) type Handler struct { + orchestrationSvc *orchestration.Service enetPulseSvc *enetpulse.Service telebirrSvc *telebirr.TelebirrService arifpaySvc *arifpay.ArifpayService @@ -53,7 +55,7 @@ type Handler struct { notificationSvc *notificationservice.Service userSvc *user.Service referralSvc *referralservice.Service - raffleSvc raffle.RaffleStore + raffleSvc raffle.RaffleStore bonusSvc *bonus.Service reportSvc report.ReportStore chapaSvc *chapa.Service @@ -68,7 +70,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 @@ -80,6 +82,7 @@ type Handler struct { } func New( + orchestrationSvc *orchestration.Service, enetPulseSvc *enetpulse.Service, telebirrSvc *telebirr.TelebirrService, arifpaySvc *arifpay.ArifpayService, @@ -99,7 +102,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, @@ -118,6 +121,7 @@ func New( mongoLoggerSvc *zap.Logger, ) *Handler { return &Handler{ + orchestrationSvc: orchestrationSvc, enetPulseSvc: enetPulseSvc, telebirrSvc: telebirrSvc, arifpaySvc: arifpaySvc, @@ -132,7 +136,7 @@ func New( chapaSvc: chapaSvc, walletSvc: walletSvc, referralSvc: referralSvc, - raffleSvc: raffleSvc, + raffleSvc: raffleSvc, bonusSvc: bonusSvc, validator: validator, userSvc: userSvc, diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index 8232ee6..5bf3c1b 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -3,6 +3,8 @@ package handlers import ( "context" "errors" + "time" + // "fmt" "strings" @@ -120,15 +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) - // fmt.Printf("\n\nVeli Start Game User ID is %v\n\n", userId) - // 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{ @@ -137,11 +130,6 @@ func (h *Handler) StartGame(c *fiber.Ctx) 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 @@ -149,13 +137,14 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { req.IP = c.IP() + // 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", @@ -163,13 +152,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, @@ -261,15 +276,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) { @@ -279,10 +292,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 8c1fdd3..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") } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c1e8146..27f8099 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, @@ -467,6 +468,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) From 857212d9ba71596f8d0e525dbfc222616a7fed72 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 28 Oct 2025 13:35:36 +0300 Subject: [PATCH 06/11] enetpulse fixture and result fixes --- db/migrations/000001_fortune.up.sql | 2 +- db/migrations/00008_enet_pulse.up.sql | 8 +- db/query/enet_pulse.sql | 100 +++++- docker-compose.yml | 2 +- gen/db/enet_pulse.sql.go | 291 +++++++++++++-- gen/db/models.go | 4 - internal/domain/enet_pulse.go | 154 +++++--- internal/repository/enet_pulse.go | 210 +++++++++-- internal/services/enet_pulse/service.go | 450 +++++++----------------- internal/web_server/cron.go | 24 +- 10 files changed, 761 insertions(+), 484 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index e791639..6f2043c 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -56,7 +56,7 @@ 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), diff --git a/db/migrations/00008_enet_pulse.up.sql b/db/migrations/00008_enet_pulse.up.sql index faa33a7..416bddf 100644 --- a/db/migrations/00008_enet_pulse.up.sql +++ b/db/migrations/00008_enet_pulse.up.sql @@ -67,9 +67,9 @@ CREATE TABLE IF NOT EXISTS enetpulse_fixtures ( 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_fk VARCHAR(50) REFERENCES enetpulse_tournament_stages(stage_id) ON DELETE CASCADE, - tournament_stage_name VARCHAR(255), + -- tournament_stage_name VARCHAR(255), tournament_name VARCHAR(255), tournament_template_name VARCHAR(255), sport_name VARCHAR(255), @@ -94,9 +94,9 @@ CREATE TABLE IF NOT EXISTS enetpulse_results ( 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_fk VARCHAR(50) REFERENCES enetpulse_tournament_stages(stage_id) ON DELETE CASCADE, - tournament_stage_name VARCHAR(255), + -- tournament_stage_name VARCHAR(255), tournament_name VARCHAR(255), tournament_template_name VARCHAR(255), sport_name VARCHAR(255), diff --git a/db/query/enet_pulse.sql b/db/query/enet_pulse.sql index 1f9c195..96cf090 100644 --- a/db/query/enet_pulse.sql +++ b/db/query/enet_pulse.sql @@ -154,8 +154,6 @@ INSERT INTO enetpulse_fixtures ( sport_fk, tournament_fk, tournament_template_fk, - tournament_stage_fk, - tournament_stage_name, tournament_name, tournament_template_name, sport_name, @@ -169,7 +167,7 @@ INSERT INTO enetpulse_fixtures ( created_at, updated_at ) VALUES ( - $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP + $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 @@ -177,8 +175,6 @@ SET sport_fk = EXCLUDED.sport_fk, tournament_fk = EXCLUDED.tournament_fk, tournament_template_fk = EXCLUDED.tournament_template_fk, - tournament_stage_fk = EXCLUDED.tournament_stage_fk, - tournament_stage_name = EXCLUDED.tournament_stage_name, tournament_name = EXCLUDED.tournament_name, tournament_template_name = EXCLUDED.tournament_template_name, sport_name = EXCLUDED.sport_name, @@ -192,6 +188,7 @@ SET updated_at = CURRENT_TIMESTAMP RETURNING *; + -- name: GetAllEnetpulseFixtures :many SELECT * FROM enetpulse_fixtures @@ -204,8 +201,6 @@ INSERT INTO enetpulse_results ( sport_fk, tournament_fk, tournament_template_fk, - tournament_stage_fk, - tournament_stage_name, tournament_name, tournament_template_name, sport_name, @@ -228,14 +223,14 @@ INSERT INTO enetpulse_results ( first_half_ended, second_half_started, second_half_ended, - game_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, $29, $30 + $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 @@ -243,8 +238,6 @@ SET sport_fk = EXCLUDED.sport_fk, tournament_fk = EXCLUDED.tournament_fk, tournament_template_fk = EXCLUDED.tournament_template_fk, - tournament_stage_fk = EXCLUDED.tournament_stage_fk, - tournament_stage_name = EXCLUDED.tournament_stage_name, tournament_name = EXCLUDED.tournament_name, tournament_template_name = EXCLUDED.tournament_template_name, sport_name = EXCLUDED.sport_name, @@ -276,6 +269,80 @@ 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, @@ -389,7 +456,6 @@ SELECT f.sport_fk, f.tournament_fk, f.tournament_template_fk, - f.tournament_stage_fk, f.start_date, f.status_type, f.status_desc_fk, diff --git a/docker-compose.yml b/docker-compose.yml index 6e5e7de..933a16d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,7 +25,7 @@ services: image: mongo:7.0.11 restart: always ports: - - "27022:27017" + - "27025:27017" environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: secret diff --git a/gen/db/enet_pulse.sql.go b/gen/db/enet_pulse.sql.go index 6a8c063..aafaad4 100644 --- a/gen/db/enet_pulse.sql.go +++ b/gen/db/enet_pulse.sql.go @@ -18,8 +18,6 @@ INSERT INTO enetpulse_fixtures ( sport_fk, tournament_fk, tournament_template_fk, - tournament_stage_fk, - tournament_stage_name, tournament_name, tournament_template_name, sport_name, @@ -33,7 +31,7 @@ INSERT INTO enetpulse_fixtures ( created_at, updated_at ) VALUES ( - $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP + $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 @@ -41,8 +39,6 @@ SET sport_fk = EXCLUDED.sport_fk, tournament_fk = EXCLUDED.tournament_fk, tournament_template_fk = EXCLUDED.tournament_template_fk, - tournament_stage_fk = EXCLUDED.tournament_stage_fk, - tournament_stage_name = EXCLUDED.tournament_stage_name, tournament_name = EXCLUDED.tournament_name, tournament_template_name = EXCLUDED.tournament_template_name, sport_name = EXCLUDED.sport_name, @@ -54,7 +50,7 @@ SET 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_stage_fk, tournament_stage_name, 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 +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 { @@ -63,8 +59,6 @@ type CreateEnetpulseFixtureParams struct { SportFk string `json:"sport_fk"` TournamentFk pgtype.Text `json:"tournament_fk"` TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"` - TournamentStageFk pgtype.Text `json:"tournament_stage_fk"` - TournamentStageName pgtype.Text `json:"tournament_stage_name"` TournamentName pgtype.Text `json:"tournament_name"` TournamentTemplateName pgtype.Text `json:"tournament_template_name"` SportName pgtype.Text `json:"sport_name"` @@ -84,8 +78,6 @@ func (q *Queries) CreateEnetpulseFixture(ctx context.Context, arg CreateEnetpuls arg.SportFk, arg.TournamentFk, arg.TournamentTemplateFk, - arg.TournamentStageFk, - arg.TournamentStageName, arg.TournamentName, arg.TournamentTemplateName, arg.SportName, @@ -105,8 +97,6 @@ func (q *Queries) CreateEnetpulseFixture(ctx context.Context, arg CreateEnetpuls &i.SportFk, &i.TournamentFk, &i.TournamentTemplateFk, - &i.TournamentStageFk, - &i.TournamentStageName, &i.TournamentName, &i.TournamentTemplateName, &i.SportName, @@ -351,8 +341,6 @@ INSERT INTO enetpulse_results ( sport_fk, tournament_fk, tournament_template_fk, - tournament_stage_fk, - tournament_stage_name, tournament_name, tournament_template_name, sport_name, @@ -375,14 +363,14 @@ INSERT INTO enetpulse_results ( first_half_ended, second_half_started, second_half_ended, - game_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, $29, $30 + $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 @@ -390,8 +378,6 @@ SET sport_fk = EXCLUDED.sport_fk, tournament_fk = EXCLUDED.tournament_fk, tournament_template_fk = EXCLUDED.tournament_template_fk, - tournament_stage_fk = EXCLUDED.tournament_stage_fk, - tournament_stage_name = EXCLUDED.tournament_stage_name, tournament_name = EXCLUDED.tournament_name, tournament_template_name = EXCLUDED.tournament_template_name, sport_name = EXCLUDED.sport_name, @@ -416,7 +402,7 @@ SET 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_stage_fk, tournament_stage_name, 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 +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 { @@ -425,8 +411,6 @@ type CreateEnetpulseResultParams struct { SportFk string `json:"sport_fk"` TournamentFk pgtype.Text `json:"tournament_fk"` TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"` - TournamentStageFk pgtype.Text `json:"tournament_stage_fk"` - TournamentStageName pgtype.Text `json:"tournament_stage_name"` TournamentName pgtype.Text `json:"tournament_name"` TournamentTemplateName pgtype.Text `json:"tournament_template_name"` SportName pgtype.Text `json:"sport_name"` @@ -459,8 +443,6 @@ func (q *Queries) CreateEnetpulseResult(ctx context.Context, arg CreateEnetpulse arg.SportFk, arg.TournamentFk, arg.TournamentTemplateFk, - arg.TournamentStageFk, - arg.TournamentStageName, arg.TournamentName, arg.TournamentTemplateName, arg.SportName, @@ -493,8 +475,6 @@ func (q *Queries) CreateEnetpulseResult(ctx context.Context, arg CreateEnetpulse &i.SportFk, &i.TournamentFk, &i.TournamentTemplateFk, - &i.TournamentStageFk, - &i.TournamentStageName, &i.TournamentName, &i.TournamentTemplateName, &i.SportName, @@ -524,6 +504,164 @@ func (q *Queries) CreateEnetpulseResult(ctx context.Context, arg CreateEnetpulse 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, @@ -781,7 +919,7 @@ func (q *Queries) CreateEnetpulseTournamentTemplate(ctx context.Context, arg Cre } const GetAllEnetpulseFixtures = `-- name: GetAllEnetpulseFixtures :many -SELECT id, fixture_id, name, sport_fk, tournament_fk, tournament_template_fk, tournament_stage_fk, tournament_stage_name, 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 +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 ` @@ -802,8 +940,6 @@ func (q *Queries) GetAllEnetpulseFixtures(ctx context.Context) ([]EnetpulseFixtu &i.SportFk, &i.TournamentFk, &i.TournamentTemplateFk, - &i.TournamentStageFk, - &i.TournamentStageName, &i.TournamentName, &i.TournamentTemplateName, &i.SportName, @@ -946,7 +1082,7 @@ func (q *Queries) GetAllEnetpulsePreoddsBettingOffers(ctx context.Context) ([]En } const GetAllEnetpulseResults = `-- name: GetAllEnetpulseResults :many -SELECT id, result_id, name, sport_fk, tournament_fk, tournament_template_fk, tournament_stage_fk, tournament_stage_name, 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 +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 ` @@ -967,8 +1103,6 @@ func (q *Queries) GetAllEnetpulseResults(ctx context.Context) ([]EnetpulseResult &i.SportFk, &i.TournamentFk, &i.TournamentTemplateFk, - &i.TournamentStageFk, - &i.TournamentStageName, &i.TournamentName, &i.TournamentTemplateName, &i.SportName, @@ -1172,6 +1306,88 @@ 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, @@ -1180,7 +1396,6 @@ SELECT f.sport_fk, f.tournament_fk, f.tournament_template_fk, - f.tournament_stage_fk, f.start_date, f.status_type, f.status_desc_fk, @@ -1221,7 +1436,6 @@ type GetFixturesWithPreoddsRow struct { SportFk string `json:"sport_fk"` TournamentFk pgtype.Text `json:"tournament_fk"` TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"` - TournamentStageFk pgtype.Text `json:"tournament_stage_fk"` StartDate pgtype.Timestamptz `json:"start_date"` StatusType pgtype.Text `json:"status_type"` StatusDescFk pgtype.Text `json:"status_desc_fk"` @@ -1264,7 +1478,6 @@ func (q *Queries) GetFixturesWithPreodds(ctx context.Context) ([]GetFixturesWith &i.SportFk, &i.TournamentFk, &i.TournamentTemplateFk, - &i.TournamentStageFk, &i.StartDate, &i.StatusType, &i.StatusDescFk, diff --git a/gen/db/models.go b/gen/db/models.go index a3b349a..dd3f35e 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -264,8 +264,6 @@ type EnetpulseFixture struct { SportFk string `json:"sport_fk"` TournamentFk pgtype.Text `json:"tournament_fk"` TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"` - TournamentStageFk pgtype.Text `json:"tournament_stage_fk"` - TournamentStageName pgtype.Text `json:"tournament_stage_name"` TournamentName pgtype.Text `json:"tournament_name"` TournamentTemplateName pgtype.Text `json:"tournament_template_name"` SportName pgtype.Text `json:"sport_name"` @@ -333,8 +331,6 @@ type EnetpulseResult struct { SportFk string `json:"sport_fk"` TournamentFk pgtype.Text `json:"tournament_fk"` TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"` - TournamentStageFk pgtype.Text `json:"tournament_stage_fk"` - TournamentStageName pgtype.Text `json:"tournament_stage_name"` TournamentName pgtype.Text `json:"tournament_name"` TournamentTemplateName pgtype.Text `json:"tournament_template_name"` SportName pgtype.Text `json:"sport_name"` diff --git a/internal/domain/enet_pulse.go b/internal/domain/enet_pulse.go index 6a5881a..78bcf77 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 @@ -486,32 +486,32 @@ type CreateEnetpulseFixture struct { // 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 + 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"` + 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"` @@ -544,13 +544,13 @@ type CreateEnetpulseResult struct { // ✅ 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"` + 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"` @@ -704,3 +704,65 @@ type EnetpulsePreodds struct { CreatedAt time.Time UpdatedAt time.Time } + +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/repository/enet_pulse.go b/internal/repository/enet_pulse.go index b66e225..2dc99a9 100644 --- a/internal/repository/enet_pulse.go +++ b/internal/repository/enet_pulse.go @@ -321,16 +321,16 @@ func (s *Store) GetFixturesWithPreodds(ctx context.Context) ([]domain.EnetpulseF 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 + // 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 } } @@ -387,13 +387,13 @@ func (s *Store) GetFixturesWithPreodds(ctx context.Context) ([]domain.EnetpulseF // 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 != ""}, + 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 != ""}, @@ -410,13 +410,13 @@ func ConvertCreateEnetpulseFixture(f domain.CreateEnetpulseFixture) dbgen.Create // 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, + 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, @@ -599,13 +599,13 @@ 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 != ""}, + 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 != ""}, @@ -635,14 +635,14 @@ func ConvertCreateEnetpulseResult(input domain.CreateEnetpulseResult) dbgen.Crea // 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, + 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, @@ -815,3 +815,135 @@ func ConvertDBEnetpulsePreoddsBettingOffer(o dbgen.EnetpulsePreoddsBettingoffer) 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/services/enet_pulse/service.go b/internal/services/enet_pulse/service.go index ed31572..2fa4304 100644 --- a/internal/services/enet_pulse/service.go +++ b/internal/services/enet_pulse/service.go @@ -479,12 +479,12 @@ func (s *Service) FetchAndStoreFixtures(ctx context.Context, date string) error TournamentTemplateName string `json:"tournament_template_name"` SportName string `json:"sport_name"` Gender string `json:"gender"` - StartDate string `json:"startdate"` // ISO 8601 + 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 + UpdatesCount string `json:"n"` // convert to int + LastUpdatedAt string `json:"ut"` // parse to time.Time } // 2️⃣ Loop through each sport @@ -585,130 +585,6 @@ func (s *Service) FetchAndStoreFixtures(ctx context.Context, date string) error return nil } - -// func (s *Service) FetchFixtures(ctx context.Context, date string) ([]domain.EnetpulseFixture, error) { -// var allFixtures []domain.EnetpulseFixture - -// // 1️⃣ Fetch all sports from the database -// sports, err := s.store.GetAllEnetpulseSports(ctx) -// if err != nil { -// return nil, fmt.Errorf("failed to fetch sports from DB: %w", err) -// } - -// // Struct for decoding each fixture from API -// 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"` -// StatusType string `json:"status_type"` -// StatusDescFK string `json:"status_descFK"` -// RoundTypeFK string `json:"round_typeFK"` -// UpdatesCount string `json:"n"` -// LastUpdatedAt string `json:"ut"` -// } - -// // 2️⃣ Loop through each sport -// for _, sport := range sports { -// // Only fetch for sport "1" (Football) -// if sport.SportID != "1" { -// continue -// } - -// url := fmt.Sprintf( -// "http://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 fixtures 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 response for sport %s: %v\n", sport.SportID, err) -// continue -// } - -// // 4️⃣ Iterate over fixtures and store them -// for _, fx := range fixturesResp.Events { -// tournamentFK, _ := strconv.Atoi(fx.TournamentFK) -// tournamentTemplateFK, _ := strconv.Atoi(fx.TournamentTemplateFK) -// tournamentStageFK, _ := strconv.Atoi(fx.TournamentStageFK) -// statusDescFK, _ := strconv.Atoi(fx.StatusDescFK) -// roundTypeFK, _ := strconv.Atoi(fx.RoundTypeFK) -// updatesCount, _ := strconv.Atoi(fx.UpdatesCount) - -// startDate, _ := time.Parse(time.RFC3339, fx.StartDate) -// lastUpdatedAt, _ := time.Parse(time.RFC3339, fx.LastUpdatedAt) - -// createFixture := domain.CreateEnetpulseFixture{ -// FixtureID: fx.FixtureID, -// Name: fx.Name, -// SportFK: fx.SportFK, -// TournamentFK: strconv.Itoa(tournamentFK), -// TournamentTemplateFK: strconv.Itoa(tournamentTemplateFK), -// TournamentStageFK: strconv.Itoa(tournamentStageFK), -// TournamentStageName: fx.TournamentStageName, -// TournamentName: fx.TournamentName, -// TournamentTemplateName: fx.TournamentTemplateName, -// SportName: fx.SportName, -// Gender: fx.Gender, -// StartDate: startDate, -// StatusType: fx.StatusType, -// StatusDescFK: strconv.Itoa(statusDescFK), -// RoundTypeFK: strconv.Itoa(roundTypeFK), -// UpdatesCount: updatesCount, -// LastUpdatedAt: lastUpdatedAt, -// } - -// dbFixture, err := s.store.CreateEnetpulseFixture(ctx, createFixture) -// if err != nil { -// fmt.Printf("failed storing fixture %s: %v\n", fx.FixtureID, err) -// continue -// } - -// allFixtures = append(allFixtures, dbFixture) -// } - -// // fmt.Printf("✅ Successfully fetched and stored fixtures for sport %s\n", sport.SportID) -// break // stop after first relevant sport -// } - -// // fmt.Println("✅ Completed fetching and storing fixtures for all sports") -// return allFixtures, nil -// } - func (s *Service) GetAllFixtures(ctx context.Context) ([]domain.EnetpulseFixture, error) { // 1️⃣ Fetch all from store fixtures, err := s.store.GetAllEnetpulseFixtures(ctx) @@ -719,59 +595,24 @@ func (s *Service) GetAllFixtures(ctx context.Context) ([]domain.EnetpulseFixture } func (s *Service) FetchAndStoreResults(ctx context.Context) error { - // 1️⃣ Fetch all sports (if you want to limit to one, adjust the loop as in your template fetcher) sports, err := s.store.GetAllEnetpulseSports(ctx) if err != nil { return fmt.Errorf("failed to fetch sports from DB: %w", err) } - type Result 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"` - 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 string `json:"game_started"` - FirstHalfEnded string `json:"first_half_ended"` - SecondHalfStarted string `json:"second_half_started"` - SecondHalfEnded string `json:"second_half_ended"` - GameEnded string `json:"game_ended"` - } - for _, sport := range sports { - if sport.SportID != "1" { // ⚽ Example: Only Football + if sport.SportID != "1" { continue } + today := time.Now().Format("2006-01-02") url := fmt.Sprintf( - // "https://eapi.enetpulse.com/event/results/?username=kirubelapiusr&token=b1d35ee5fb8371938c6ca1b4fd6c75cc&sportFK=1&language_typeFK=3&date=2025-10-12" "http://eapi.enetpulse.com/event/results/?sportFK=%s&date=%s&username=%s&token=%s", sport.SportID, - time.DateOnly, + today, s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, ) - fmt.Println("Fetching results:", url) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) @@ -791,87 +632,130 @@ func (s *Service) FetchAndStoreResults(ctx context.Context) error { sport.SportID, resp.StatusCode, string(body)) } - var raw struct { - EventResults json.RawMessage `json:"results"` + 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, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("reading results response for sport %s: %w", sport.SportID, err) - } - if err := json.Unmarshal(bodyBytes, &raw); err != nil { - return fmt.Errorf("unmarshalling raw results for sport %s: %w", sport.SportID, err) + bodyBytes, _ := io.ReadAll(resp.Body) + if err := json.Unmarshal(bodyBytes, &data); err != nil { + return fmt.Errorf("decoding results failed: %w", err) } - results := map[string]Result{} - if len(raw.EventResults) > 0 && raw.EventResults[0] == '{' { - if err := json.Unmarshal(raw.EventResults, &results); err != nil { - return fmt.Errorf("decoding results (object) for sport %s: %w", sport.SportID, err) - } - } else { - fmt.Printf("No results found for sport %s\n", sport.SportID) - continue - } - - for _, r := range results { - updatesCount := 0 - if r.N != "" { - if n, err := strconv.Atoi(r.N); err == nil { - updatesCount = n - } - } - - lastUpdatedAt, _ := time.Parse(time.RFC3339, r.UT) - startDate, _ := time.Parse(time.RFC3339, r.StartDate) - gameStarted, _ := time.Parse(time.RFC3339, r.GameStarted) - firstHalfEnded, _ := time.Parse(time.RFC3339, r.FirstHalfEnded) - secondHalfStarted, _ := time.Parse(time.RFC3339, r.SecondHalfStarted) - secondHalfEnded, _ := time.Parse(time.RFC3339, r.SecondHalfEnded) - gameEnded, _ := time.Parse(time.RFC3339, r.GameEnded) + 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: r.ID, - Name: r.Name, - SportFK: r.SportFK, - TournamentFK: r.TournamentFK, - TournamentTemplateFK: r.TournamentTemplateFK, - TournamentStageFK: r.TournamentStageFK, - TournamentStageName: r.TournamentStageName, - TournamentName: r.TournamentName, - TournamentTemplateName: r.TournamentTemplateName, - SportName: r.SportName, + 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: r.StatusType, - StatusDescFK: r.StatusDescFK, - RoundTypeFK: r.RoundTypeFK, - UpdatesCount: int32(updatesCount), + StatusType: event.StatusType, + StatusDescFK: event.StatusDescFK, + RoundTypeFK: event.RoundTypeFK, LastUpdatedAt: lastUpdatedAt, - Round: r.Round, - Live: r.Live, - VenueName: r.VenueName, - LivestatsPlus: r.LivestatsPlus, - LivestatsType: r.LivestatsType, - Commentary: r.Commentary, - LineupConfirmed: r.LineupConfirmed, - Verified: r.Verified, - Spectators: r.Spectators, - GameStarted: &gameStarted, - FirstHalfEnded: &firstHalfEnded, - SecondHalfStarted: &secondHalfStarted, - SecondHalfEnded: &secondHalfEnded, - GameEnded: &gameEnded, } if _, err := s.store.CreateEnetpulseResult(ctx, createResult); err != nil { - fmt.Printf("❌ failed to store result %s: %v\n", r.ID, err) + 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 // limit to one sport if necessary + break // stop after the first sport (football) } - fmt.Println("✅ Successfully fetched and stored EnetPulse results") + fmt.Println("✅ Successfully fetched and stored EnetPulse results + participants + referees") return nil } @@ -1077,6 +961,8 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error { LastUpdatedAt: lastUpdatedAt, } + fmt.Printf("\n\nPreodds are:%v\n\n", createPreodds) + storedPreodds, err := s.store.CreateEnetpulsePreodds(ctx, createPreodds) if err != nil { continue @@ -1128,6 +1014,7 @@ func (s *Service) GetAllPreodds(ctx context.Context) ([]domain.EnetpulsePreodds, 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 } @@ -1240,18 +1127,6 @@ func (s *Service) GetFixturesWithPreodds(ctx context.Context) ([]domain.Enetpuls return fixtures, nil } -// helper to safely parse string to int32 -// func parseStringToInt32(s string) int32 { -// if s == "" { -// return 0 -// } -// i, err := strconv.Atoi(s) -// if err != nil { -// return 0 -// } -// return int32(i) -// } - func (s *Service) FetchTournamentTemplates(ctx context.Context) (*domain.TournamentTemplatesResponse, error) { url := fmt.Sprintf( "http://eapi.enetpulse.com/tournamenttemplate/list/?username=%s&token=%s", @@ -1501,9 +1376,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 != "" { @@ -1557,73 +1432,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", @@ -1636,9 +1444,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 { @@ -1774,7 +1582,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") } @@ -1786,9 +1594,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 { @@ -1859,9 +1667,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/web_server/cron.go b/internal/web_server/cron.go index 51f225f..f3aa93f 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -316,12 +316,12 @@ func StartEnetPulseCron(enetPulseSvc *enetpulse.Service, mongoLogger *zap.Logger } // 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") - } + // 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") @@ -333,12 +333,12 @@ func StartEnetPulseCron(enetPulseSvc *enetpulse.Service, mongoLogger *zap.Logger } // 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") - } + // 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") From 7575f293866468e8d0ce09f5ad52f8aef40c5e7c Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 29 Oct 2025 15:01:39 +0300 Subject: [PATCH 07/11] virtual_games wallet service fixes --- docs/docs.go | 8 +- docs/swagger.json | 8 +- docs/swagger.yaml | 6 +- .../services/virtualGame/atlas/service.go | 16 +- internal/services/virtualGame/service.go | 134 +++++----- .../virtualGame/veli/game_orchestration.go | 1 - internal/services/virtualGame/veli/service.go | 231 ++++++++---------- 7 files changed, 188 insertions(+), 216 deletions(-) delete mode 100644 internal/services/virtualGame/veli/game_orchestration.go diff --git a/docs/docs.go b/docs/docs.go index 5eddf6c..798bba4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -11176,10 +11176,8 @@ const docTemplate = `{ "tournament_name": { "type": "string" }, - "tournament_stageFK": { - "type": "string" - }, "tournament_stage_name": { + "description": "TournamentStageFK string ` + "`" + `json:\"tournament_stageFK\"` + "`" + `", "type": "string" }, "tournament_templateFK": { @@ -11381,10 +11379,8 @@ const docTemplate = `{ "tournament_name": { "type": "string" }, - "tournament_stage_fk": { - "type": "string" - }, "tournament_stage_name": { + "description": "TournamentStageFK string ` + "`" + `json:\"tournament_stage_fk\"` + "`" + `", "type": "string" }, "tournament_template_fk": { diff --git a/docs/swagger.json b/docs/swagger.json index 3823958..0cc3648 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -11168,10 +11168,8 @@ "tournament_name": { "type": "string" }, - "tournament_stageFK": { - "type": "string" - }, "tournament_stage_name": { + "description": "TournamentStageFK string `json:\"tournament_stageFK\"`", "type": "string" }, "tournament_templateFK": { @@ -11373,10 +11371,8 @@ "tournament_name": { "type": "string" }, - "tournament_stage_fk": { - "type": "string" - }, "tournament_stage_name": { + "description": "TournamentStageFK string `json:\"tournament_stage_fk\"`", "type": "string" }, "tournament_template_fk": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 66ec732..20b3cdb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -838,8 +838,7 @@ definitions: tournament_name: type: string tournament_stage_name: - type: string - tournament_stageFK: + description: TournamentStageFK string `json:"tournament_stageFK"` type: string tournament_template_name: type: string @@ -975,9 +974,8 @@ definitions: type: string tournament_name: type: string - tournament_stage_fk: - type: string tournament_stage_name: + description: TournamentStageFK string `json:"tournament_stage_fk"` type: string tournament_template_fk: type: string diff --git a/internal/services/virtualGame/atlas/service.go b/internal/services/virtualGame/atlas/service.go index 36c2c29..a4b6a50 100644 --- a/internal/services/virtualGame/atlas/service.go +++ b/internal/services/virtualGame/atlas/service.go @@ -116,7 +116,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) } @@ -157,13 +157,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) } @@ -172,7 +172,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 @@ -196,7 +196,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) } @@ -228,7 +228,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) } @@ -283,7 +283,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) } @@ -312,7 +312,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/service.go b/internal/services/virtualGame/service.go index 435e03b..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 } diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go deleted file mode 100644 index 6082148..0000000 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ /dev/null @@ -1 +0,0 @@ -package veli diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 3e2992c..324b20d 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -218,23 +218,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{ @@ -243,19 +244,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 } @@ -280,91 +281,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 { @@ -385,21 +359,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 @@ -411,7 +383,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, @@ -422,18 +394,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{ @@ -464,21 +436,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 @@ -502,7 +471,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, @@ -520,23 +489,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, @@ -545,7 +514,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, } } @@ -553,12 +522,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{ From 1e39d752395e966d63824500c24b6f1d651bd7cb Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 29 Oct 2025 15:12:06 +0300 Subject: [PATCH 08/11] payment gateways wallet service fixes --- internal/services/chapa/service.go | 30 +++++++++++++------------- internal/services/santimpay/service.go | 8 +++---- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 03cba51..95ce795 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -135,24 +135,24 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma } // Get user's wallet - wallets, err := s.walletStore.GetWalletsByUser(ctx, userID) + 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 - } - } + // 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") - } + // if withdrawWallet.ID == 0 { + // return nil, errors.New("withdrawal wallet not found") + // } // Check balance - if withdrawWallet.Balance < domain.Currency(amount) { + if float64(wallet.RegularBalance) < float64(amount) { return nil, domain.ErrInsufficientBalance } @@ -164,7 +164,7 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma Amount: domain.Currency(amount), Type: domain.WITHDRAW, SenderWalletID: domain.ValidInt64{ - Value: withdrawWallet.ID, + Value: wallet.RegularID, Valid: true, }, Status: string(domain.PaymentStatusPending), @@ -205,8 +205,8 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma 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 { + 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) } diff --git a/internal/services/santimpay/service.go b/internal/services/santimpay/service.go index 18a15b6..08fa3f0 100644 --- a/internal/services/santimpay/service.go +++ b/internal/services/santimpay/service.go @@ -134,16 +134,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, @@ -237,7 +237,7 @@ func (s *SantimPayService) ProcessDirectPayment(ctx context.Context, req domain. if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } - + // 5. Save transfer in DB transfer := domain.CreateTransfer{ Amount: domain.Currency(req.Amount), From 46d70d7c8ce96e51b2ed4953a3f78cdc529486a2 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 3 Nov 2025 17:20:35 +0300 Subject: [PATCH 09/11] chapa minor fixes --- db/migrations/000001_fortune.up.sql | 1 + db/query/enet_pulse.sql | 40 ++ db/query/transfer.sql | 3 +- gen/db/enet_pulse.sql.go | 122 +++++++ gen/db/models.go | 64 ++-- gen/db/transfer.sql.go | 42 ++- internal/config/config.go | 2 + internal/domain/chapa.go | 117 +++++- internal/domain/enet_pulse.go | 1 + internal/repository/enet_pulse.go | 67 +++- internal/services/bet/service.go | 1 + internal/services/chapa/client.go | 173 +++++++-- internal/services/chapa/port.go | 2 +- internal/services/chapa/service.go | 402 ++++++++++++++------- internal/services/enet_pulse/port.go | 1 + internal/services/enet_pulse/service.go | 113 +++--- internal/web_server/handlers/chapa.go | 172 ++++++--- internal/web_server/handlers/enet_pulse.go | 56 +++ internal/web_server/routes.go | 8 +- 19 files changed, 1069 insertions(+), 318 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6f2043c..b911279 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -253,6 +253,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/query/enet_pulse.sql b/db/query/enet_pulse.sql index 96cf090..f586d46 100644 --- a/db/query/enet_pulse.sql +++ b/db/query/enet_pulse.sql @@ -448,6 +448,46 @@ 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, 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/gen/db/enet_pulse.sql.go b/gen/db/enet_pulse.sql.go index aafaad4..f06cd69 100644 --- a/gen/db/enet_pulse.sql.go +++ b/gen/db/enet_pulse.sql.go @@ -1081,6 +1081,128 @@ func (q *Queries) GetAllEnetpulsePreoddsBettingOffers(ctx context.Context) ([]En 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 diff --git a/gen/db/models.go b/gen/db/models.go index dd3f35e..a3ff73c 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1138,38 +1138,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/internal/config/config.go b/internal/config/config.go index 73031ba..b802be0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -144,6 +144,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 @@ -262,6 +263,7 @@ func (c *Config) loadEnv() error { 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" } diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index bf6b683..1d814ee 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 @@ -69,10 +77,91 @@ type ChapaDepositVerification struct { } type ChapaVerificationResponse struct { - Status string `json:"status"` - Amount float64 `json:"amount"` - Currency string `json:"currency"` - TxRef string `json:"tx_ref"` + 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 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 { @@ -221,3 +310,13 @@ type SwapResponse struct { CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } + +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 78bcf77..00725d9 100644 --- a/internal/domain/enet_pulse.go +++ b/internal/domain/enet_pulse.go @@ -703,6 +703,7 @@ type EnetpulsePreodds struct { LastUpdatedAt time.Time CreatedAt time.Time UpdatedAt time.Time + BettingOffers []EnetpulsePreoddsBettingOffer } type EnetpulseResultParticipant struct { diff --git a/internal/repository/enet_pulse.go b/internal/repository/enet_pulse.go index 2dc99a9..4d3b2df 100644 --- a/internal/repository/enet_pulse.go +++ b/internal/repository/enet_pulse.go @@ -302,6 +302,71 @@ func (s *Store) GetAllEnetpulsePreoddsBettingOffers(ctx context.Context) ([]doma 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 { @@ -944,6 +1009,6 @@ func ConvertDBEnetpulseResultReferee(r dbgen.EnetpulseResultReferee) domain.Enet Var1RefereeFk: r.Var1RefereeFk.String, Var2RefereeFk: r.Var2RefereeFk.String, LastUpdatedAt: r.LastUpdatedAt.Time, - CreatedAt: r.CreatedAt.Time , + CreatedAt: r.CreatedAt.Time, } } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index d6ff26a..eb9ca75 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -893,6 +893,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..01c0f41 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, "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"` @@ -133,12 +134,13 @@ func (c *Client) VerifyPayment(ctx context.Context, reference string) (domain.Ch func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, 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,32 +149,24 @@ 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.ChapaVerificationResponse + 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) { @@ -215,11 +209,74 @@ func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domai return &domain.ChapaVerificationResponse{ Status: string(status), - Amount: response.Amount, - Currency: response.Currency, + // Amount: response.Amount, + // Currency: response.Currency, }, nil } +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.Bank, error) { req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil) if err != nil { @@ -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 862e3c4..e2a0667 100644 --- a/internal/services/chapa/port.go +++ b/internal/services/chapa/port.go @@ -15,7 +15,7 @@ import ( // } type ChapaStore interface { - InitializePayment(request domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) + InitializePayment(request domain.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) CreateWithdrawal(userID string, amount float64, accountNumber, bankCode string) (*domain.ChapaWithdrawal, error) diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 95ce795..5ec26fb 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -56,22 +56,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.IsTransferable { - 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 { @@ -92,9 +92,16 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma 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, @@ -103,6 +110,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 @@ -127,6 +135,157 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma return response.CheckoutURL, nil } +func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaPaymentWebhookRequest) error { + // Find payment by reference + payment, err := s.transferStore.GetTransferByReference(ctx, transfer.TxRef) + if err != nil { + return domain.ErrPaymentNotFound + } + + if payment.Verified { + return nil + } + + // Verify payment with Chapa + // verification, err := s.chapaClient.VerifyPayment(ctx, transfer.Reference) + // if err != nil { + // return fmt.Errorf("failed to verify payment: %w", err) + // } + + // Update payment status + // verified := false + // if transfer.Status == string(domain.PaymentStatusCompleted) { + // verified = true + // } + + // If payment is completed, credit user's wallet + if transfer.Status == 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.TxRef, + }, + }, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil { + return fmt.Errorf("failed to credit user wallet: %w", err) + } + } + + return nil +} + +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.ParseInt(req.Amount, 10, 64) @@ -213,124 +372,7 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma 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 { - // Find payment by reference - payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference) - if err != nil { - return domain.ErrPaymentNotFound - } - - if payment.Verified { - return nil - } - - // Verify payment with Chapa - // verification, err := s.chapaClient.VerifyPayment(ctx, transfer.Reference) - // if err != nil { - // return fmt.Errorf("failed to verify payment: %w", err) - // } - - // Update payment status - // verified := false - // if transfer.Status == string(domain.PaymentStatusCompleted) { - // verified = true - // } - - // If payment is completed, credit user's wallet - if transfer.Status == string(domain.PaymentStatusSuccessful) { - - if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { - return fmt.Errorf("failed to update payment status: %w", err) - } - - if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{ - ReferenceNumber: domain.ValidString{ - Value: transfer.Reference, - }, - }, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil { - return fmt.Errorf("failed to credit user wallet: %w", err) - } - } - - return nil -} - -func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error { +func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error { // Find payment by reference transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference) if err != nil { @@ -369,15 +411,111 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai return nil } -func (s *Service) GetPaymentReceiptURL(ctx context.Context, chapaRef string) (string, error) { - if chapaRef == "" { - return "", fmt.Errorf("chapa reference ID is required") +func (s *Service) GetPaymentReceiptURL(refId string) (string, error) { + if refId == "" { + return "", fmt.Errorf("reference ID cannot be empty") } - receiptURL := fmt.Sprintf("https://chapa.link/payment-receipt/%s", chapaRef) + receiptURL := s.cfg.CHAPA_RECEIPT_URL + refId return receiptURL, 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 already verified, just return a completed response + if transfer.Verified { + return &domain.ChapaVerificationResponse{}, 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 *domain.ChapaVerificationResponse + + 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) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.chapa.co/v1/transfers", nil) if err != 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 2fa4304..fd9b7d2 100644 --- a/internal/services/enet_pulse/service.go +++ b/internal/services/enet_pulse/service.go @@ -872,6 +872,7 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error { 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, @@ -896,6 +897,7 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error { continue } + // Struct adjusted exactly to match JSON structure var preoddsResp struct { Preodds map[string]struct { ID string `json:"id"` @@ -910,17 +912,18 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error { Sparam string `json:"sparam"` N string `json:"n"` UT string `json:"ut"` - BettingOffers []struct { - ID string `json:"id"` - 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:"bettingoffers"` + + 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"` } @@ -929,65 +932,53 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error { } for _, p := range preoddsResp.Preodds { - updatesCount := 0 - if p.N != "" { - if n, err := strconv.Atoi(p.N); err == nil { - updatesCount = n - } - } - - lastUpdatedAt, _ := time.Parse(time.RFC3339, p.UT) - - eventParticipantNumber := int32(0) - if p.EventParticipantNumber != "" { - if epn, err := strconv.Atoi(p.EventParticipantNumber); err == nil { - eventParticipantNumber = int32(epn) - } - } + // 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: outcome.OutcomeTypeID, - OutcomeScopeFK: string(p.OutcomeScopeFK), - OutcomeSubtypeFK: string(p.OutcomeSubtypeFK), - EventParticipantNumber: int(eventParticipantNumber), + 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: int(updatesCount), + UpdatesCount: updatesCount, LastUpdatedAt: lastUpdatedAt, } - fmt.Printf("\n\nPreodds are:%v\n\n", createPreodds) - - storedPreodds, err := s.store.CreateEnetpulsePreodds(ctx, createPreodds) + // Store preodds in DB + _, err := s.store.CreateEnetpulsePreodds(ctx, createPreodds) if err != nil { continue } - for _, o := range p.BettingOffers { - bettingUpdates := 0 - if o.N != "" { - if n, err := strconv.Atoi(o.N); err == nil { - bettingUpdates = n - } - } + // 5️⃣ Loop through betting offers map + for _, o := range p.PreoddsBettingOffers { + bettingUpdates, _ := strconv.Atoi(defaultIfEmpty(o.N, "0")) + bettingLastUpdatedAt := parseTimeOrNow(o.UT) - bettingLastUpdatedAt, _ := time.Parse(time.RFC3339, 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: storedPreodds.PreoddsID, - BettingOfferStatusFK: o.BettingOfferStatusFK, - OddsProviderFK: o.OddsProviderFK, - Odds: o.Odds, - OddsOld: o.OddsOld, + PreoddsFK: createPreodds.PreoddsID, + BettingOfferStatusFK: int32(bettingOfferStatusFK), + OddsProviderFK: int32(oddsProviderFK), + Odds: odds, + OddsOld: oddsOld, Active: o.Active, CouponKey: o.CouponKey, - UpdatesCount: int(bettingUpdates), + UpdatesCount: bettingUpdates, LastUpdatedAt: bettingLastUpdatedAt, } @@ -1000,6 +991,23 @@ func (s *Service) FetchAndStorePreodds(ctx context.Context) error { 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 == "" { @@ -1117,6 +1125,15 @@ func (s *Service) GetAllBettingOffers(ctx context.Context) ([]domain.EnetpulsePr 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) diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index f260bc5..c671dbb 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -39,7 +39,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 +51,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, @@ -114,16 +80,16 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error { switch chapaTransactionType.Type { case h.Cfg.CHAPA_PAYMENT_TYPE: - chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer) + chapaTransferVerificationRequest := new(domain.ChapaPaymentWebhookRequest) if err := c.BodyParser(chapaTransferVerificationRequest); err != nil { return domain.UnProcessableEntityResponse(c) } - err := h.chapaSvc.HandleVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest) + err := h.chapaSvc.ProcessVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to verify Chapa depposit", + Message: "Failed to verify Chapa deposit", Error: err.Error(), }) } @@ -140,7 +106,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error { return domain.UnProcessableEntityResponse(c) } - err := h.chapaSvc.HandleVerifyWithdrawWebhook(c.Context(), *chapaPaymentVerificationRequest) + err := h.chapaSvc.ProcessVerifyWithdrawWebhook(c.Context(), *chapaPaymentVerificationRequest) if err != nil { return domain.UnExpectedErrorResponse(c) } @@ -161,6 +127,120 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error { }) } +// 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, + }) +} + // VerifyPayment godoc // @Summary Verify a payment manually // @Description Manually verify a payment using Chapa's API @@ -171,7 +251,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error { // @Success 200 {object} domain.ChapaVerificationResponse // @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 +269,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, }) } @@ -284,7 +364,7 @@ func (h *Handler) GetPaymentReceipt(c *fiber.Ctx) error { }) } - receiptURL, err := h.chapaSvc.GetPaymentReceiptURL(c.Context(), chapaRef) + receiptURL, err := h.chapaSvc.GetPaymentReceiptURL(chapaRef) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to get Chapa payment receipt", diff --git a/internal/web_server/handlers/enet_pulse.go b/internal/web_server/handlers/enet_pulse.go index 7bd3616..e89c7b2 100644 --- a/internal/web_server/handlers/enet_pulse.go +++ b/internal/web_server/handlers/enet_pulse.go @@ -205,6 +205,62 @@ func (h *Handler) GetAllPreodds(c *fiber.Ctx) error { }) } +// 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 diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 27f8099..01c70aa 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -292,7 +292,8 @@ func (a *App) initAppRoutes() { groupV1.Get("/tournament_stages", h.GetAllTournamentStages) groupV1.Get("/fixtures", h.GetFixturesByDate) groupV1.Get("/results", h.GetAllResults) - groupV1.Get("/preodds", h.GetAllPreodds) + groupV1.Get("/preodds", h.GetAllPreoddsWithBettingOffers) + groupV1.Get("/bettingoffers", h.GetAllBettingOffers) groupV1.Get("/fixtures/preodds", h.GetFixturesWithPreodds) // Leagues @@ -380,7 +381,10 @@ 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", h.ManualVerifyTransaction) + groupV1.Put("/chapa/transaction/cancel/:tx_ref", a.authMiddleware, h.CancelDeposit) + groupV1.Get("/chapa/transactions", h.FetchAllTransactions) + groupV1.Get("/chapa/transaction/events/:ref_id", 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) From 4fdc76280ad451fecd6c8820e7f13501929e0b4a Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Mon, 3 Nov 2025 17:39:16 +0300 Subject: [PATCH 10/11] chapa minor fixes swagger --- docs/docs.go | 538 +++++++++++++++++++++++++++++++++++++++++----- docs/swagger.json | 538 +++++++++++++++++++++++++++++++++++++++++----- docs/swagger.yaml | 349 ++++++++++++++++++++++++++---- 3 files changed, 1285 insertions(+), 140 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 798bba4..6251fd1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2208,50 +2208,6 @@ const docTemplate = `{ } } }, - "/api/v1/chapa/payments/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.ChapaVerificationResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, "/api/v1/chapa/payments/receipt/{chapa_ref}": { "get": { "description": "Retrieve the Chapa payment receipt URL using the reference ID", @@ -2452,6 +2408,189 @@ const docTemplate = `{ } } }, + "/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.ChapaVerificationResponse" + } + }, + "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", @@ -3216,6 +3355,50 @@ 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", @@ -3348,6 +3531,50 @@ const docTemplate = `{ } } }, + "/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", @@ -10630,6 +10857,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": [ @@ -10652,19 +10925,128 @@ 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.ChapaTransactionEvent": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "item": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.ChapaVerificationResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "charge": { + "type": "number" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "customization": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "logo": {}, + "title": { + "type": "string" + } + } + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "meta": {}, + "method": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tx_ref": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "message": { + "type": "string" + }, + "status": { "type": "string" } } @@ -10675,13 +11057,15 @@ const docTemplate = `{ "amount": { "type": "integer" }, - "currency": { - "type": "string" - }, "status": { - "$ref": "#/definitions/domain.PaymentStatus" + "description": "Currency string ` + "`" + `json:\"currency\"` + "`" + `", + "allOf": [ + { + "$ref": "#/definitions/domain.PaymentStatus" + } + ] }, - "tx_ref": { + "trx_ref": { "type": "string" } } @@ -11251,6 +11635,12 @@ const docTemplate = `{ "domain.EnetpulsePreodds": { "type": "object", "properties": { + "bettingOffers": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreoddsBettingOffer" + } + }, "createdAt": { "type": "string" }, @@ -11301,6 +11691,50 @@ const docTemplate = `{ } } }, + "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": { diff --git a/docs/swagger.json b/docs/swagger.json index 0cc3648..42e5c28 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2200,50 +2200,6 @@ } } }, - "/api/v1/chapa/payments/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.ChapaVerificationResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - } - }, "/api/v1/chapa/payments/receipt/{chapa_ref}": { "get": { "description": "Retrieve the Chapa payment receipt URL using the reference ID", @@ -2444,6 +2400,189 @@ } } }, + "/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.ChapaVerificationResponse" + } + }, + "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", @@ -3208,6 +3347,50 @@ } } }, + "/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", @@ -3340,6 +3523,50 @@ } } }, + "/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", @@ -10622,6 +10849,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": [ @@ -10644,19 +10917,128 @@ } } }, - "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.ChapaTransactionEvent": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "item": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "domain.ChapaVerificationResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "amount": { + "type": "number" + }, + "charge": { + "type": "number" + }, + "created_at": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "customization": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "logo": {}, + "title": { + "type": "string" + } + } + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "meta": {}, + "method": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tx_ref": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "message": { + "type": "string" + }, + "status": { "type": "string" } } @@ -10667,13 +11049,15 @@ "amount": { "type": "integer" }, - "currency": { - "type": "string" - }, "status": { - "$ref": "#/definitions/domain.PaymentStatus" + "description": "Currency string `json:\"currency\"`", + "allOf": [ + { + "$ref": "#/definitions/domain.PaymentStatus" + } + ] }, - "tx_ref": { + "trx_ref": { "type": "string" } } @@ -11243,6 +11627,12 @@ "domain.EnetpulsePreodds": { "type": "object", "properties": { + "bettingOffers": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.EnetpulsePreoddsBettingOffer" + } + }, "createdAt": { "type": "string" }, @@ -11293,6 +11683,50 @@ } } }, + "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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 20b3cdb..d439353 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -466,6 +466,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: @@ -480,26 +510,98 @@ 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.ChapaTransactionEvent: + properties: + created_at: + type: string + item: + type: integer + message: + type: string + type: + type: string + updated_at: + type: string + type: object + domain.ChapaVerificationResponse: + properties: + data: + properties: + amount: + type: number + charge: + type: number + created_at: + type: string + currency: + type: string + customization: + properties: + description: + type: string + logo: {} + title: + type: string + type: object + email: + type: string + first_name: + type: string + last_name: + type: string + meta: {} + method: + type: string + mode: + type: string + reference: + type: string + status: + type: string + tx_ref: + type: string + type: + type: string + updated_at: + type: string + type: object + message: + type: string + status: type: string type: object domain.ChapaWebhookPayload: properties: amount: type: integer - currency: - type: string status: - $ref: '#/definitions/domain.PaymentStatus' - tx_ref: + allOf: + - $ref: '#/definitions/domain.PaymentStatus' + description: Currency string `json:"currency"` + trx_ref: type: string type: object domain.ChapaWithdrawalRequest: @@ -889,6 +991,10 @@ definitions: type: object domain.EnetpulsePreodds: properties: + bettingOffers: + items: + $ref: '#/definitions/domain.EnetpulsePreoddsBettingOffer' + type: array createdAt: type: string dparam: @@ -922,6 +1028,35 @@ definitions: 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: @@ -5621,35 +5756,6 @@ paths: summary: Initiate a deposit tags: - Chapa - /api/v1/chapa/payments/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.ChapaVerificationResponse' - "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/payments/receipt/{chapa_ref}: get: consumes: @@ -5781,6 +5887,124 @@ paths: summary: Initiate a currency swap 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.ChapaVerificationResponse' + "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: @@ -6279,6 +6503,32 @@ 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: @@ -6358,6 +6608,33 @@ paths: 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: From d654d5f2ef8f6ca43cdd0f29ab2be023a0dd7b45 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Thu, 6 Nov 2025 16:37:41 +0300 Subject: [PATCH 11/11] veli games fixes --- .../000004_virtual_game_Session.up.sql | 5 +- db/query/virtual_games.sql | 44 +- docs/docs.go | 187 +++------ docs/swagger.json | 187 +++------ docs/swagger.yaml | 139 +++---- gen/db/models.go | 3 - gen/db/virtual_games.sql.go | 93 ++--- internal/config/config.go | 2 + internal/domain/arifpay.go | 12 +- internal/domain/chapa.go | 168 +++++--- internal/domain/veli_games.go | 12 +- internal/repository/virtual_game.go | 50 ++- internal/services/arifpay/service.go | 377 ++++++++++++------ internal/services/chapa/client.go | 24 +- internal/services/chapa/port.go | 6 +- internal/services/chapa/service.go | 241 +++++++---- internal/services/virtualGame/Alea/service.go | 14 +- internal/services/virtualGame/veli/service.go | 18 +- internal/web_server/handlers/arifpay.go | 49 ++- internal/web_server/handlers/chapa.go | 127 +++--- internal/web_server/handlers/veli_games.go | 3 + internal/web_server/routes.go | 22 +- 22 files changed, 967 insertions(+), 816 deletions(-) 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/query/virtual_games.sql b/db/query/virtual_games.sql index 8bee846..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, diff --git a/docs/docs.go b/docs/docs.go index 6251fd1..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": [ @@ -2254,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" ], @@ -2272,7 +2272,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ChapaWebhookPayload" + "$ref": "#/definitions/domain.ChapaWebhookPayment" } } ], @@ -2280,8 +2280,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -2329,7 +2328,7 @@ const docTemplate = `{ } ], "responses": { - "201": { + "200": { "description": "Chapa withdrawal process initiated successfully", "schema": { "$ref": "#/definitions/domain.Response" @@ -2364,7 +2363,7 @@ const docTemplate = `{ }, "/api/v1/chapa/swap": { "post": { - "description": "Perform a USD to ETB currency swap using Chapa's API", + "description": "Convert an amount from one currency to another using Chapa's currency swap API", "consumes": [ "application/json" ], @@ -2374,11 +2373,11 @@ const docTemplate = `{ "tags": [ "Chapa" ], - "summary": "Initiate a currency swap", + "summary": "Swap currency using Chapa API", "parameters": [ { - "description": "Swap Request Payload", - "name": "payload", + "description": "Swap request payload", + "name": "request", "in": "body", "required": true, "schema": { @@ -2536,7 +2535,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.ChapaVerificationResponse" + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -10229,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": { @@ -10980,92 +10957,75 @@ const docTemplate = `{ } } }, - "domain.ChapaVerificationResponse": { + "domain.ChapaWebhookCustomization": { "type": "object", "properties": { - "data": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "charge": { - "type": "number" - }, - "created_at": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "customization": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "logo": {}, - "title": { - "type": "string" - } - } - }, - "email": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "meta": {}, - "method": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "reference": { - "type": "string" - }, - "status": { - "type": "string" - }, - "tx_ref": { - "type": "string" - }, - "type": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "message": { + "description": { "type": "string" }, - "status": { + "logo": { + "type": "string" + }, + "title": { "type": "string" } } }, - "domain.ChapaWebhookPayload": { + "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": { - "description": "Currency string ` + "`" + `json:\"currency\"` + "`" + `", - "allOf": [ - { - "$ref": "#/definitions/domain.PaymentStatus" - } - ] + "type": "string" }, - "trx_ref": { + "tx_ref": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { "type": "string" } } @@ -12884,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": { diff --git a/docs/swagger.json b/docs/swagger.json index 42e5c28..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": [ @@ -2246,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" ], @@ -2264,7 +2264,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.ChapaWebhookPayload" + "$ref": "#/definitions/domain.ChapaWebhookPayment" } } ], @@ -2272,8 +2272,7 @@ "200": { "description": "OK", "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -2321,7 +2320,7 @@ } ], "responses": { - "201": { + "200": { "description": "Chapa withdrawal process initiated successfully", "schema": { "$ref": "#/definitions/domain.Response" @@ -2356,7 +2355,7 @@ }, "/api/v1/chapa/swap": { "post": { - "description": "Perform a USD to ETB currency swap using Chapa's API", + "description": "Convert an amount from one currency to another using Chapa's currency swap API", "consumes": [ "application/json" ], @@ -2366,11 +2365,11 @@ "tags": [ "Chapa" ], - "summary": "Initiate a currency swap", + "summary": "Swap currency using Chapa API", "parameters": [ { - "description": "Swap Request Payload", - "name": "payload", + "description": "Swap request payload", + "name": "request", "in": "body", "required": true, "schema": { @@ -2528,7 +2527,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.ChapaVerificationResponse" + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -10221,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": { @@ -10972,92 +10949,75 @@ } } }, - "domain.ChapaVerificationResponse": { + "domain.ChapaWebhookCustomization": { "type": "object", "properties": { - "data": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "charge": { - "type": "number" - }, - "created_at": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "customization": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "logo": {}, - "title": { - "type": "string" - } - } - }, - "email": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "meta": {}, - "method": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "reference": { - "type": "string" - }, - "status": { - "type": "string" - }, - "tx_ref": { - "type": "string" - }, - "type": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "message": { + "description": { "type": "string" }, - "status": { + "logo": { + "type": "string" + }, + "title": { "type": "string" } } }, - "domain.ChapaWebhookPayload": { + "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": { - "description": "Currency string `json:\"currency\"`", - "allOf": [ - { - "$ref": "#/definitions/domain.PaymentStatus" - } - ] + "type": "string" }, - "trx_ref": { + "tx_ref": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { "type": "string" } } @@ -12876,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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d439353..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: @@ -546,62 +531,52 @@ definitions: updated_at: type: string type: object - domain.ChapaVerificationResponse: + domain.ChapaWebhookCustomization: properties: - data: - properties: - amount: - type: number - charge: - type: number - created_at: - type: string - currency: - type: string - customization: - properties: - description: - type: string - logo: {} - title: - type: string - type: object - email: - type: string - first_name: - type: string - last_name: - type: string - meta: {} - method: - type: string - mode: - type: string - reference: - type: string - status: - type: string - tx_ref: - type: string - type: - type: string - updated_at: - type: string - type: object - message: + description: type: string - status: + logo: + type: string + title: type: string type: object - domain.ChapaWebhookPayload: + 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: - allOf: - - $ref: '#/definitions/domain.PaymentStatus' - description: Currency string `json:"currency"` - trx_ref: + type: string + tx_ref: + type: string + type: + type: string + updated_at: type: string type: object domain.ChapaWithdrawalRequest: @@ -1844,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: @@ -4608,7 +4571,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/domain.ArifpayB2CRequest' + $ref: '#/definitions/domain.CheckoutSessionClientRequest' produces: - application/json responses: @@ -4695,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 @@ -5789,22 +5752,21 @@ paths: 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: @@ -5832,7 +5794,7 @@ paths: produces: - application/json responses: - "201": + "200": description: Chapa withdrawal process initiated successfully schema: $ref: '#/definitions/domain.Response' @@ -5861,11 +5823,12 @@ paths: post: consumes: - application/json - description: Perform a USD to ETB currency swap using Chapa's API + description: Convert an amount from one currency to another using Chapa's currency + swap API parameters: - - description: Swap Request Payload + - description: Swap request payload in: body - name: payload + name: request required: true schema: $ref: '#/definitions/domain.SwapRequest' @@ -5884,7 +5847,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: Initiate a currency swap + summary: Swap currency using Chapa API tags: - Chapa /api/v1/chapa/transaction/cancel/{tx_ref}: @@ -5970,7 +5933,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/domain.ChapaVerificationResponse' + $ref: '#/definitions/domain.Response' "400": description: Bad Request schema: diff --git a/gen/db/models.go b/gen/db/models.go index a3ff73c..474e91f 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1093,11 +1093,8 @@ type VirtualGameSession struct { 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 { diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index a9e8bec..1e39f92 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -409,54 +409,36 @@ func (q *Queries) CreateVirtualGameReport(ctx context.Context, arg CreateVirtual 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 } @@ -751,11 +733,8 @@ 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 ` @@ -768,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 } @@ -1117,23 +1119,6 @@ func (q *Queries) UpdateVirtualGameProviderReportByDate(ctx context.Context, arg return err } -const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec -UPDATE virtual_game_sessions -SET status = $2, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1 -` - -type UpdateVirtualGameSessionStatusParams struct { - ID int64 `json:"id"` - Status string `json:"status"` -} - -func (q *Queries) UpdateVirtualGameSessionStatus(ctx context.Context, arg UpdateVirtualGameSessionStatusParams) error { - _, err := q.db.Exec(ctx, UpdateVirtualGameSessionStatus, arg.ID, arg.Status) - return err -} - const UpdateVirtualGameTransactionStatus = `-- name: UpdateVirtualGameTransactionStatus :exec UPDATE virtual_game_transactions SET status = $2, diff --git a/internal/config/config.go b/internal/config/config.go index b802be0..717cc19 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -136,6 +136,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 @@ -259,6 +260,7 @@ 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") 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/chapa.go b/internal/domain/chapa.go index 1d814ee..ef749d9 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -76,7 +76,7 @@ type ChapaDepositVerification struct { Currency string } -type ChapaVerificationResponse struct { +type ChapaPaymentVerificationResponse struct { Message string `json:"message"` Status string `json:"status"` Data struct { @@ -103,6 +103,31 @@ type ChapaVerificationResponse struct { } `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"` @@ -182,6 +207,57 @@ type ChapaCustomer 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"` @@ -246,44 +322,49 @@ 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 { @@ -298,19 +379,6 @@ type SwapRequest struct { Amount float64 `json:"amount"` } -type SwapResponse 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"` -} - type ChapaCancelResponse struct { Message string `json:"message"` Status string `json:"status"` 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/repository/virtual_game.go b/internal/repository/virtual_game.go index f970a9d..e7befef 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -21,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 @@ -166,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 { @@ -187,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{ diff --git a/internal/services/arifpay/service.go b/internal/services/arifpay/service.go index a074d98..37c3c08 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" @@ -31,15 +32,15 @@ func NewArifpayService(cfg *config.Config, transferStore wallet.TransferStore, w } } -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 } @@ -129,6 +130,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 { @@ -138,7 +143,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) @@ -176,17 +181,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 } @@ -196,7 +203,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 // } @@ -212,10 +219,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{ @@ -231,35 +238,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") @@ -267,11 +333,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)) } @@ -282,109 +372,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, @@ -394,55 +408,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, @@ -451,30 +478,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/chapa/client.go b/internal/services/chapa/client.go index 01c0f41..764c273 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -37,7 +37,7 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaInitDepo "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, } @@ -131,7 +131,7 @@ 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, http.MethodGet, url, nil) @@ -153,7 +153,7 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) } - var verification domain.ChapaVerificationResponse + var verification domain.ChapaPaymentVerificationResponse if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } @@ -169,7 +169,7 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain 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) @@ -207,8 +207,8 @@ func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domai status = domain.PaymentStatusFailed } - return &domain.ChapaVerificationResponse{ - Status: string(status), + return &domain.ChapaTransferVerificationResponse{ + Status: string(status), // Amount: response.Amount, // Currency: response.Currency, }, nil @@ -277,7 +277,7 @@ func (c *Client) GetTransactionEvents(ctx context.Context, refId string) ([]doma return response.Data, nil } -func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) { +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) @@ -300,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, @@ -324,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) @@ -361,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) @@ -385,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) } diff --git a/internal/services/chapa/port.go b/internal/services/chapa/port.go index e2a0667..1739482 100644 --- a/internal/services/chapa/port.go +++ b/internal/services/chapa/port.go @@ -16,11 +16,11 @@ import ( type ChapaStore interface { InitializePayment(request domain.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error) - ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, 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) diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 5ec26fb..2c64ba0 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -3,6 +3,9 @@ package chapa import ( "bytes" "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -43,6 +46,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 @@ -88,7 +116,7 @@ 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, @@ -135,9 +163,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma return response.CheckoutURL, nil } -func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaPaymentWebhookRequest) error { +func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, req domain.ChapaWebhookPayment) error { // Find payment by reference - payment, err := s.transferStore.GetTransferByReference(ctx, transfer.TxRef) + payment, err := s.transferStore.GetTransferByReference(ctx, req.TxRef) if err != nil { return domain.ErrPaymentNotFound } @@ -159,7 +187,7 @@ func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer doma // } // If payment is completed, credit user's wallet - if transfer.Status == 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) @@ -171,7 +199,7 @@ func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, transfer doma if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{ ReferenceNumber: domain.ValidString{ - Value: transfer.TxRef, + 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) @@ -288,7 +316,7 @@ func (s *Service) FetchTransactionEvents(ctx context.Context, refID string) ([]d 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) + amount, err := strconv.ParseFloat(req.Amount, 64) if err != nil || amount <= 0 { return nil, domain.ErrInvalidWithdrawalAmount } @@ -319,7 +347,7 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma reference := uuid.New().String() createTransfer := domain.CreateTransfer{ - Message: fmt.Sprintf("Withdrawing %d into wallet using chapa. Reference Number %s", amount, reference), + Message: fmt.Sprintf("Withdrawing %f into wallet using chapa. Reference Number %s", amount, reference), Amount: domain.Currency(amount), Type: domain.WITHDRAW, SenderWalletID: domain.ValidInt64{ @@ -341,40 +369,49 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma transferReq := domain.ChapaWithdrawalRequest{ AccountName: req.AccountName, AccountNumber: req.AccountNumber, - Amount: fmt.Sprintf("%d", amount), + Amount: fmt.Sprintf("%f", 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 := 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, payment domain.ChapaWebHookPayment) error { +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 } @@ -395,7 +432,7 @@ func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, payment doma // 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 @@ -420,15 +457,7 @@ func (s *Service) GetPaymentReceiptURL(refId string) (string, error) { return receiptURL, 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) { +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 { @@ -437,7 +466,7 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha // If already verified, just return a completed response if transfer.Verified { - return &domain.ChapaVerificationResponse{}, errors.New("transfer already verified") + return map[string]any{}, errors.New("transfer already verified") } // Validate sender wallet @@ -445,12 +474,12 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID) } - var verification *domain.ChapaVerificationResponse + var verification any switch strings.ToLower(string(transfer.Type)) { case string(domain.DEPOSIT): // Verify Chapa payment - verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef) + verification, err := s.chapaClient.ManualVerifyPayment(ctx, txRef) if err != nil { return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err) } @@ -481,7 +510,7 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha case string(domain.WITHDRAW): // Verify Chapa transfer - verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef) + verification, err := s.chapaClient.ManualVerifyTransfer(ctx, txRef) if err != nil { return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err) } @@ -516,8 +545,16 @@ func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.Cha return verification, nil } -func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.chapa.co/v1/transfers", 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) } @@ -535,26 +572,22 @@ func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error 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.Transfer `json:"data"` - } - + 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 result.Data, nil + // 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) { - baseURL := "https://api.chapa.co/v1/balances" + URL := s.cfg.CHAPA_BASE_URL + "/balances" if currencyCode != "" { - baseURL = fmt.Sprintf("%s/%s", baseURL, strings.ToLower(currencyCode)) + URL = fmt.Sprintf("%s/%s", URL, strings.ToLower(currencyCode)) } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL, nil) if err != nil { return nil, fmt.Errorf("failed to create balance request: %w", err) } @@ -585,59 +618,103 @@ func (s *Service) GetAccountBalance(ctx context.Context, currencyCode string) ([ return result.Data, 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") - } +func (s *Service) SwapCurrency(ctx context.Context, reqBody domain.SwapRequest) (*domain.SwapResponse, error) { + URL := s.cfg.CHAPA_BASE_URL + "/swap" - payload := domain.SwapRequest{ - Amount: amount, - From: strings.ToUpper(from), - To: strings.ToUpper(to), - } + // Normalize currency codes + reqBody.From = strings.ToUpper(reqBody.From) + reqBody.To = strings.ToUpper(reqBody.To) - // payload := map[string]any{ - // "amount": amount, - // "from": strings.ToUpper(from), - // "to": strings.ToUpper(to), - // } - - body, err := json.Marshal(payload) + // Marshal request body + body, err := json.Marshal(reqBody) if err != nil { - return nil, fmt.Errorf("failed to encode swap payload: %w", err) + return nil, fmt.Errorf("failed to marshal swap payload: %w", err) } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.chapa.co/v1/swap", bytes.NewBuffer(body)) + // 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("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY)) 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() - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + // 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)) } - var result struct { - Message string `json:"message"` - Status string `json:"status"` - Data domain.SwapResponse `json:"data"` - } - + // 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.Data, nil + 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/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/veli/service.go b/internal/services/virtualGame/veli/service.go index 324b20d..fdf6fa3 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -15,6 +15,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" ) @@ -167,7 +168,7 @@ 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, } @@ -178,6 +179,21 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (* 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 } 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/chapa.go b/internal/web_server/handlers/chapa.go index c671dbb..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" @@ -61,69 +63,66 @@ 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.ChapaPaymentWebhookRequest) + // 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.ProcessVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest) - if 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 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.ProcessVerifyWithdrawWebhook(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, }) } @@ -248,7 +247,7 @@ func (h *Handler) GetTransactionEvents(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/transaction/manual/verify/{tx_ref} [get] @@ -311,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" @@ -336,9 +335,9 @@ 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, }) @@ -430,44 +429,56 @@ func (h *Handler) GetAccountBalance(c *fiber.Ctx) error { } return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Chapa account balance retrieved successfully", + Message: "Chapa account balances retrieved successfully", Data: balances, StatusCode: fiber.StatusOK, Success: true, }) } -// InitiateSwap godoc -// @Summary Initiate a currency swap -// @Description Perform a USD to ETB currency swap using Chapa's API +// 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 payload body domain.SwapRequest true "Swap Request Payload" +// @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) InitiateSwap(c *fiber.Ctx) error { - var req domain.SwapRequest - if err := c.BodyParser(&req); err != nil { +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(), }) } - swapResult, err := h.chapaSvc.InitiateSwap(c.Context(), req.Amount, req.From, req.To) + // 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 initiate currency swap", + Message: "Failed to perform currency swap", Error: err.Error(), }) } + // Success response return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Currency swap initiated successfully", - Data: swapResult, + Message: "Currency swapped successfully", + Data: resp, StatusCode: fiber.StatusOK, Success: true, }) diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index 5bf3c1b..607ac06 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -135,7 +135,10 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { req.BrandID = h.Cfg.VeliGames.BrandID } + 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) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 01c70aa..21ee01f 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -150,9 +150,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) @@ -381,17 +381,17 @@ func (a *App) initAppRoutes() { //Chapa Routes groupV1.Post("/chapa/payments/webhook/verify", h.WebhookCallback) - groupV1.Get("/chapa/transaction/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", h.FetchAllTransactions) - groupV1.Get("/chapa/transaction/events/:ref_id", h.GetTransactionEvents) + 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", h.GetPaymentReceipt) - groupV1.Get("/chapa/transfers", h.GetAllTransfers) - groupV1.Get("/chapa/balance", h.GetAccountBalance) - groupV1.Post("/chapa/init-swap", h.InitiateSwap) + 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) @@ -409,7 +409,7 @@ func (a *App) initAppRoutes() { //Veli Virtual Game Routes groupV1.Post("/veli/providers", h.GetProviders) groupV1.Post("/veli/games-list", h.GetGamesByProvider) - groupV1.Post("/veli/start-game", h.StartGame) + groupV1.Post("/veli/start-game", a.authMiddleware, h.StartGame) groupV1.Post("/veli/start-demo-game", h.StartDemoGame) a.fiber.Post("/balance", h.GetBalance) groupV1.Post("/veli/gaming-activity", a.authMiddleware, h.GetGamingActivity)