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)