Merge remote-tracking branch 'origin/virtual_games'

This commit is contained in:
Samuel Tariku 2025-11-06 18:28:50 +03:00
commit ab1d85897e
61 changed files with 10756 additions and 2349 deletions

View File

@ -60,6 +60,7 @@ import (
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/orchestration"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor"
@ -251,6 +252,12 @@ func main() {
aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger)
veliCLient := veli.NewClient(cfg, walletSvc) veliCLient := veli.NewClient(cfg, walletSvc)
veliVirtualGameService := veli.New(virtualGameSvc, vitualGameRepo, veliCLient, walletSvc, repository.NewTransferStore(store), domain.MongoDBLogger, cfg) veliVirtualGameService := veli.New(virtualGameSvc, vitualGameRepo, veliCLient, walletSvc, repository.NewTransferStore(store), domain.MongoDBLogger, cfg)
orchestrationSvc := orchestration.New(
virtualGameSvc,
virtuaGamesRepo,
cfg,
veliCLient,
)
atlasClient := atlas.NewClient(cfg, walletSvc) atlasClient := atlas.NewClient(cfg, walletSvc)
atlasVirtualGameService := atlas.New(virtualGameSvc, vitualGameRepo, atlasClient, walletSvc, repository.NewTransferStore(store), cfg) atlasVirtualGameService := atlas.New(virtualGameSvc, vitualGameRepo, atlasClient, walletSvc, repository.NewTransferStore(store), cfg)
recommendationSvc := recommendation.NewService(recommendationRepo) recommendationSvc := recommendation.NewService(recommendationRepo)
@ -301,8 +308,8 @@ func main() {
store, store,
) )
go httpserver.SetupReportandVirtualGameCronJobs(context.Background(), reportSvc, veliVirtualGameService, "C:/Users/User/Desktop")
go httpserver.StartEnetPulseCron(enePulseSvc, domain.MongoDBLogger) go httpserver.StartEnetPulseCron(enePulseSvc, domain.MongoDBLogger)
go httpserver.SetupReportandVirtualGameCronJobs(context.Background(), reportSvc, orchestrationSvc, "C:/Users/User/Desktop")
go httpserver.ProcessBetCashback(context.TODO(), betSvc) go httpserver.ProcessBetCashback(context.TODO(), betSvc)
bankRepository := repository.NewBankRepository(store) bankRepository := repository.NewBankRepository(store)
@ -339,9 +346,9 @@ func main() {
fixerFertcherSvc, fixerFertcherSvc,
) )
exchangeWorker := currency.NewExchangeRateWorker(fixerFertcherSvc, logger, cfg) // exchangeWorker := currency.NewExchangeRateWorker(fixerFertcherSvc, logger, cfg)
exchangeWorker.Start(context.Background()) // exchangeWorker.Start(context.Background())
defer exchangeWorker.Stop() // defer exchangeWorker.Stop()
go walletMonitorSvc.Start() go walletMonitorSvc.Start()
httpserver.StartBetAPIDataFetchingCrons(eventSvc, *oddsSvc, resultSvc, domain.MongoDBLogger) httpserver.StartBetAPIDataFetchingCrons(eventSvc, *oddsSvc, resultSvc, domain.MongoDBLogger)
@ -369,6 +376,7 @@ func main() {
enetPulseSvc, enetPulseSvc,
atlasVirtualGameService, atlasVirtualGameService,
veliVirtualGameService, veliVirtualGameService,
orchestrationSvc,
telebirrSvc, telebirrSvc,
arifpaySvc, arifpaySvc,
santimpaySvc, santimpaySvc,

View File

@ -36,9 +36,27 @@ CREATE TABLE IF NOT EXISTS virtual_game_providers (
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ updated_at TIMESTAMPTZ
); );
CREATE TABLE IF NOT EXISTS virtual_game_provider_reports (
id BIGSERIAL PRIMARY KEY,
provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE,
report_date DATE NOT NULL,
total_games_played BIGINT DEFAULT 0,
total_bets NUMERIC(18,2) DEFAULT 0,
total_payouts NUMERIC(18,2) DEFAULT 0,
total_profit NUMERIC(18,2) GENERATED ALWAYS AS (total_bets - total_payouts) STORED,
total_players BIGINT DEFAULT 0,
report_type VARCHAR(50) DEFAULT 'daily',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_provider_report
ON virtual_game_provider_reports (provider_id, report_date, report_type);
CREATE TABLE IF NOT EXISTS virtual_games ( CREATE TABLE IF NOT EXISTS virtual_games (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
game_id VARCHAR(150) NOT NULL, game_id VARCHAR(150) UNIQUE NOT NULL,
provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE, provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
category VARCHAR(100), category VARCHAR(100),
@ -54,6 +72,25 @@ CREATE TABLE IF NOT EXISTS virtual_games (
updated_at TIMESTAMPTZ updated_at TIMESTAMPTZ
); );
CREATE UNIQUE INDEX IF NOT EXISTS ux_virtual_games_provider_game ON virtual_games (provider_id, game_id); CREATE UNIQUE INDEX IF NOT EXISTS ux_virtual_games_provider_game ON virtual_games (provider_id, game_id);
CREATE TABLE IF NOT EXISTS virtual_game_reports (
id BIGSERIAL PRIMARY KEY,
game_id VARCHAR(150) NOT NULL REFERENCES virtual_games(game_id) ON DELETE CASCADE,
provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE,
report_date DATE NOT NULL,
total_rounds BIGINT DEFAULT 0,
total_bets NUMERIC(18,2) DEFAULT 0,
total_payouts NUMERIC(18,2) DEFAULT 0,
total_profit NUMERIC(18,2) GENERATED ALWAYS AS (total_bets - total_payouts) STORED,
total_players BIGINT DEFAULT 0,
report_type VARCHAR(50) DEFAULT 'daily',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_game_report
ON virtual_game_reports (game_id, report_date, report_type);
CREATE TABLE IF NOT EXISTS wallets ( CREATE TABLE IF NOT EXISTS wallets (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
balance BIGINT NOT NULL DEFAULT 0, balance BIGINT NOT NULL DEFAULT 0,
@ -234,6 +271,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer (
cashier_id BIGINT, cashier_id BIGINT,
verified BOOLEAN DEFAULT false, verified BOOLEAN DEFAULT false,
reference_number VARCHAR(255) NOT NULL, reference_number VARCHAR(255) NOT NULL,
ext_reference_number VARCHAR(255),
session_id VARCHAR(255), session_id VARCHAR(255),
status VARCHAR(255), status VARCHAR(255),
payment_method VARCHAR(255), payment_method VARCHAR(255),

View File

@ -3,11 +3,8 @@ CREATE TABLE virtual_game_sessions (
user_id BIGINT NOT NULL REFERENCES users(id), user_id BIGINT NOT NULL REFERENCES users(id),
game_id VARCHAR(50) NOT NULL, game_id VARCHAR(50) NOT NULL,
session_token VARCHAR(255) NOT NULL UNIQUE, session_token VARCHAR(255) NOT NULL UNIQUE,
currency VARCHAR(3) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, COMPLETED, FAILED
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
); );
CREATE TABLE virtual_game_transactions ( CREATE TABLE virtual_game_transactions (

View File

@ -1,2 +0,0 @@
DROP TABLE IF EXISTS enetpulse_sports;
DROP TABLE IF EXISTS enetpulse_tournament_templates;

View File

@ -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);

View File

@ -1,2 +1,11 @@
DROP TABLE IF EXISTS enetpulse_sports; DROP TABLE IF EXISTS enetpulse_sports;
DROP TABLE IF EXISTS enetpulse_tournament_templates; DROP TABLE IF EXISTS enetpulse_tournament_templates;
DROP TABLE IF EXISTS enetpulse_tournaments;
DROP TABLE IF EXISTS enetpulse_tournament_stages;
DROP TABLE IF EXISTS enetpulse_fixtures;
DROP TABLE IF EXISTS enetpulse_results;
DROP TABLE IF EXISTS enetpulse_result_participants;
DROP TABLE IF EXISTS enetpulse_result_referees;
DROP TABLE IF EXISTS enetpulse_outcome_types;
DROP TABLE IF EXISTS enetpulse_preodds;
DROP TABLE IF EXISTS enetpulse_preodds_bettingoffers;

View File

@ -21,3 +21,207 @@ CREATE TABLE IF NOT EXISTS enetpulse_tournament_templates (
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ 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
);

View File

@ -68,6 +68,9 @@ SELECT
FROM enetpulse_tournament_templates FROM enetpulse_tournament_templates
ORDER BY name; ORDER BY name;
-- -- name: DeleteEnetpulseTournamentTemplateByID :exec
-- DELETE FROM enetpulse_tournament_templates WHERE template_id = $1;
-- name: CreateEnetpulseTournament :one -- name: CreateEnetpulseTournament :one
INSERT INTO enetpulse_tournaments ( INSERT INTO enetpulse_tournaments (
tournament_id, tournament_id,
@ -104,7 +107,8 @@ INSERT INTO enetpulse_tournament_stages (
updates_count, updates_count,
last_updated_at, last_updated_at,
status status
) VALUES ( )
VALUES (
$1, -- stage_id $1, -- stage_id
$2, -- name $2, -- name
$3, -- tournament_fk $3, -- tournament_fk
@ -117,6 +121,19 @@ INSERT INTO enetpulse_tournament_stages (
$10, -- last_updated_at $10, -- last_updated_at
$11 -- status $11 -- status
) )
ON CONFLICT (stage_id) DO UPDATE
SET
name = EXCLUDED.name,
tournament_fk = EXCLUDED.tournament_fk,
gender = EXCLUDED.gender,
country_fk = EXCLUDED.country_fk,
country_name = EXCLUDED.country_name,
start_date = EXCLUDED.start_date,
end_date = EXCLUDED.end_date,
updates_count = EXCLUDED.updates_count,
last_updated_at = EXCLUDED.last_updated_at,
status = EXCLUDED.status,
updated_at = NOW()
RETURNING *; RETURNING *;
-- name: GetAllEnetpulseTournamentStages :many -- name: GetAllEnetpulseTournamentStages :many
@ -130,5 +147,391 @@ FROM enetpulse_tournament_stages
WHERE tournament_fk = $1 WHERE tournament_fk = $1
ORDER BY created_at DESC; ORDER BY created_at DESC;
-- name: CreateEnetpulseFixture :one
INSERT INTO enetpulse_fixtures (
fixture_id,
name,
sport_fk,
tournament_fk,
tournament_template_fk,
tournament_name,
tournament_template_name,
sport_name,
gender,
start_date,
status_type,
status_desc_fk,
round_type_fk,
updates_count,
last_updated_at,
created_at,
updated_at
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP
)
ON CONFLICT (fixture_id) DO UPDATE
SET
name = EXCLUDED.name,
sport_fk = EXCLUDED.sport_fk,
tournament_fk = EXCLUDED.tournament_fk,
tournament_template_fk = EXCLUDED.tournament_template_fk,
tournament_name = EXCLUDED.tournament_name,
tournament_template_name = EXCLUDED.tournament_template_name,
sport_name = EXCLUDED.sport_name,
gender = EXCLUDED.gender,
start_date = EXCLUDED.start_date,
status_type = EXCLUDED.status_type,
status_desc_fk = EXCLUDED.status_desc_fk,
round_type_fk = EXCLUDED.round_type_fk,
updates_count = EXCLUDED.updates_count,
last_updated_at = EXCLUDED.last_updated_at,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: GetAllEnetpulseFixtures :many
SELECT *
FROM enetpulse_fixtures
ORDER BY created_at DESC;
-- name: CreateEnetpulseResult :one
INSERT INTO enetpulse_results (
result_id,
name,
sport_fk,
tournament_fk,
tournament_template_fk,
tournament_name,
tournament_template_name,
sport_name,
start_date,
status_type,
status_desc_fk,
round_type_fk,
updates_count,
last_updated_at,
round,
live,
venue_name,
livestats_plus,
livestats_type,
commentary,
lineup_confirmed,
verified,
spectators,
game_started,
first_half_ended,
second_half_started,
second_half_ended,
game_ended,
created_at,
updated_at
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,
$11,$12,$13,$14,$15,$16,$17,$18,
$19,$20,$21,$22,$23,$24,$25,$26,
$27,$28,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP
)
ON CONFLICT (result_id) DO UPDATE
SET
name = EXCLUDED.name,
sport_fk = EXCLUDED.sport_fk,
tournament_fk = EXCLUDED.tournament_fk,
tournament_template_fk = EXCLUDED.tournament_template_fk,
tournament_name = EXCLUDED.tournament_name,
tournament_template_name = EXCLUDED.tournament_template_name,
sport_name = EXCLUDED.sport_name,
start_date = EXCLUDED.start_date,
status_type = EXCLUDED.status_type,
status_desc_fk = EXCLUDED.status_desc_fk,
round_type_fk = EXCLUDED.round_type_fk,
updates_count = EXCLUDED.updates_count,
last_updated_at = EXCLUDED.last_updated_at,
round = EXCLUDED.round,
live = EXCLUDED.live,
venue_name = EXCLUDED.venue_name,
livestats_plus = EXCLUDED.livestats_plus,
livestats_type = EXCLUDED.livestats_type,
commentary = EXCLUDED.commentary,
lineup_confirmed = EXCLUDED.lineup_confirmed,
verified = EXCLUDED.verified,
spectators = EXCLUDED.spectators,
game_started = EXCLUDED.game_started,
first_half_ended = EXCLUDED.first_half_ended,
second_half_started = EXCLUDED.second_half_started,
second_half_ended = EXCLUDED.second_half_ended,
game_ended = EXCLUDED.game_ended,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: GetAllEnetpulseResults :many
SELECT *
FROM enetpulse_results
ORDER BY created_at DESC;
-- name: CreateEnetpulseResultParticipant :one
INSERT INTO enetpulse_result_participants (
participant_map_id,
result_fk,
participant_fk,
number,
name,
gender,
type,
country_fk,
country_name,
ordinary_time,
running_score,
halftime,
final_result,
last_updated_at,
created_at
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,CURRENT_TIMESTAMP
)
ON CONFLICT (participant_map_id) DO UPDATE
SET
result_fk = EXCLUDED.result_fk,
participant_fk = EXCLUDED.participant_fk,
number = EXCLUDED.number,
name = EXCLUDED.name,
gender = EXCLUDED.gender,
type = EXCLUDED.type,
country_fk = EXCLUDED.country_fk,
country_name = EXCLUDED.country_name,
ordinary_time = EXCLUDED.ordinary_time,
running_score = EXCLUDED.running_score,
halftime = EXCLUDED.halftime,
final_result = EXCLUDED.final_result,
last_updated_at = EXCLUDED.last_updated_at
RETURNING *;
-- name: GetEnetpulseResultParticipantsByResultFK :many
SELECT *
FROM enetpulse_result_participants
WHERE result_fk = $1
ORDER BY created_at DESC;
-- name: CreateEnetpulseResultReferee :one
INSERT INTO enetpulse_result_referees (
result_fk,
referee_fk,
assistant1_referee_fk,
assistant2_referee_fk,
fourth_referee_fk,
var1_referee_fk,
var2_referee_fk,
last_updated_at,
created_at
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,CURRENT_TIMESTAMP
)
ON CONFLICT (result_fk) DO UPDATE
SET
referee_fk = EXCLUDED.referee_fk,
assistant1_referee_fk = EXCLUDED.assistant1_referee_fk,
assistant2_referee_fk = EXCLUDED.assistant2_referee_fk,
fourth_referee_fk = EXCLUDED.fourth_referee_fk,
var1_referee_fk = EXCLUDED.var1_referee_fk,
var2_referee_fk = EXCLUDED.var2_referee_fk,
last_updated_at = EXCLUDED.last_updated_at
RETURNING *;
-- name: GetEnetpulseResultRefereesByResultFK :many
SELECT *
FROM enetpulse_result_referees
WHERE result_fk = $1
ORDER BY created_at DESC;
-- name: CreateEnetpulseOutcomeType :one
INSERT INTO enetpulse_outcome_types (
outcome_type_id,
name,
description,
updates_count,
last_updated_at,
created_at,
updated_at
) VALUES (
$1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (outcome_type_id) DO UPDATE
SET
name = EXCLUDED.name,
description = EXCLUDED.description,
updates_count = EXCLUDED.updates_count,
last_updated_at = EXCLUDED.last_updated_at,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: GetAllEnetpulseOutcomeTypes :many
SELECT *
FROM enetpulse_outcome_types
ORDER BY created_at DESC;
-- name: CreateEnetpulsePreodds :one
INSERT INTO enetpulse_preodds (
preodds_id,
event_fk,
outcome_type_fk,
outcome_scope_fk,
outcome_subtype_fk,
event_participant_number,
iparam,
iparam2,
dparam,
dparam2,
sparam,
updates_count,
last_updated_at,
created_at,
updated_at
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP
)
ON CONFLICT (preodds_id) DO UPDATE
SET
event_fk = EXCLUDED.event_fk,
outcome_type_fk = EXCLUDED.outcome_type_fk,
outcome_scope_fk = EXCLUDED.outcome_scope_fk,
outcome_subtype_fk = EXCLUDED.outcome_subtype_fk,
event_participant_number = EXCLUDED.event_participant_number,
iparam = EXCLUDED.iparam,
iparam2 = EXCLUDED.iparam2,
dparam = EXCLUDED.dparam,
dparam2 = EXCLUDED.dparam2,
sparam = EXCLUDED.sparam,
updates_count = EXCLUDED.updates_count,
last_updated_at = EXCLUDED.last_updated_at,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: GetAllEnetpulsePreodds :many
SELECT *
FROM enetpulse_preodds
ORDER BY created_at DESC;
-- name: CreateEnetpulsePreoddsBettingOffer :one
INSERT INTO enetpulse_preodds_bettingoffers (
bettingoffer_id,
preodds_fk,
bettingoffer_status_fk,
odds_provider_fk,
odds,
odds_old,
active,
coupon_key,
updates_count,
last_updated_at,
created_at,
updated_at
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP
)
ON CONFLICT (bettingoffer_id) DO UPDATE
SET
preodds_fk = EXCLUDED.preodds_fk,
bettingoffer_status_fk = EXCLUDED.bettingoffer_status_fk,
odds_provider_fk = EXCLUDED.odds_provider_fk,
odds = EXCLUDED.odds,
odds_old = EXCLUDED.odds_old,
active = EXCLUDED.active,
coupon_key = EXCLUDED.coupon_key,
updates_count = EXCLUDED.updates_count,
last_updated_at = EXCLUDED.last_updated_at,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: GetAllEnetpulsePreoddsBettingOffers :many
SELECT *
FROM enetpulse_preodds_bettingoffers
ORDER BY created_at DESC;
-- name: GetAllEnetpulsePreoddsWithBettingOffers :many
SELECT
p.id AS preodds_db_id,
p.preodds_id,
p.event_fk,
p.outcome_type_fk,
p.outcome_scope_fk,
p.outcome_subtype_fk,
p.event_participant_number,
p.iparam,
p.iparam2,
p.dparam,
p.dparam2,
p.sparam,
p.updates_count AS preodds_updates_count,
p.last_updated_at AS preodds_last_updated_at,
p.created_at AS preodds_created_at,
p.updated_at AS preodds_updated_at,
-- Betting offer fields
bo.id AS bettingoffer_db_id,
bo.bettingoffer_id,
bo.preodds_fk, -- ✅ ensure alias matches struct field
bo.bettingoffer_status_fk,
bo.odds_provider_fk,
bo.odds,
bo.odds_old,
bo.active,
bo.coupon_key,
bo.updates_count AS bettingoffer_updates_count,
bo.last_updated_at AS bettingoffer_last_updated_at,
bo.created_at AS bettingoffer_created_at,
bo.updated_at AS bettingoffer_updated_at
FROM enetpulse_preodds p
LEFT JOIN enetpulse_preodds_bettingoffers bo
ON bo.preodds_fk = p.preodds_id
ORDER BY p.created_at DESC, bo.created_at DESC;
-- name: GetFixturesWithPreodds :many
SELECT
f.fixture_id AS id,
f.fixture_id AS fixture_id,
f.name AS fixture_name,
f.sport_fk,
f.tournament_fk,
f.tournament_template_fk,
f.start_date,
f.status_type,
f.status_desc_fk,
f.round_type_fk,
f.updates_count AS fixture_updates_count,
f.last_updated_at AS fixture_last_updated_at,
f.created_at AS fixture_created_at,
f.updated_at AS fixture_updated_at,
-- Preodds fields
p.id AS preodds_db_id,
p.preodds_id,
p.event_fk,
p.outcome_type_fk,
p.outcome_scope_fk,
p.outcome_subtype_fk,
p.event_participant_number,
p.iparam,
p.iparam2,
p.dparam,
p.dparam2,
p.sparam,
p.updates_count AS preodds_updates_count,
p.last_updated_at AS preodds_last_updated_at,
p.created_at AS preodds_created_at,
p.updated_at AS preodds_updated_at
FROM enetpulse_fixtures f
LEFT JOIN enetpulse_preodds p
ON p.event_fk = f.id
ORDER BY f.start_date DESC;

View File

@ -8,11 +8,12 @@ INSERT INTO wallet_transfer (
cashier_id, cashier_id,
verified, verified,
reference_number, reference_number,
ext_reference_number,
session_id, session_id,
status, status,
payment_method payment_method
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING *; RETURNING *;
-- name: GetAllTransfers :many -- name: GetAllTransfers :many
SELECT * SELECT *

View File

@ -63,38 +63,38 @@ RETURNING id,
INSERT INTO virtual_game_sessions ( INSERT INTO virtual_game_sessions (
user_id, user_id,
game_id, game_id,
session_token, session_token
currency,
status,
expires_at
) )
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3)
RETURNING id, RETURNING
id,
user_id, user_id,
game_id, game_id,
session_token, session_token,
currency,
status,
created_at, created_at,
updated_at, updated_at;
expires_at;
-- name: GetVirtualGameSessionByUserID :one
SELECT
id,
user_id,
game_id,
session_token,
created_at,
updated_at
FROM virtual_game_sessions
WHERE user_id = $1;
-- name: GetVirtualGameSessionByToken :one -- name: GetVirtualGameSessionByToken :one
SELECT id, SELECT id,
user_id, user_id,
game_id, game_id,
session_token, session_token,
currency,
status,
created_at, created_at,
updated_at, updated_at
expires_at
FROM virtual_game_sessions FROM virtual_game_sessions
WHERE session_token = $1; WHERE session_token = $1;
-- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions
SET status = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: CreateVirtualGameTransaction :one -- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions ( INSERT INTO virtual_game_transactions (
session_id, session_id,
@ -288,3 +288,86 @@ ORDER BY vg.created_at DESC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: DeleteAllVirtualGames :exec -- name: DeleteAllVirtualGames :exec
DELETE FROM virtual_games; DELETE FROM virtual_games;
-- name: CreateVirtualGameProviderReport :one
INSERT INTO virtual_game_provider_reports (
provider_id,
report_date,
total_games_played,
total_bets,
total_payouts,
total_players,
report_type,
created_at,
updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, COALESCE($7, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (provider_id, report_date, report_type) DO UPDATE
SET
total_games_played = EXCLUDED.total_games_played,
total_bets = EXCLUDED.total_bets,
total_payouts = EXCLUDED.total_payouts,
total_players = EXCLUDED.total_players,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: CreateVirtualGameReport :one
INSERT INTO virtual_game_reports (
game_id,
provider_id,
report_date,
total_rounds,
total_bets,
total_payouts,
total_players,
report_type,
created_at,
updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, COALESCE($8, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (game_id, report_date, report_type) DO UPDATE
SET
total_rounds = EXCLUDED.total_rounds,
total_bets = EXCLUDED.total_bets,
total_payouts = EXCLUDED.total_payouts,
total_players = EXCLUDED.total_players,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: GetVirtualGameProviderReportByProviderAndDate :one
SELECT *
FROM virtual_game_provider_reports
WHERE provider_id = $1
AND report_date = $2
AND report_type = $3;
-- name: UpdateVirtualGameProviderReportByDate :exec
UPDATE virtual_game_provider_reports
SET
total_games_played = total_games_played + $4,
total_bets = total_bets + $5,
total_payouts = total_payouts + $6,
total_players = total_players + $7,
updated_at = CURRENT_TIMESTAMP
WHERE
provider_id = $1
AND report_date = $2
AND report_type = $3;
-- name: ListVirtualGameProviderReportsByGamesPlayedAsc :many
SELECT *
FROM virtual_game_provider_reports
ORDER BY total_games_played ASC;
-- name: ListVirtualGameProviderReportsByGamesPlayedDesc :many
SELECT *
FROM virtual_game_provider_reports
ORDER BY total_games_played DESC;

View File

@ -5,7 +5,7 @@ services:
container_name: fortunebet-backend-postgres-1 container_name: fortunebet-backend-postgres-1
image: postgres:16-alpine image: postgres:16-alpine
ports: ports:
- 5422:5432 - "5422:5432"
environment: environment:
- POSTGRES_PASSWORD=secret - POSTGRES_PASSWORD=secret
- POSTGRES_USER=root - POSTGRES_USER=root
@ -20,12 +20,13 @@ services:
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
- ./exports:/exports - ./exports:/exports
mongo: mongo:
container_name: fortunebet-mongo container_name: fortunebet-mongo
image: mongo:7.0.11 image: mongo:7.0.11
restart: always restart: always
ports: ports:
- "27017:27017" - "27025:27017"
environment: environment:
MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: secret MONGO_INITDB_ROOT_PASSWORD: secret
@ -38,6 +39,7 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
migrate: migrate:
image: migrate/migrate image: migrate/migrate
volumes: volumes:
@ -72,7 +74,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
target: runner target: runner
ports: ports:
- ${PORT}:8080 - "${PORT}:8080"
environment: environment:
- DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable - DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable
- MONGO_URI=mongodb://root:secret@mongo:27017 - MONGO_URI=mongodb://root:secret@mongo:27017

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -325,6 +325,139 @@ type DirectDeposit struct {
VerifiedAt pgtype.Timestamp `json:"verified_at"` VerifiedAt pgtype.Timestamp `json:"verified_at"`
} }
type EnetpulseFixture struct {
ID int64 `json:"id"`
FixtureID string `json:"fixture_id"`
Name string `json:"name"`
SportFk string `json:"sport_fk"`
TournamentFk pgtype.Text `json:"tournament_fk"`
TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"`
TournamentName pgtype.Text `json:"tournament_name"`
TournamentTemplateName pgtype.Text `json:"tournament_template_name"`
SportName pgtype.Text `json:"sport_name"`
Gender pgtype.Text `json:"gender"`
StartDate pgtype.Timestamptz `json:"start_date"`
StatusType pgtype.Text `json:"status_type"`
StatusDescFk pgtype.Text `json:"status_desc_fk"`
RoundTypeFk pgtype.Text `json:"round_type_fk"`
UpdatesCount pgtype.Int4 `json:"updates_count"`
LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type EnetpulseOutcomeType struct {
ID int64 `json:"id"`
OutcomeTypeID string `json:"outcome_type_id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
UpdatesCount pgtype.Int4 `json:"updates_count"`
LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type EnetpulsePreodd struct {
ID int64 `json:"id"`
PreoddsID string `json:"preodds_id"`
EventFk int64 `json:"event_fk"`
OutcomeTypeFk pgtype.Int4 `json:"outcome_type_fk"`
OutcomeScopeFk pgtype.Int4 `json:"outcome_scope_fk"`
OutcomeSubtypeFk pgtype.Int4 `json:"outcome_subtype_fk"`
EventParticipantNumber pgtype.Int4 `json:"event_participant_number"`
Iparam pgtype.Text `json:"iparam"`
Iparam2 pgtype.Text `json:"iparam2"`
Dparam pgtype.Text `json:"dparam"`
Dparam2 pgtype.Text `json:"dparam2"`
Sparam pgtype.Text `json:"sparam"`
UpdatesCount pgtype.Int4 `json:"updates_count"`
LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type EnetpulsePreoddsBettingoffer struct {
ID int64 `json:"id"`
BettingofferID string `json:"bettingoffer_id"`
PreoddsFk string `json:"preodds_fk"`
BettingofferStatusFk pgtype.Int4 `json:"bettingoffer_status_fk"`
OddsProviderFk pgtype.Int4 `json:"odds_provider_fk"`
Odds pgtype.Numeric `json:"odds"`
OddsOld pgtype.Numeric `json:"odds_old"`
Active pgtype.Bool `json:"active"`
CouponKey pgtype.Text `json:"coupon_key"`
UpdatesCount pgtype.Int4 `json:"updates_count"`
LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type EnetpulseResult struct {
ID int64 `json:"id"`
ResultID string `json:"result_id"`
Name string `json:"name"`
SportFk string `json:"sport_fk"`
TournamentFk pgtype.Text `json:"tournament_fk"`
TournamentTemplateFk pgtype.Text `json:"tournament_template_fk"`
TournamentName pgtype.Text `json:"tournament_name"`
TournamentTemplateName pgtype.Text `json:"tournament_template_name"`
SportName pgtype.Text `json:"sport_name"`
StartDate pgtype.Timestamptz `json:"start_date"`
StatusType pgtype.Text `json:"status_type"`
StatusDescFk pgtype.Text `json:"status_desc_fk"`
RoundTypeFk pgtype.Text `json:"round_type_fk"`
UpdatesCount pgtype.Int4 `json:"updates_count"`
LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"`
Round pgtype.Text `json:"round"`
Live pgtype.Text `json:"live"`
VenueName pgtype.Text `json:"venue_name"`
LivestatsPlus pgtype.Text `json:"livestats_plus"`
LivestatsType pgtype.Text `json:"livestats_type"`
Commentary pgtype.Text `json:"commentary"`
LineupConfirmed pgtype.Bool `json:"lineup_confirmed"`
Verified pgtype.Bool `json:"verified"`
Spectators pgtype.Int4 `json:"spectators"`
GameStarted pgtype.Timestamptz `json:"game_started"`
FirstHalfEnded pgtype.Timestamptz `json:"first_half_ended"`
SecondHalfStarted pgtype.Timestamptz `json:"second_half_started"`
SecondHalfEnded pgtype.Timestamptz `json:"second_half_ended"`
GameEnded pgtype.Timestamptz `json:"game_ended"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type EnetpulseResultParticipant struct {
ID int64 `json:"id"`
ParticipantMapID string `json:"participant_map_id"`
ResultFk string `json:"result_fk"`
ParticipantFk string `json:"participant_fk"`
Number pgtype.Int4 `json:"number"`
Name pgtype.Text `json:"name"`
Gender pgtype.Text `json:"gender"`
Type pgtype.Text `json:"type"`
CountryFk pgtype.Text `json:"country_fk"`
CountryName pgtype.Text `json:"country_name"`
OrdinaryTime pgtype.Text `json:"ordinary_time"`
RunningScore pgtype.Text `json:"running_score"`
Halftime pgtype.Text `json:"halftime"`
FinalResult pgtype.Text `json:"final_result"`
LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type EnetpulseResultReferee struct {
ID int64 `json:"id"`
ResultFk string `json:"result_fk"`
RefereeFk pgtype.Text `json:"referee_fk"`
Assistant1RefereeFk pgtype.Text `json:"assistant1_referee_fk"`
Assistant2RefereeFk pgtype.Text `json:"assistant2_referee_fk"`
FourthRefereeFk pgtype.Text `json:"fourth_referee_fk"`
Var1RefereeFk pgtype.Text `json:"var1_referee_fk"`
Var2RefereeFk pgtype.Text `json:"var2_referee_fk"`
LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type EnetpulseSport struct { type EnetpulseSport struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SportID string `json:"sport_id"` SportID string `json:"sport_id"`
@ -1050,16 +1183,42 @@ type VirtualGameProvider struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
type VirtualGameProviderReport struct {
ID int64 `json:"id"`
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
TotalGamesPlayed pgtype.Int8 `json:"total_games_played"`
TotalBets pgtype.Numeric `json:"total_bets"`
TotalPayouts pgtype.Numeric `json:"total_payouts"`
TotalProfit pgtype.Numeric `json:"total_profit"`
TotalPlayers pgtype.Int8 `json:"total_players"`
ReportType pgtype.Text `json:"report_type"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type VirtualGameReport struct {
ID int64 `json:"id"`
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
TotalRounds pgtype.Int8 `json:"total_rounds"`
TotalBets pgtype.Numeric `json:"total_bets"`
TotalPayouts pgtype.Numeric `json:"total_payouts"`
TotalProfit pgtype.Numeric `json:"total_profit"`
TotalPlayers pgtype.Int8 `json:"total_players"`
ReportType pgtype.Text `json:"report_type"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type VirtualGameSession struct { type VirtualGameSession struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
GameID string `json:"game_id"` GameID string `json:"game_id"`
SessionToken string `json:"session_token"` SessionToken string `json:"session_token"`
Currency string `json:"currency"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
} }
type VirtualGameTransaction struct { type VirtualGameTransaction struct {
@ -1127,6 +1286,7 @@ type WalletTransfer struct {
CashierID pgtype.Int8 `json:"cashier_id"` CashierID pgtype.Int8 `json:"cashier_id"`
Verified pgtype.Bool `json:"verified"` Verified pgtype.Bool `json:"verified"`
ReferenceNumber string `json:"reference_number"` ReferenceNumber string `json:"reference_number"`
ExtReferenceNumber pgtype.Text `json:"ext_reference_number"`
SessionID pgtype.Text `json:"session_id"` SessionID pgtype.Text `json:"session_id"`
Status pgtype.Text `json:"status"` Status pgtype.Text `json:"status"`
PaymentMethod pgtype.Text `json:"payment_method"` PaymentMethod pgtype.Text `json:"payment_method"`
@ -1144,6 +1304,7 @@ type WalletTransferDetail struct {
CashierID pgtype.Int8 `json:"cashier_id"` CashierID pgtype.Int8 `json:"cashier_id"`
Verified pgtype.Bool `json:"verified"` Verified pgtype.Bool `json:"verified"`
ReferenceNumber string `json:"reference_number"` ReferenceNumber string `json:"reference_number"`
ExtReferenceNumber pgtype.Text `json:"ext_reference_number"`
SessionID pgtype.Text `json:"session_id"` SessionID pgtype.Text `json:"session_id"`
Status pgtype.Text `json:"status"` Status pgtype.Text `json:"status"`
PaymentMethod pgtype.Text `json:"payment_method"` PaymentMethod pgtype.Text `json:"payment_method"`

View File

@ -21,12 +21,13 @@ INSERT INTO wallet_transfer (
cashier_id, cashier_id,
verified, verified,
reference_number, reference_number,
ext_reference_number,
session_id, session_id,
status, status,
payment_method payment_method
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at RETURNING id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at
` `
type CreateTransferParams struct { type CreateTransferParams struct {
@ -38,6 +39,7 @@ type CreateTransferParams struct {
CashierID pgtype.Int8 `json:"cashier_id"` CashierID pgtype.Int8 `json:"cashier_id"`
Verified pgtype.Bool `json:"verified"` Verified pgtype.Bool `json:"verified"`
ReferenceNumber string `json:"reference_number"` ReferenceNumber string `json:"reference_number"`
ExtReferenceNumber pgtype.Text `json:"ext_reference_number"`
SessionID pgtype.Text `json:"session_id"` SessionID pgtype.Text `json:"session_id"`
Status pgtype.Text `json:"status"` Status pgtype.Text `json:"status"`
PaymentMethod pgtype.Text `json:"payment_method"` PaymentMethod pgtype.Text `json:"payment_method"`
@ -53,6 +55,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
arg.CashierID, arg.CashierID,
arg.Verified, arg.Verified,
arg.ReferenceNumber, arg.ReferenceNumber,
arg.ExtReferenceNumber,
arg.SessionID, arg.SessionID,
arg.Status, arg.Status,
arg.PaymentMethod, arg.PaymentMethod,
@ -68,6 +71,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber, &i.ReferenceNumber,
&i.ExtReferenceNumber,
&i.SessionID, &i.SessionID,
&i.Status, &i.Status,
&i.PaymentMethod, &i.PaymentMethod,
@ -78,7 +82,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams)
} }
const GetAllTransfers = `-- name: GetAllTransfers :many const GetAllTransfers = `-- name: GetAllTransfers :many
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
FROM wallet_transfer_details FROM wallet_transfer_details
` `
@ -101,6 +105,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransferDetail,
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber, &i.ReferenceNumber,
&i.ExtReferenceNumber,
&i.SessionID, &i.SessionID,
&i.Status, &i.Status,
&i.PaymentMethod, &i.PaymentMethod,
@ -121,7 +126,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransferDetail,
} }
const GetTransferByID = `-- name: GetTransferByID :one const GetTransferByID = `-- name: GetTransferByID :one
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
FROM wallet_transfer_details FROM wallet_transfer_details
WHERE id = $1 WHERE id = $1
` `
@ -139,6 +144,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber, &i.ReferenceNumber,
&i.ExtReferenceNumber,
&i.SessionID, &i.SessionID,
&i.Status, &i.Status,
&i.PaymentMethod, &i.PaymentMethod,
@ -152,7 +158,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer
} }
const GetTransferByReference = `-- name: GetTransferByReference :one const GetTransferByReference = `-- name: GetTransferByReference :one
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
FROM wallet_transfer_details FROM wallet_transfer_details
WHERE reference_number = $1 WHERE reference_number = $1
` `
@ -170,6 +176,7 @@ func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber st
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber, &i.ReferenceNumber,
&i.ExtReferenceNumber,
&i.SessionID, &i.SessionID,
&i.Status, &i.Status,
&i.PaymentMethod, &i.PaymentMethod,
@ -217,7 +224,7 @@ func (q *Queries) GetTransferStats(ctx context.Context, senderWalletID pgtype.In
} }
const GetTransfersByWallet = `-- name: GetTransfersByWallet :many const GetTransfersByWallet = `-- name: GetTransfersByWallet :many
SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number SELECT id, amount, message, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, reference_number, ext_reference_number, session_id, status, payment_method, created_at, updated_at, first_name, last_name, phone_number
FROM wallet_transfer_details FROM wallet_transfer_details
WHERE receiver_wallet_id = $1 WHERE receiver_wallet_id = $1
OR sender_wallet_id = $1 OR sender_wallet_id = $1
@ -242,6 +249,7 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgt
&i.CashierID, &i.CashierID,
&i.Verified, &i.Verified,
&i.ReferenceNumber, &i.ReferenceNumber,
&i.ExtReferenceNumber,
&i.SessionID, &i.SessionID,
&i.Status, &i.Status,
&i.PaymentMethod, &i.PaymentMethod,

View File

@ -281,56 +281,164 @@ func (q *Queries) CreateVirtualGameProvider(ctx context.Context, arg CreateVirtu
return i, err return i, err
} }
const CreateVirtualGameProviderReport = `-- name: CreateVirtualGameProviderReport :one
INSERT INTO virtual_game_provider_reports (
provider_id,
report_date,
total_games_played,
total_bets,
total_payouts,
total_players,
report_type,
created_at,
updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, COALESCE($7, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (provider_id, report_date, report_type) DO UPDATE
SET
total_games_played = EXCLUDED.total_games_played,
total_bets = EXCLUDED.total_bets,
total_payouts = EXCLUDED.total_payouts,
total_players = EXCLUDED.total_players,
updated_at = CURRENT_TIMESTAMP
RETURNING id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at
`
type CreateVirtualGameProviderReportParams struct {
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
TotalGamesPlayed pgtype.Int8 `json:"total_games_played"`
TotalBets pgtype.Numeric `json:"total_bets"`
TotalPayouts pgtype.Numeric `json:"total_payouts"`
TotalPlayers pgtype.Int8 `json:"total_players"`
Column7 interface{} `json:"column_7"`
}
func (q *Queries) CreateVirtualGameProviderReport(ctx context.Context, arg CreateVirtualGameProviderReportParams) (VirtualGameProviderReport, error) {
row := q.db.QueryRow(ctx, CreateVirtualGameProviderReport,
arg.ProviderID,
arg.ReportDate,
arg.TotalGamesPlayed,
arg.TotalBets,
arg.TotalPayouts,
arg.TotalPlayers,
arg.Column7,
)
var i VirtualGameProviderReport
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.ReportDate,
&i.TotalGamesPlayed,
&i.TotalBets,
&i.TotalPayouts,
&i.TotalProfit,
&i.TotalPlayers,
&i.ReportType,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const CreateVirtualGameReport = `-- name: CreateVirtualGameReport :one
INSERT INTO virtual_game_reports (
game_id,
provider_id,
report_date,
total_rounds,
total_bets,
total_payouts,
total_players,
report_type,
created_at,
updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, COALESCE($8, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (game_id, report_date, report_type) DO UPDATE
SET
total_rounds = EXCLUDED.total_rounds,
total_bets = EXCLUDED.total_bets,
total_payouts = EXCLUDED.total_payouts,
total_players = EXCLUDED.total_players,
updated_at = CURRENT_TIMESTAMP
RETURNING id, game_id, provider_id, report_date, total_rounds, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at
`
type CreateVirtualGameReportParams struct {
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
TotalRounds pgtype.Int8 `json:"total_rounds"`
TotalBets pgtype.Numeric `json:"total_bets"`
TotalPayouts pgtype.Numeric `json:"total_payouts"`
TotalPlayers pgtype.Int8 `json:"total_players"`
Column8 interface{} `json:"column_8"`
}
func (q *Queries) CreateVirtualGameReport(ctx context.Context, arg CreateVirtualGameReportParams) (VirtualGameReport, error) {
row := q.db.QueryRow(ctx, CreateVirtualGameReport,
arg.GameID,
arg.ProviderID,
arg.ReportDate,
arg.TotalRounds,
arg.TotalBets,
arg.TotalPayouts,
arg.TotalPlayers,
arg.Column8,
)
var i VirtualGameReport
err := row.Scan(
&i.ID,
&i.GameID,
&i.ProviderID,
&i.ReportDate,
&i.TotalRounds,
&i.TotalBets,
&i.TotalPayouts,
&i.TotalProfit,
&i.TotalPlayers,
&i.ReportType,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one
INSERT INTO virtual_game_sessions ( INSERT INTO virtual_game_sessions (
user_id, user_id,
game_id, game_id,
session_token, session_token
currency,
status,
expires_at
) )
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3)
RETURNING id, RETURNING
id,
user_id, user_id,
game_id, game_id,
session_token, session_token,
currency,
status,
created_at, created_at,
updated_at, updated_at
expires_at
` `
type CreateVirtualGameSessionParams struct { type CreateVirtualGameSessionParams struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
GameID string `json:"game_id"` GameID string `json:"game_id"`
SessionToken string `json:"session_token"` SessionToken string `json:"session_token"`
Currency string `json:"currency"`
Status string `json:"status"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
} }
func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtualGameSessionParams) (VirtualGameSession, error) { func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtualGameSessionParams) (VirtualGameSession, error) {
row := q.db.QueryRow(ctx, CreateVirtualGameSession, row := q.db.QueryRow(ctx, CreateVirtualGameSession, arg.UserID, arg.GameID, arg.SessionToken)
arg.UserID,
arg.GameID,
arg.SessionToken,
arg.Currency,
arg.Status,
arg.ExpiresAt,
)
var i VirtualGameSession var i VirtualGameSession
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.UserID, &i.UserID,
&i.GameID, &i.GameID,
&i.SessionToken, &i.SessionToken,
&i.Currency,
&i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ExpiresAt,
) )
return i, err return i, err
} }
@ -587,16 +695,46 @@ func (q *Queries) GetVirtualGameProviderByID(ctx context.Context, providerID str
return i, err return i, err
} }
const GetVirtualGameProviderReportByProviderAndDate = `-- name: GetVirtualGameProviderReportByProviderAndDate :one
SELECT id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at
FROM virtual_game_provider_reports
WHERE provider_id = $1
AND report_date = $2
AND report_type = $3
`
type GetVirtualGameProviderReportByProviderAndDateParams struct {
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
ReportType pgtype.Text `json:"report_type"`
}
func (q *Queries) GetVirtualGameProviderReportByProviderAndDate(ctx context.Context, arg GetVirtualGameProviderReportByProviderAndDateParams) (VirtualGameProviderReport, error) {
row := q.db.QueryRow(ctx, GetVirtualGameProviderReportByProviderAndDate, arg.ProviderID, arg.ReportDate, arg.ReportType)
var i VirtualGameProviderReport
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.ReportDate,
&i.TotalGamesPlayed,
&i.TotalBets,
&i.TotalPayouts,
&i.TotalProfit,
&i.TotalPlayers,
&i.ReportType,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetVirtualGameSessionByToken = `-- name: GetVirtualGameSessionByToken :one const GetVirtualGameSessionByToken = `-- name: GetVirtualGameSessionByToken :one
SELECT id, SELECT id,
user_id, user_id,
game_id, game_id,
session_token, session_token,
currency,
status,
created_at, created_at,
updated_at, updated_at
expires_at
FROM virtual_game_sessions FROM virtual_game_sessions
WHERE session_token = $1 WHERE session_token = $1
` `
@ -609,11 +747,34 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken
&i.UserID, &i.UserID,
&i.GameID, &i.GameID,
&i.SessionToken, &i.SessionToken,
&i.Currency,
&i.Status,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ExpiresAt, )
return i, err
}
const GetVirtualGameSessionByUserID = `-- name: GetVirtualGameSessionByUserID :one
SELECT
id,
user_id,
game_id,
session_token,
created_at,
updated_at
FROM virtual_game_sessions
WHERE user_id = $1
`
func (q *Queries) GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (VirtualGameSession, error) {
row := q.db.QueryRow(ctx, GetVirtualGameSessionByUserID, userID)
var i VirtualGameSession
err := row.Scan(
&i.ID,
&i.UserID,
&i.GameID,
&i.SessionToken,
&i.CreatedAt,
&i.UpdatedAt,
) )
return i, err return i, err
} }
@ -745,6 +906,82 @@ func (q *Queries) ListFavoriteGames(ctx context.Context, userID int64) ([]int64,
return items, nil return items, nil
} }
const ListVirtualGameProviderReportsByGamesPlayedAsc = `-- name: ListVirtualGameProviderReportsByGamesPlayedAsc :many
SELECT id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at
FROM virtual_game_provider_reports
ORDER BY total_games_played ASC
`
func (q *Queries) ListVirtualGameProviderReportsByGamesPlayedAsc(ctx context.Context) ([]VirtualGameProviderReport, error) {
rows, err := q.db.Query(ctx, ListVirtualGameProviderReportsByGamesPlayedAsc)
if err != nil {
return nil, err
}
defer rows.Close()
var items []VirtualGameProviderReport
for rows.Next() {
var i VirtualGameProviderReport
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.ReportDate,
&i.TotalGamesPlayed,
&i.TotalBets,
&i.TotalPayouts,
&i.TotalProfit,
&i.TotalPlayers,
&i.ReportType,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ListVirtualGameProviderReportsByGamesPlayedDesc = `-- name: ListVirtualGameProviderReportsByGamesPlayedDesc :many
SELECT id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at
FROM virtual_game_provider_reports
ORDER BY total_games_played DESC
`
func (q *Queries) ListVirtualGameProviderReportsByGamesPlayedDesc(ctx context.Context) ([]VirtualGameProviderReport, error) {
rows, err := q.db.Query(ctx, ListVirtualGameProviderReportsByGamesPlayedDesc)
if err != nil {
return nil, err
}
defer rows.Close()
var items []VirtualGameProviderReport
for rows.Next() {
var i VirtualGameProviderReport
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.ReportDate,
&i.TotalGamesPlayed,
&i.TotalBets,
&i.TotalPayouts,
&i.TotalProfit,
&i.TotalPlayers,
&i.ReportType,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ListVirtualGameProviders = `-- name: ListVirtualGameProviders :many const ListVirtualGameProviders = `-- name: ListVirtualGameProviders :many
SELECT id, SELECT id,
provider_id, provider_id,
@ -845,20 +1082,40 @@ func (q *Queries) UpdateVirtualGameProviderEnabled(ctx context.Context, arg Upda
return i, err return i, err
} }
const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec const UpdateVirtualGameProviderReportByDate = `-- name: UpdateVirtualGameProviderReportByDate :exec
UPDATE virtual_game_sessions UPDATE virtual_game_provider_reports
SET status = $2, SET
total_games_played = total_games_played + $4,
total_bets = total_bets + $5,
total_payouts = total_payouts + $6,
total_players = total_players + $7,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $1 WHERE
provider_id = $1
AND report_date = $2
AND report_type = $3
` `
type UpdateVirtualGameSessionStatusParams struct { type UpdateVirtualGameProviderReportByDateParams struct {
ID int64 `json:"id"` ProviderID string `json:"provider_id"`
Status string `json:"status"` ReportDate pgtype.Date `json:"report_date"`
ReportType pgtype.Text `json:"report_type"`
TotalGamesPlayed pgtype.Int8 `json:"total_games_played"`
TotalBets pgtype.Numeric `json:"total_bets"`
TotalPayouts pgtype.Numeric `json:"total_payouts"`
TotalPlayers pgtype.Int8 `json:"total_players"`
} }
func (q *Queries) UpdateVirtualGameSessionStatus(ctx context.Context, arg UpdateVirtualGameSessionStatusParams) error { func (q *Queries) UpdateVirtualGameProviderReportByDate(ctx context.Context, arg UpdateVirtualGameProviderReportByDateParams) error {
_, err := q.db.Exec(ctx, UpdateVirtualGameSessionStatus, arg.ID, arg.Status) _, err := q.db.Exec(ctx, UpdateVirtualGameProviderReportByDate,
arg.ProviderID,
arg.ReportDate,
arg.ReportType,
arg.TotalGamesPlayed,
arg.TotalBets,
arg.TotalPayouts,
arg.TotalPlayers,
)
return err return err
} }

8
go.mod
View File

@ -81,19 +81,11 @@ require (
require github.com/twilio/twilio-go v1.26.3 require github.com/twilio/twilio-go v1.26.3
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/mock v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/redis/go-redis/v9 v9.10.0 // direct
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
) )
require (
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/segmentio/kafka-go v0.4.48 // direct
)
// require github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f // require github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f
// require github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f // direct // require github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f // direct

33
go.sum
View File

@ -1,5 +1,3 @@
github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f h1:UOp9At84RG8OT2Nw2TQidYWaoW1rAfTqChOJLdhYcm8=
github.com/AnaniyaBelew/ArifpayGoPlugin v0.0.0-20231127130208-54b9bc51118f/go.mod h1:N2NQ6ad3i+oLQU+MlPci2f7mx6Mtg+wUcvzCv77KCl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
@ -12,17 +10,11 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
@ -31,8 +23,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -91,7 +81,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
@ -129,15 +118,11 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/resend/resend-go/v2 v2.20.0 h1:MrIrgV0aHhwRgmcRPw33Nexn6aGJvCvG2XwfFpAMBGM= github.com/resend/resend-go/v2 v2.20.0 h1:MrIrgV0aHhwRgmcRPw33Nexn6aGJvCvG2XwfFpAMBGM=
github.com/resend/resend-go/v2 v2.20.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ= github.com/resend/resend-go/v2 v2.20.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@ -150,8 +135,6 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/kafka-go v0.4.48 h1:9jyu9CWK4W5W+SroCe8EffbrRZVqAOkuaLd/ApID4Vs=
github.com/segmentio/kafka-go v0.4.48/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@ -215,12 +198,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -233,15 +214,11 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -257,25 +234,16 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -284,7 +252,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -47,8 +47,9 @@ var (
) )
type EnetPulseConfig struct { type EnetPulseConfig struct {
UserName string `mapstructure:"username"` // "https://api.aleaplay.com" UserName string `mapstructure:"username"`
Token string `mapstructure:"token"` // Your operator ID with Alea Token string `mapstructure:"token"`
ProviderID string `mapstructure:"provider_id"`
} }
type AleaPlayConfig struct { type AleaPlayConfig struct {
@ -136,6 +137,7 @@ type Config struct {
AFRO_SMS_SENDER_NAME string AFRO_SMS_SENDER_NAME string
AFRO_SMS_RECEIVER_PHONE_NUMBER string AFRO_SMS_RECEIVER_PHONE_NUMBER string
ADRO_SMS_HOST_URL string ADRO_SMS_HOST_URL string
CHAPA_WEBHOOK_SECRET string
CHAPA_TRANSFER_TYPE string CHAPA_TRANSFER_TYPE string
CHAPA_PAYMENT_TYPE string CHAPA_PAYMENT_TYPE string
CHAPA_SECRET_KEY string CHAPA_SECRET_KEY string
@ -144,6 +146,7 @@ type Config struct {
CHAPA_ENCRYPTION_KEY string CHAPA_ENCRYPTION_KEY string
CHAPA_CALLBACK_URL string CHAPA_CALLBACK_URL string
CHAPA_RETURN_URL string CHAPA_RETURN_URL string
CHAPA_RECEIPT_URL string
Bet365Token string Bet365Token string
EnetPulseConfig EnetPulseConfig EnetPulseConfig EnetPulseConfig
PopOK domain.PopOKConfig PopOK domain.PopOKConfig
@ -192,6 +195,7 @@ func (c *Config) loadEnv() error {
c.EnetPulseConfig.Token = os.Getenv("ENETPULSE_TOKEN") c.EnetPulseConfig.Token = os.Getenv("ENETPULSE_TOKEN")
c.EnetPulseConfig.UserName = os.Getenv("ENETPULSE_USERNAME") 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_TRANSFER_TYPE = os.Getenv("CHAPA_TRANSFER_TYPE")
c.CHAPA_PAYMENT_TYPE = os.Getenv("CHAPA_PAYMENT_TYPE") c.CHAPA_PAYMENT_TYPE = os.Getenv("CHAPA_PAYMENT_TYPE")
@ -260,10 +264,12 @@ func (c *Config) loadEnv() error {
c.TELEBIRR.TelebirrCallbackURL = os.Getenv("TELEBIRR_CALLBACK_URL") c.TELEBIRR.TelebirrCallbackURL = os.Getenv("TELEBIRR_CALLBACK_URL")
//Chapa //Chapa
c.CHAPA_WEBHOOK_SECRET = os.Getenv("CHAPA_WEBHOOK_SECRET")
c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY") c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY")
c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY") c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY")
c.CHAPA_ENCRYPTION_KEY = os.Getenv("CHAPA_ENCRYPTION_KEY") c.CHAPA_ENCRYPTION_KEY = os.Getenv("CHAPA_ENCRYPTION_KEY")
c.CHAPA_BASE_URL = os.Getenv("CHAPA_BASE_URL") c.CHAPA_BASE_URL = os.Getenv("CHAPA_BASE_URL")
c.CHAPA_RECEIPT_URL = os.Getenv("CHAPA_RECEIPT_URL")
if c.CHAPA_BASE_URL == "" { if c.CHAPA_BASE_URL == "" {
c.CHAPA_BASE_URL = "https://api.chapa.co/v1" c.CHAPA_BASE_URL = "https://api.chapa.co/v1"
} }
@ -431,34 +437,34 @@ func (c *Config) loadEnv() error {
Platform: popOKPlatform, Platform: popOKPlatform,
} }
AtlasBaseUrl := os.Getenv("ATLAS_BASE_URL") // AtlasBaseUrl := os.Getenv("ATLAS_BASE_URL")
if AtlasBaseUrl == "" { // if AtlasBaseUrl == "" {
return ErrInvalidAtlasBaseUrl // return ErrInvalidAtlasBaseUrl
} // }
AtlasSecretKey := os.Getenv("ATLAS_SECRET_KEY") // AtlasSecretKey := os.Getenv("ATLAS_SECRET_KEY")
if AtlasSecretKey == "" { // if AtlasSecretKey == "" {
return ErrInvalidAtlasSecretKey // return ErrInvalidAtlasSecretKey
} // }
AtlasBrandID := os.Getenv("ATLAS_BRAND_ID") // AtlasBrandID := os.Getenv("ATLAS_BRAND_ID")
if AtlasBrandID == "" { // if AtlasBrandID == "" {
return ErrInvalidAtlasBrandID // return ErrInvalidAtlasBrandID
} // }
AtlasPartnerID := os.Getenv("ATLAS_PARTNER_ID") // AtlasPartnerID := os.Getenv("ATLAS_PARTNER_ID")
if AtlasPartnerID == "" { // if AtlasPartnerID == "" {
return ErrInvalidAtlasPartnerID // return ErrInvalidAtlasPartnerID
} // }
AtlasOperatorID := os.Getenv("ATLAS_OPERATOR_ID") // AtlasOperatorID := os.Getenv("ATLAS_OPERATOR_ID")
if AtlasOperatorID == "" { // if AtlasOperatorID == "" {
return ErrInvalidAtlasOperatorID // return ErrInvalidAtlasOperatorID
} // }
c.Atlas = AtlasConfig{ // c.Atlas = AtlasConfig{
BaseURL: AtlasBaseUrl, // BaseURL: AtlasBaseUrl,
SecretKey: AtlasSecretKey, // SecretKey: AtlasSecretKey,
CasinoID: AtlasBrandID, // CasinoID: AtlasBrandID,
PartnerID: AtlasPartnerID, // PartnerID: AtlasPartnerID,
OperatorID: AtlasOperatorID, // OperatorID: AtlasOperatorID,
} // }
betToken := os.Getenv("BET365_TOKEN") betToken := os.Getenv("BET365_TOKEN")
if betToken == "" { if betToken == "" {

View File

@ -59,12 +59,12 @@ type WebhookRequest struct {
SessionID string `json:"sessionId"` SessionID string `json:"sessionId"`
} }
type ArifpayB2CRequest struct{ // type ArifpayB2CRequest struct{
PhoneNumber string `json:"Phonenumber"` // PhoneNumber string `json:"Phonenumber"`
Amount float64 `json:"amount" binding:"required"` // Amount float64 `json:"amount" binding:"required"`
CustomerEmail string `json:"customerEmail" binding:"required"` // CustomerEmail string `json:"customerEmail" binding:"required"`
CustomerPhone string `json:"customerPhone" binding:"required"` // // CustomerPhone string `json:"customerPhone" binding:"required"`
} // }
type ArifpayVerifyByTransactionIDRequest struct{ type ArifpayVerifyByTransactionIDRequest struct{
TransactionId string `json:"transactionId"` TransactionId string `json:"transactionId"`

View File

@ -46,7 +46,7 @@ type AtlasBetResponse struct {
type AtlasBetWinRequest struct { type AtlasBetWinRequest struct {
Game string `json:"game"` Game string `json:"game"`
CasinoID string `json:"casino_id"` CasinoID string `json:"casino_id"`
RoundID int64 `json:"round_id"` RoundID string `json:"round_id"`
PlayerID string `json:"player_id"` PlayerID string `json:"player_id"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
BetAmount float64 `json:"betAmount"` BetAmount float64 `json:"betAmount"`

View File

@ -33,7 +33,7 @@ const (
PaymentStatusFailed PaymentStatus = "failed" PaymentStatusFailed PaymentStatus = "failed"
) )
type ChapaDepositRequest struct { type ChapaInitDepositRequest struct {
Amount Currency `json:"amount"` Amount Currency `json:"amount"`
Currency string `json:"currency"` Currency string `json:"currency"`
Email string `json:"email"` Email string `json:"email"`
@ -42,6 +42,8 @@ type ChapaDepositRequest struct {
TxRef string `json:"tx_ref"` TxRef string `json:"tx_ref"`
CallbackURL string `json:"callback_url"` CallbackURL string `json:"callback_url"`
ReturnURL string `json:"return_url"` ReturnURL string `json:"return_url"`
PhoneNumber string `json:"phone_number"`
// PhoneNumber string `json:"phone_number"`
} }
type ChapaDepositRequestPayload struct { type ChapaDepositRequestPayload struct {
@ -49,9 +51,15 @@ type ChapaDepositRequestPayload struct {
} }
type ChapaWebhookPayload struct { type ChapaWebhookPayload struct {
TxRef string `json:"tx_ref"` TxRef string `json:"trx_ref"`
Amount Currency `json:"amount"` Amount Currency `json:"amount"`
Currency string `json:"currency"` // Currency string `json:"currency"`
Status PaymentStatus `json:"status"`
}
type ChapaPaymentWebhookRequest struct {
TxRef string `json:"trx_ref"`
RefId string `json:"ref_id"`
Status PaymentStatus `json:"status"` Status PaymentStatus `json:"status"`
} }
@ -68,11 +76,117 @@ type ChapaDepositVerification struct {
Currency string Currency string
} }
type ChapaVerificationResponse struct { type ChapaPaymentVerificationResponse struct {
Message string `json:"message"`
Status string `json:"status"` Status string `json:"status"`
Amount float64 `json:"amount"` Data struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Currency string `json:"currency"` Currency string `json:"currency"`
Amount float64 `json:"amount"`
Charge float64 `json:"charge"`
Mode string `json:"mode"`
Method string `json:"method"`
Type string `json:"type"`
Status string `json:"status"`
Reference string `json:"reference"`
TxRef string `json:"tx_ref"` TxRef string `json:"tx_ref"`
Customization struct {
Title string `json:"title"`
Description string `json:"description"`
Logo interface{} `json:"logo"`
} `json:"customization"`
Meta interface{} `json:"meta"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
type ChapaTransferVerificationResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
Mobile interface{} `json:"mobile"`
Currency string `json:"currency"`
Amount float64 `json:"amount"`
Charge float64 `json:"charge"`
Mode string `json:"mode"`
TransferMethod string `json:"transfer_method"`
Narration interface{} `json:"narration"`
ChapaTransferID string `json:"chapa_transfer_id"`
BankCode int `json:"bank_code"`
BankName string `json:"bank_name"`
CrossPartyReference interface{} `json:"cross_party_reference"`
IPAddress string `json:"ip_address"`
Status string `json:"status"`
TxRef string `json:"tx_ref"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
type ChapaAllTransactionsResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
Transactions []struct {
Status string `json:"status"`
RefID string `json:"ref_id"`
Type string `json:"type"`
CreatedAt string `json:"created_at"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Charge string `json:"charge"`
TransID *string `json:"trans_id"`
PaymentMethod string `json:"payment_method"`
Customer struct {
ID int64 `json:"id"`
Email *string `json:"email"`
FirstName *string `json:"first_name"`
LastName *string `json:"last_name"`
Mobile *string `json:"mobile"`
} `json:"customer"`
} `json:"transactions"`
Pagination struct {
PerPage int `json:"per_page"`
CurrentPage int `json:"current_page"`
FirstPageURL string `json:"first_page_url"`
NextPageURL *string `json:"next_page_url"`
PrevPageURL *string `json:"prev_page_url"`
} `json:"pagination"`
} `json:"data"`
}
type ChapaTransactionEvent struct {
Item int64 `json:"item"`
Message string `json:"message"`
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ChapaTransaction struct {
Status string `json:"status"`
RefID string `json:"ref_id"`
Type string `json:"type"`
CreatedAt string `json:"created_at"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Charge string `json:"charge"`
TransID *string `json:"trans_id"`
PaymentMethod string `json:"payment_method"`
Customer ChapaCustomer `json:"customer"`
}
type ChapaCustomer struct {
ID int64 `json:"id"`
Email *string `json:"email"`
FirstName *string `json:"first_name"`
LastName *string `json:"last_name"`
Mobile *string `json:"mobile"`
} }
// type Bank struct { // type Bank struct {
@ -93,6 +207,57 @@ type ChapaVerificationResponse struct {
// BankLogo string `json:"bank_logo"` // URL or base64 // BankLogo string `json:"bank_logo"` // URL or base64
// } // }
type SwapResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
Status string `json:"status"`
RefID string `json:"ref_id"`
FromCurrency string `json:"from_currency"`
ToCurrency string `json:"to_currency"`
Amount float64 `json:"amount"`
ExchangedAmount float64 `json:"exchanged_amount"`
Charge float64 `json:"charge"`
Rate float64 `json:"rate"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
type ChapaTransfersListResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Meta struct {
CurrentPage int `json:"current_page"`
FirstPageURL string `json:"first_page_url"`
LastPage int `json:"last_page"`
LastPageURL string `json:"last_page_url"`
NextPageURL string `json:"next_page_url"`
Path string `json:"path"`
PerPage int `json:"per_page"`
PrevPageURL interface{} `json:"prev_page_url"`
To int `json:"to"`
Total int `json:"total"`
Error []interface{} `json:"error"`
} `json:"meta"`
Data []struct {
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
Currency string `json:"currency"`
Amount float64 `json:"amount"`
Charge float64 `json:"charge"`
TransferType string `json:"transfer_type"`
ChapaReference string `json:"chapa_reference"`
BankCode int `json:"bank_code"`
BankName string `json:"bank_name"`
BankReference interface{} `json:"bank_reference"`
Status string `json:"status"`
Reference interface{} `json:"reference"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
type BankResponse struct { type BankResponse struct {
Message string `json:"message"` Message string `json:"message"`
Status string `json:"status"` Status string `json:"status"`
@ -157,42 +322,69 @@ type ChapaTransactionType struct {
Type string `json:"type"` Type string `json:"type"`
} }
type ChapaWebHookTransfer struct { type ChapaWebhookTransfer struct {
Event string `json:"event"`
Type string `json:"type"`
AccountName string `json:"account_name"` AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"` AccountNumber string `json:"account_number"`
BankId string `json:"bank_id"` BankID int `json:"bank_id"`
BankName string `json:"bank_name"` BankName string `json:"bank_name"`
Currency string `json:"currency"`
Amount string `json:"amount"` Amount string `json:"amount"`
Type string `json:"type"` Charge string `json:"charge"`
Currency string `json:"currency"`
Status string `json:"status"` Status string `json:"status"`
Reference string `json:"reference"` Reference string `json:"reference"`
TxRef string `json:"tx_ref"`
ChapaReference string `json:"chapa_reference"` ChapaReference string `json:"chapa_reference"`
CreatedAt time.Time `json:"created_at"` BankReference string `json:"bank_reference"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} }
type ChapaWebHookPayment struct { type ChapaWebhookPayment struct {
Event string `json:"event"` Event string `json:"event"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Email string `json:"email"` Email *string `json:"email,omitempty"`
Mobile interface{} `json:"mobile"` Mobile string `json:"mobile"`
Currency string `json:"currency"` Currency string `json:"currency"`
Amount string `json:"amount"` Amount string `json:"amount"`
Charge string `json:"charge"` Charge string `json:"charge"`
Status string `json:"status"` Status string `json:"status"`
Mode string `json:"mode"` Mode string `json:"mode"`
Reference string `json:"reference"` Reference string `json:"reference"`
CreatedAt time.Time `json:"created_at"` CreatedAt string `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt string `json:"updated_at"`
Type string `json:"type"` Type string `json:"type"`
TxRef string `json:"tx_ref"` TxRef string `json:"tx_ref"`
PaymentMethod string `json:"payment_method"` PaymentMethod string `json:"payment_method"`
Customization struct { Customization ChapaWebhookCustomization `json:"customization"`
Title interface{} `json:"title"` Meta interface{} `json:"meta"` // may vary in structure, so kept flexible
Description interface{} `json:"description"` }
Logo interface{} `json:"logo"`
} `json:"customization"` type ChapaWebhookCustomization struct {
Meta string `json:"meta"` Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
Logo *string `json:"logo,omitempty"`
}
type Balance struct {
Currency string `json:"currency"`
AvailableBalance float64 `json:"available_balance"`
LedgerBalance float64 `json:"ledger_balance"`
}
type SwapRequest struct {
From string `json:"from"`
To string `json:"to"`
Amount float64 `json:"amount"`
}
type ChapaCancelResponse struct {
Message string `json:"message"`
Status string `json:"status"`
TxRef string `json:"tx_ref"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} }

View File

@ -123,7 +123,7 @@ type TournamentStageParticipantsResponse struct {
type DailyEventsRequest struct { type DailyEventsRequest struct {
SportFK int // one of these three required SportFK int // one of these three required
TournamentTemplateFK int TournamentTemplateFK int
TournamentStageFK int // TournamentStageFK int
Date string // YYYY-MM-DD optional Date string // YYYY-MM-DD optional
Live string // yes/no optional Live string // yes/no optional
IncludeVenue string // yes/no optional IncludeVenue string // yes/no optional
@ -147,7 +147,7 @@ type DailyEventsResponse struct {
type FixturesRequest struct { type FixturesRequest struct {
SportFK int SportFK int
TournamentTemplateFK int TournamentTemplateFK int
TournamentStageFK int // TournamentStageFK int
LanguageTypeFK int LanguageTypeFK int
Date string // YYYY-MM-DD Date string // YYYY-MM-DD
Live string // "yes" | "no" Live string // "yes" | "no"
@ -172,7 +172,7 @@ type FixtureEvent struct {
type ResultsRequest struct { type ResultsRequest struct {
SportFK int SportFK int
TournamentTemplateFK int TournamentTemplateFK int
TournamentStageFK int // TournamentStageFK int
LanguageTypeFK int LanguageTypeFK int
Date string // YYYY-MM-DD Date string // YYYY-MM-DD
Live string // "yes" | "no" Live string // "yes" | "no"
@ -225,7 +225,7 @@ type EventDetailsResponse struct {
type EventListRequest struct { type EventListRequest struct {
TournamentFK int // optional TournamentFK int // optional
TournamentStageFK int // optional // TournamentStageFK int // optional
IncludeEventProperties bool // default true IncludeEventProperties bool // default true
StatusType string // e.g. "finished", "inprogress" StatusType string // e.g. "finished", "inprogress"
IncludeVenue bool IncludeVenue bool
@ -250,7 +250,7 @@ type ParticipantFixturesRequest struct {
SportFK int SportFK int
TournamentFK int TournamentFK int
TournamentTemplateFK int TournamentTemplateFK int
TournamentStageFK int // TournamentStageFK int
Date string Date string
Live string Live string
Limit int Limit int
@ -462,3 +462,308 @@ type CreateEnetpulseTournamentStage struct {
CountryName string `json:"country_name"` // country name from API CountryName string `json:"country_name"` // country name from API
Status int `json:"status"` // active/inactive Status int `json:"status"` // active/inactive
} }
// For insertion
type CreateEnetpulseFixture struct {
FixtureID string
Name string
SportFK string
TournamentFK string
TournamentTemplateFK string
TournamentStageFK string
TournamentStageName string
TournamentName string
TournamentTemplateName string
SportName string
Gender string
StartDate time.Time
StatusType string
StatusDescFK string
RoundTypeFK string
UpdatesCount int
LastUpdatedAt time.Time
}
// Full domain model
type EnetpulseFixture struct {
FixtureID string `json:"id"`
Name string `json:"name"`
SportFK string `json:"sportFK"`
TournamentFK string `json:"tournamentFK"`
TournamentTemplateFK string `json:"tournament_templateFK"`
// TournamentStageFK string `json:"tournament_stageFK"`
TournamentStageName string `json:"tournament_stage_name"`
TournamentName string `json:"tournament_name"`
TournamentTemplateName string `json:"tournament_template_name"`
SportName string `json:"sport_name"`
Gender string `json:"gender"`
StartDate string `json:"startdate"` // ISO 8601
StatusType string `json:"status_type"`
StatusDescFK string `json:"status_descFK"`
RoundTypeFK string `json:"round_typeFK"`
UpdatesCount string `json:"n"` // convert to int
LastUpdatedAt string `json:"ut"` // parse to time.Time
}
type CreateEnetpulseResult struct {
ResultID string `json:"result_id"`
Name string `json:"name"`
SportFK string `json:"sport_fk"`
TournamentFK string `json:"tournament_fk"`
TournamentTemplateFK string `json:"tournament_template_fk"`
// TournamentStageFK string `json:"tournament_stage_fk"`
TournamentStageName string `json:"tournament_stage_name"`
TournamentName string `json:"tournament_name"`
TournamentTemplateName string `json:"tournament_template_name"`
SportName string `json:"sport_name"`
StartDate time.Time `json:"start_date"`
StatusType string `json:"status_type"`
StatusDescFK string `json:"status_desc_fk"`
RoundTypeFK string `json:"round_type_fk"`
UpdatesCount int32 `json:"updates_count"`
LastUpdatedAt time.Time `json:"last_updated_at"`
// Optional metadata
Round string `json:"round"`
Live string `json:"live"`
VenueName string `json:"venue_name"`
LivestatsPlus string `json:"livestats_plus"`
LivestatsType string `json:"livestats_type"`
Commentary string `json:"commentary"`
LineupConfirmed bool `json:"lineup_confirmed"`
Verified bool `json:"verified"`
Spectators int32 `json:"spectators"`
// Time-related metadata
GameStarted *time.Time `json:"game_started"`
FirstHalfEnded *time.Time `json:"first_half_ended"`
SecondHalfStarted *time.Time `json:"second_half_started"`
SecondHalfEnded *time.Time `json:"second_half_ended"`
GameEnded *time.Time `json:"game_ended"`
}
// ✅ Used for reading result records
type EnetpulseResult struct {
ID int64 `json:"id"`
ResultID string `json:"result_id"`
Name string `json:"name"`
SportFK string `json:"sport_fk"`
TournamentFK string `json:"tournament_fk"`
TournamentTemplateFK string `json:"tournament_template_fk"`
// TournamentStageFK string `json:"tournament_stage_fk"`
TournamentStageName string `json:"tournament_stage_name"`
TournamentName string `json:"tournament_name"`
TournamentTemplateName string `json:"tournament_template_name"`
SportName string `json:"sport_name"`
StartDate time.Time `json:"start_date"`
StatusType string `json:"status_type"`
StatusDescFK string `json:"status_desc_fk"`
RoundTypeFK string `json:"round_type_fk"`
UpdatesCount int32 `json:"updates_count"`
LastUpdatedAt *time.Time `json:"last_updated_at"`
Round string `json:"round"`
Live string `json:"live"`
VenueName string `json:"venue_name"`
LivestatsPlus string `json:"livestats_plus"`
LivestatsType string `json:"livestats_type"`
Commentary string `json:"commentary"`
LineupConfirmed bool `json:"lineup_confirmed"`
Verified bool `json:"verified"`
Spectators int32 `json:"spectators"`
GameStarted *time.Time `json:"game_started"`
FirstHalfEnded *time.Time `json:"first_half_ended"`
SecondHalfStarted *time.Time `json:"second_half_started"`
SecondHalfEnded *time.Time `json:"second_half_ended"`
GameEnded *time.Time `json:"game_ended"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
}
type EnetpulseOutcomeType struct {
ID int64 `json:"id"`
OutcomeTypeID string `json:"outcome_type_id"` // changed from int64 → string
Name string `json:"name"`
Description string `json:"description"`
UpdatesCount int32 `json:"updates_count"`
LastUpdatedAt time.Time `json:"last_updated_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateEnetpulseOutcomeType represents the payload to create or update an outcome type.
type CreateEnetpulseOutcomeType struct {
OutcomeTypeID string `json:"outcome_type_id"` // changed from int64 → string
Name string `json:"name"`
Description string `json:"description"`
UpdatesCount int32 `json:"updates_count"`
LastUpdatedAt time.Time `json:"last_updated_at"`
}
type EnetpulsePreoddsTemp struct {
PreoddsID string `json:"preodds_id"`
EventFK string `json:"event_fk"`
OutcomeTypeFK string `json:"outcome_type_fk"`
OutcomeScopeFK string `json:"outcome_scope_fk"`
OutcomeSubtypeFK string `json:"outcome_subtype_fk"`
EventParticipantNumber int `json:"event_participant_number"`
IParam string `json:"iparam"`
IParam2 string `json:"iparam2"`
DParam string `json:"dparam"`
DParam2 string `json:"dparam2"`
SParam string `json:"sparam"`
UpdatesCount int `json:"updates_count"`
LastUpdatedAt time.Time `json:"last_updated_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateEnetpulsePreodds is used when inserting a new preodds record
type CreateEnetpulsePreodds struct {
PreoddsID string `json:"preodds_id"`
EventFK string `json:"event_fk"`
OutcomeTypeFK string `json:"outcome_type_fk"`
OutcomeScopeFK string `json:"outcome_scope_fk"`
OutcomeSubtypeFK string `json:"outcome_subtype_fk"`
EventParticipantNumber int `json:"event_participant_number"`
IParam string `json:"iparam"`
IParam2 string `json:"iparam2"`
DParam string `json:"dparam"`
DParam2 string `json:"dparam2"`
SParam string `json:"sparam"`
UpdatesCount int `json:"updates_count"`
LastUpdatedAt time.Time `json:"last_updated_at"`
}
type CreateEnetpulsePreoddsBettingOffer struct {
BettingOfferID string
PreoddsFK string
BettingOfferStatusFK int32
OddsProviderFK int32
Odds float64
OddsOld float64
Active string
CouponKey string
UpdatesCount int
LastUpdatedAt time.Time
}
// EnetpulsePreoddsBettingOffer represents the DB record of a betting offer
type EnetpulsePreoddsBettingOffer struct {
ID int64 `json:"id"`
BettingOfferID string `json:"betting_offer_id"`
PreoddsFK string `json:"preodds_fk"`
BettingOfferStatusFK int32 `json:"betting_offer_status_fk"`
OddsProviderFK int32 `json:"odds_provider_fk"`
Odds float64 `json:"odds"`
OddsOld float64 `json:"odds_old"`
Active string `json:"active"`
CouponKey string `json:"coupon_key"`
UpdatesCount int `json:"updates_count"`
LastUpdatedAt time.Time `json:"last_updated_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type EnetpulseFixtureWithPreodds struct {
FixtureID string
FixtureApiID string
FixtureName string
SportFk string
TournamentFk string
TournamentTemplateFk string
TournamentStageFk string
StartDate time.Time
StatusType string
StatusDescFk string
RoundTypeFk string
UpdatesCount int32
LastUpdatedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
Preodds []EnetpulsePreodds
}
type EnetpulsePreodds struct {
ID int64
PreoddsID string
EventFK int64
OutcomeTypeFK int32
OutcomeScopeFK int32
OutcomeSubtypeFK int32
EventParticipantNumber int32
IParam string
IParam2 string
DParam string
DParam2 string
SParam string
UpdatesCount int32
LastUpdatedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
BettingOffers []EnetpulsePreoddsBettingOffer
}
type EnetpulseResultParticipant struct {
ID int64 `json:"id"`
ParticipantMapID string `json:"participant_map_id"`
ResultFk string `json:"result_fk"`
ParticipantFk string `json:"participant_fk"`
Number int32 `json:"number"`
Name string `json:"name"`
Gender string `json:"gender"`
Type string `json:"type"`
CountryFk string `json:"country_fk"`
CountryName string `json:"country_name"`
OrdinaryTime string `json:"ordinary_time"`
RunningScore string `json:"running_score"`
Halftime string `json:"halftime"`
FinalResult string `json:"final_result"`
LastUpdatedAt time.Time `json:"last_updated_at"`
CreatedAt time.Time `json:"created_at"`
}
// CreateEnetpulseResultParticipant is the payload for inserting or updating a participant record.
type CreateEnetpulseResultParticipant struct {
ParticipantMapID string `json:"participant_map_id"`
ResultFk string `json:"result_fk"`
ParticipantFk string `json:"participant_fk"`
Number int32 `json:"number"`
Name string `json:"name"`
Gender string `json:"gender"`
Type string `json:"type"`
CountryFk string `json:"country_fk"`
CountryName string `json:"country_name"`
OrdinaryTime string `json:"ordinary_time"`
RunningScore string `json:"running_score"`
Halftime string `json:"halftime"`
FinalResult string `json:"final_result"`
LastUpdatedAt time.Time `json:"last_updated_at"`
}
type EnetpulseResultReferee struct {
ID int64 `json:"id"`
ResultFk string `json:"result_fk"`
RefereeFk string `json:"referee_fk"`
Assistant1RefereeFk string `json:"assistant1_referee_fk"`
Assistant2RefereeFk string `json:"assistant2_referee_fk"`
FourthRefereeFk string `json:"fourth_referee_fk"`
Var1RefereeFk string `json:"var1_referee_fk"`
Var2RefereeFk string `json:"var2_referee_fk"`
LastUpdatedAt time.Time `json:"last_updated_at"`
CreatedAt time.Time `json:"created_at"`
}
// CreateEnetpulseResultReferee is the payload for inserting or updating referee assignments.
type CreateEnetpulseResultReferee struct {
ResultFk string `json:"result_fk"`
RefereeFk string `json:"referee_fk"`
Assistant1RefereeFk string `json:"assistant1_referee_fk"`
Assistant2RefereeFk string `json:"assistant2_referee_fk"`
FourthRefereeFk string `json:"fourth_referee_fk"`
Var1RefereeFk string `json:"var1_referee_fk"`
Var2RefereeFk string `json:"var2_referee_fk"`
LastUpdatedAt time.Time `json:"last_updated_at"`
}

View File

@ -58,10 +58,10 @@ type GameStartRequest struct {
Country string `json:"country"` Country string `json:"country"`
IP string `json:"ip"` IP string `json:"ip"`
BrandID string `json:"brandId"` BrandID string `json:"brandId"`
UserAgent string `json:"userAgent,omitempty"` // UserAgent string `json:"userAgent,omitempty"`
LobbyURL string `json:"lobbyUrl,omitempty"` // LobbyURL string `json:"lobbyUrl,omitempty"`
CashierURL string `json:"cashierUrl,omitempty"` // CashierURL string `json:"cashierUrl,omitempty"`
PlayerName string `json:"playerName,omitempty"` // PlayerName string `json:"playerName,omitempty"`
} }
type DemoGameRequest struct { type DemoGameRequest struct {
@ -71,8 +71,8 @@ type DemoGameRequest struct {
DeviceType string `json:"deviceType"` DeviceType string `json:"deviceType"`
IP string `json:"ip"` IP string `json:"ip"`
BrandID string `json:"brandId"` BrandID string `json:"brandId"`
PlayerID string `json:"playerId,omitempty"` // PlayerID string `json:"playerId,omitempty"`
Country string `json:"country,omitempty"` // Country string `json:"country,omitempty"`
} }
type GameStartResponse struct { type GameStartResponse struct {

View File

@ -316,3 +316,61 @@ type UnifiedGame struct {
Status int `json:"status,omitempty"` Status int `json:"status,omitempty"`
DemoURL string `json:"demoUrl"` DemoURL string `json:"demoUrl"`
} }
type CreateVirtualGameProviderReport struct {
ProviderID string `json:"provider_id"`
ReportDate time.Time `json:"report_date"`
TotalGamesPlayed int64 `json:"total_games_played"`
TotalBets float64 `json:"total_bets"`
TotalPayouts float64 `json:"total_payouts"`
TotalPlayers int64 `json:"total_players"`
ReportType string `json:"report_type"` // e.g., "daily", "weekly"
}
type VirtualGameProviderReport struct {
ID int64 `json:"id"`
ProviderID string `json:"provider_id"`
ReportDate time.Time `json:"report_date"`
TotalGamesPlayed int64 `json:"total_games_played"`
TotalBets float64 `json:"total_bets"`
TotalPayouts float64 `json:"total_payouts"`
TotalProfit float64 `json:"total_profit"`
TotalPlayers int64 `json:"total_players"`
ReportType string `json:"report_type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateVirtualGameReport struct {
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
ReportDate time.Time `json:"report_date"`
TotalRounds int64 `json:"total_rounds"`
TotalBets float64 `json:"total_bets"`
TotalPayouts float64 `json:"total_payouts"`
TotalPlayers int64 `json:"total_players"`
ReportType string `json:"report_type"` // e.g., "daily", "weekly"
}
type VirtualGameReport struct {
ID int64 `json:"id"`
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
ReportDate time.Time `json:"report_date"`
TotalRounds int64 `json:"total_rounds"`
TotalBets float64 `json:"total_bets"`
TotalPayouts float64 `json:"total_payouts"`
TotalProfit float64 `json:"total_profit"`
TotalPlayers int64 `json:"total_players"`
ReportType string `json:"report_type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateVirtualGameProviderReportsRequest struct {
Reports []CreateVirtualGameProviderReport
}
type CreateVirtualGameReportsRequest struct {
Reports []CreateVirtualGameReport
}

View File

@ -3,6 +3,9 @@ package repository
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"math/big"
"strconv"
"time" "time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
@ -140,6 +143,295 @@ func (s *Store) GetTournamentStagesByTournamentFK(ctx context.Context, tournamen
return stages, nil return stages, nil
} }
// Create a new fixture
func (s *Store) CreateEnetpulseFixture(
ctx context.Context,
fixture domain.CreateEnetpulseFixture,
) (domain.EnetpulseFixture, error) {
// Convert domain model to DB params (sqlc-generated struct or parameters)
dbFixture, err := s.queries.CreateEnetpulseFixture(
ctx,
ConvertCreateEnetpulseFixture(fixture), // your converter
)
if err != nil {
return domain.EnetpulseFixture{}, err
}
return ConvertDBEnetpulseFixture(dbFixture), nil // convert DB row to domain
}
// Fetch all fixtures
func (s *Store) GetAllEnetpulseFixtures(ctx context.Context) ([]domain.EnetpulseFixture, error) {
dbFixtures, err := s.queries.GetAllEnetpulseFixtures(ctx)
if err != nil {
return nil, err
}
var fixtures []domain.EnetpulseFixture
for _, dbFixture := range dbFixtures {
fixtures = append(fixtures, ConvertDBEnetpulseFixture(dbFixture))
}
return fixtures, nil
}
func (s *Store) CreateEnetpulseResult(
ctx context.Context,
result domain.CreateEnetpulseResult,
) (domain.EnetpulseResult, error) {
dbResult, err := s.queries.CreateEnetpulseResult(
ctx,
ConvertCreateEnetpulseResult(result),
)
if err != nil {
return domain.EnetpulseResult{}, err
}
return ConvertDBEnetpulseResult(dbResult), nil
}
// GetAllEnetpulseResults retrieves all Enetpulse results.
func (s *Store) GetAllEnetpulseResults(ctx context.Context) ([]domain.EnetpulseResult, error) {
dbResults, err := s.queries.GetAllEnetpulseResults(ctx)
if err != nil {
return nil, err
}
results := make([]domain.EnetpulseResult, 0, len(dbResults))
for _, dbR := range dbResults {
results = append(results, ConvertDBEnetpulseResult(dbR))
}
return results, nil
}
// CreateEnetpulseOutcomeType inserts or updates an EnetPulse outcome type record.
func (s *Store) CreateEnetpulseOutcomeType(
ctx context.Context,
outcomeType domain.CreateEnetpulseOutcomeType,
) (domain.EnetpulseOutcomeType, error) {
dbOutcome, err := s.queries.CreateEnetpulseOutcomeType(
ctx,
ConvertCreateEnetpulseOutcomeType(outcomeType),
)
if err != nil {
return domain.EnetpulseOutcomeType{}, err
}
return ConvertDBEnetpulseOutcomeType(dbOutcome), nil
}
// GetAllEnetpulseOutcomeTypes retrieves all outcome types.
func (s *Store) GetAllEnetpulseOutcomeTypes(ctx context.Context) ([]domain.EnetpulseOutcomeType, error) {
dbOutcomes, err := s.queries.GetAllEnetpulseOutcomeTypes(ctx)
if err != nil {
return nil, err
}
outcomes := make([]domain.EnetpulseOutcomeType, 0, len(dbOutcomes))
for _, dbO := range dbOutcomes {
outcomes = append(outcomes, ConvertDBEnetpulseOutcomeType(dbO))
}
return outcomes, nil
}
// CreateEnetpulsePreodds inserts or updates a preodds record.
func (s *Store) CreateEnetpulsePreodds(
ctx context.Context,
preodds domain.CreateEnetpulsePreodds,
) (domain.EnetpulsePreodds, error) {
// Convert domain to DB params
params, err := ConvertCreateEnetpulsePreodds(preodds)
if err != nil {
return domain.EnetpulsePreodds{}, err
}
// Insert into DB
dbPreodds, err := s.queries.CreateEnetpulsePreodds(ctx, params)
if err != nil {
return domain.EnetpulsePreodds{}, err
}
return ConvertDBEnetpulsePreodds(dbPreodds), nil
}
// GetAllEnetpulsePreodds retrieves all preodds records.
func (s *Store) GetAllEnetpulsePreodds(ctx context.Context) ([]domain.EnetpulsePreodds, error) {
dbPreodds, err := s.queries.GetAllEnetpulsePreodds(ctx)
if err != nil {
return nil, err
}
preodds := make([]domain.EnetpulsePreodds, 0, len(dbPreodds))
for _, dbP := range dbPreodds {
preodds = append(preodds, ConvertDBEnetpulsePreodds(dbP))
}
return preodds, nil
}
// CreateEnetpulsePreoddsBettingOffer inserts or updates a betting offer
func (s *Store) CreateEnetpulsePreoddsBettingOffer(
ctx context.Context,
bettingOffer domain.CreateEnetpulsePreoddsBettingOffer,
) (domain.EnetpulsePreoddsBettingOffer, error) {
params := ConvertCreateEnetpulsePreoddsBettingOffer(bettingOffer)
dbOffer, err := s.queries.CreateEnetpulsePreoddsBettingOffer(ctx, params)
if err != nil {
return domain.EnetpulsePreoddsBettingOffer{}, err
}
return ConvertDBEnetpulsePreoddsBettingOffer(dbOffer), nil
}
// GetAllEnetpulsePreoddsBettingOffers retrieves all betting offers
func (s *Store) GetAllEnetpulsePreoddsBettingOffers(ctx context.Context) ([]domain.EnetpulsePreoddsBettingOffer, error) {
dbOffers, err := s.queries.GetAllEnetpulsePreoddsBettingOffers(ctx)
if err != nil {
return nil, err
}
offers := make([]domain.EnetpulsePreoddsBettingOffer, 0, len(dbOffers))
for _, dbO := range dbOffers {
offers = append(offers, ConvertDBEnetpulsePreoddsBettingOffer(dbO))
}
return offers, nil
}
func (s *Store) GetAllEnetpulsePreoddsWithBettingOffers(ctx context.Context) ([]domain.EnetpulsePreodds, error) {
rows, err := s.queries.GetAllEnetpulsePreoddsWithBettingOffers(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch preodds with betting offers: %w", err)
}
// Map for grouping betting offers under each Preodd
preoddsMap := make(map[string]*domain.EnetpulsePreodds)
for _, row := range rows {
pid := row.PreoddsID
preodd, exists := preoddsMap[pid]
if !exists {
// Create the base Preodd entry
preodd = &domain.EnetpulsePreodds{
ID: row.PreoddsDbID,
PreoddsID: row.PreoddsID,
EventFK: row.EventFk,
OutcomeTypeFK: row.OutcomeTypeFk.Int32,
OutcomeScopeFK: row.OutcomeScopeFk.Int32,
OutcomeSubtypeFK: row.OutcomeSubtypeFk.Int32,
EventParticipantNumber: row.EventParticipantNumber.Int32,
IParam: row.Iparam.String,
IParam2: row.Iparam2.String,
DParam: row.Dparam.String,
DParam2: row.Dparam2.String,
SParam: row.Sparam.String,
UpdatesCount: row.PreoddsUpdatesCount.Int32,
LastUpdatedAt: row.PreoddsLastUpdatedAt.Time,
CreatedAt: row.PreoddsCreatedAt.Time,
UpdatedAt: row.PreoddsUpdatedAt.Time,
BettingOffers: []domain.EnetpulsePreoddsBettingOffer{},
}
preoddsMap[pid] = preodd
}
// Append BettingOffer only if exists
if row.BettingofferID.Valid && row.BettingofferID.String != "" {
offer := domain.EnetpulsePreoddsBettingOffer{
ID: row.BettingofferDbID.Int64,
BettingOfferID: row.BettingofferID.String,
BettingOfferStatusFK: row.BettingofferStatusFk.Int32,
OddsProviderFK: row.OddsProviderFk.Int32,
Odds: float64(row.Odds.Exp),
OddsOld: float64(row.OddsOld.Exp),
Active: fmt.Sprintf("%v", row.Active),
CouponKey: row.CouponKey.String,
UpdatesCount: int(row.BettingofferUpdatesCount.Int32),
LastUpdatedAt: row.BettingofferLastUpdatedAt.Time,
CreatedAt: row.BettingofferCreatedAt.Time,
UpdatedAt: row.BettingofferUpdatedAt.Time,
}
preodd.BettingOffers = append(preodd.BettingOffers, offer)
}
}
// Convert map to slice
result := make([]domain.EnetpulsePreodds, 0, len(preoddsMap))
for _, p := range preoddsMap {
result = append(result, *p)
}
return result, nil
}
func (s *Store) GetFixturesWithPreodds(ctx context.Context) ([]domain.EnetpulseFixtureWithPreodds, error) {
dbRows, err := s.queries.GetFixturesWithPreodds(ctx)
if err != nil {
return nil, err
}
// Use a map to group preodds by fixture
fixtureMap := make(map[string]*domain.EnetpulseFixtureWithPreodds)
for _, row := range dbRows {
// If fixture not yet in map, add it
if _, exists := fixtureMap[row.FixtureID]; !exists {
fixtureMap[row.FixtureID] = &domain.EnetpulseFixtureWithPreodds{
FixtureID: row.FixtureID,
FixtureApiID: row.FixtureID, // same alias used in query
FixtureName: row.FixtureName,
SportFk: row.SportFk,
TournamentFk: row.TournamentFk.String,
TournamentTemplateFk: row.TournamentTemplateFk.String,
// TournamentStageFk: row.TournamentStageFk.String,
StartDate: row.StartDate.Time,
StatusType: row.StatusType.String,
StatusDescFk: row.StatusDescFk.String,
RoundTypeFk: row.RoundTypeFk.String,
UpdatesCount: row.FixtureUpdatesCount.Int32,
LastUpdatedAt: row.FixtureLastUpdatedAt.Time,
CreatedAt: row.FixtureCreatedAt.Time,
UpdatedAt: row.FixtureUpdatedAt.Time,
Preodds: []domain.EnetpulsePreodds{}, // initialize slice
}
}
// Add preodds only if it exists (avoid NULL rows)
if row.PreoddsDbID.Valid {
preodds := domain.EnetpulsePreodds{
ID: row.PreoddsDbID.Int64,
PreoddsID: row.PreoddsID.String,
EventFK: row.EventFk.Int64,
OutcomeTypeFK: row.OutcomeTypeFk.Int32,
OutcomeScopeFK: row.OutcomeScopeFk.Int32,
OutcomeSubtypeFK: row.OutcomeSubtypeFk.Int32,
EventParticipantNumber: row.EventParticipantNumber.Int32,
IParam: row.Iparam.String,
IParam2: row.Iparam2.String,
DParam: row.Dparam.String,
DParam2: row.Dparam2.String,
SParam: row.Sparam.String,
UpdatesCount: row.PreoddsUpdatesCount.Int32,
LastUpdatedAt: row.PreoddsLastUpdatedAt.Time,
CreatedAt: row.PreoddsCreatedAt.Time,
UpdatedAt: row.PreoddsUpdatedAt.Time,
}
fixtureMap[row.FixtureID].Preodds = append(fixtureMap[row.FixtureID].Preodds, preodds)
}
}
// Flatten the map into a slice
result := make([]domain.EnetpulseFixtureWithPreodds, 0, len(fixtureMap))
for _, f := range fixtureMap {
result = append(result, *f)
}
return result, nil
}
// func ConvertCreateEnetpulseTournamentStage(stage domain.CreateEnetpulseTournamentStage) dbgen.EnetpulseTournamentStage { // func ConvertCreateEnetpulseTournamentStage(stage domain.CreateEnetpulseTournamentStage) dbgen.EnetpulseTournamentStage {
// return dbgen.EnetpulseTournamentStage{ // return dbgen.EnetpulseTournamentStage{
// StageID: stage.StageID, // StageID: stage.StageID,
@ -157,6 +449,54 @@ func (s *Store) GetTournamentStagesByTournamentFK(ctx context.Context, tournamen
// } // }
// } // }
// ConvertCreateEnetpulseFixture converts the domain model to the SQLC params struct.
func ConvertCreateEnetpulseFixture(f domain.CreateEnetpulseFixture) dbgen.CreateEnetpulseFixtureParams {
return dbgen.CreateEnetpulseFixtureParams{
FixtureID: f.FixtureID,
Name: f.Name,
SportFk: f.SportFK,
TournamentFk: pgtype.Text{String: f.TournamentFK, Valid: f.TournamentFK != ""},
TournamentTemplateFk: pgtype.Text{String: f.TournamentTemplateFK, Valid: f.TournamentTemplateFK != ""},
// TournamentStageFk: pgtype.Text{String: f.TournamentStageFK, Valid: f.TournamentStageFK != ""},
// TournamentStageName: pgtype.Text{String: f.TournamentStageName, Valid: f.TournamentStageName != ""},
TournamentName: pgtype.Text{String: f.TournamentName, Valid: f.TournamentName != ""},
TournamentTemplateName: pgtype.Text{String: f.TournamentTemplateName, Valid: f.TournamentTemplateName != ""},
SportName: pgtype.Text{String: f.SportName, Valid: f.SportName != ""},
Gender: pgtype.Text{String: f.Gender, Valid: f.Gender != ""},
StartDate: pgtype.Timestamptz{Time: f.StartDate, Valid: !f.StartDate.IsZero()},
StatusType: pgtype.Text{String: f.StatusType, Valid: f.StatusType != ""},
StatusDescFk: pgtype.Text{String: f.StatusDescFK, Valid: f.StatusDescFK != ""},
RoundTypeFk: pgtype.Text{String: f.RoundTypeFK, Valid: f.RoundTypeFK != ""},
UpdatesCount: pgtype.Int4{Int32: int32(f.UpdatesCount), Valid: true},
LastUpdatedAt: pgtype.Timestamptz{Time: f.LastUpdatedAt, Valid: !f.LastUpdatedAt.IsZero()},
}
}
// ConvertDBEnetpulseFixture converts the DB row to the domain model.
func ConvertDBEnetpulseFixture(dbF dbgen.EnetpulseFixture) domain.EnetpulseFixture {
return domain.EnetpulseFixture{
FixtureID: dbF.FixtureID,
Name: dbF.Name,
SportFK: dbF.SportFk,
TournamentFK: dbF.TournamentFk.String,
TournamentTemplateFK: dbF.TournamentTemplateFk.String,
// TournamentStageFK: dbF.TournamentStageFk.String,
// TournamentStageName: dbF.TournamentStageName.String,
TournamentName: dbF.TournamentName.String,
TournamentTemplateName: dbF.TournamentTemplateName.String,
SportName: dbF.SportName.String,
Gender: dbF.Gender.String,
StartDate: dbF.StartDate.Time.String(),
StatusType: dbF.StatusType.String,
StatusDescFK: dbF.StatusDescFk.String,
RoundTypeFK: dbF.RoundTypeFk.String,
UpdatesCount: fmt.Sprintf("%v", dbF.UpdatesCount),
LastUpdatedAt: dbF.LastUpdatedAt.Time.String(),
// CreatedAt: dbF.CreatedAt.Time,
// UpdatedAt: dbF.UpdatedAt.Time,
}
}
func ConvertCreateEnetpulseTournamentStage(stage domain.CreateEnetpulseTournamentStage) dbgen.CreateEnetpulseTournamentStageParams { func ConvertCreateEnetpulseTournamentStage(stage domain.CreateEnetpulseTournamentStage) dbgen.CreateEnetpulseTournamentStageParams {
return dbgen.CreateEnetpulseTournamentStageParams{ return dbgen.CreateEnetpulseTournamentStageParams{
StageID: stage.StageID, StageID: stage.StageID,
@ -321,3 +661,354 @@ func ConvertDBEnetpulseTournament(dbT dbgen.EnetpulseTournament) domain.Enetpuls
}(), }(),
} }
} }
func ConvertCreateEnetpulseResult(input domain.CreateEnetpulseResult) dbgen.CreateEnetpulseResultParams {
return dbgen.CreateEnetpulseResultParams{
ResultID: input.ResultID,
Name: input.Name,
SportFk: input.SportFK,
TournamentFk: pgtype.Text{String: input.TournamentFK, Valid: input.TournamentFK != ""},
TournamentTemplateFk: pgtype.Text{String: input.TournamentTemplateFK, Valid: input.TournamentTemplateFK != ""},
// TournamentStageFk: pgtype.Text{String: input.TournamentStageFK, Valid: input.TournamentStageFK != ""},
// TournamentStageName: pgtype.Text{String: input.TournamentStageName, Valid: input.TournamentStageName != ""},
TournamentName: pgtype.Text{String: input.TournamentName, Valid: input.TournamentName != ""},
TournamentTemplateName: pgtype.Text{String: input.TournamentTemplateName, Valid: input.TournamentTemplateName != ""},
SportName: pgtype.Text{String: input.SportName, Valid: input.SportName != ""},
StartDate: pgtype.Timestamptz{Time: input.StartDate, Valid: !input.StartDate.IsZero()},
StatusType: pgtype.Text{String: input.StatusType, Valid: input.StatusType != ""},
StatusDescFk: pgtype.Text{String: input.StatusDescFK, Valid: input.StatusDescFK != ""},
RoundTypeFk: pgtype.Text{String: input.RoundTypeFK, Valid: input.RoundTypeFK != ""},
UpdatesCount: pgtype.Int4{Int32: int32(input.UpdatesCount), Valid: true},
LastUpdatedAt: pgtype.Timestamptz{Time: input.LastUpdatedAt, Valid: !input.LastUpdatedAt.IsZero()},
Round: pgtype.Text{String: input.Round, Valid: input.Round != ""},
Live: pgtype.Text{String: input.Live, Valid: input.Live != ""},
VenueName: pgtype.Text{String: input.VenueName, Valid: input.VenueName != ""},
LivestatsPlus: pgtype.Text{String: input.LivestatsPlus, Valid: input.LivestatsPlus != ""},
LivestatsType: pgtype.Text{String: input.LivestatsType, Valid: input.LivestatsType != ""},
Commentary: pgtype.Text{String: input.Commentary, Valid: input.Commentary != ""},
LineupConfirmed: pgtype.Bool{Bool: input.LineupConfirmed, Valid: true},
Verified: pgtype.Bool{Bool: input.Verified, Valid: true},
Spectators: pgtype.Int4{Int32: int32(input.Spectators), Valid: true},
GameStarted: pgtype.Timestamptz{Time: *input.GameStarted, Valid: !input.GameStarted.IsZero()},
FirstHalfEnded: pgtype.Timestamptz{Time: *input.FirstHalfEnded, Valid: !input.FirstHalfEnded.IsZero()},
SecondHalfStarted: pgtype.Timestamptz{Time: *input.SecondHalfStarted, Valid: !input.SecondHalfStarted.IsZero()},
SecondHalfEnded: pgtype.Timestamptz{Time: *input.SecondHalfEnded, Valid: !input.SecondHalfEnded.IsZero()},
GameEnded: pgtype.Timestamptz{Time: *input.GameEnded, Valid: !input.GameEnded.IsZero()},
}
}
// ConvertDBEnetpulseResult maps SQLC result → domain model
func ConvertDBEnetpulseResult(db dbgen.EnetpulseResult) domain.EnetpulseResult {
return domain.EnetpulseResult{
ID: db.ID,
ResultID: db.ResultID,
Name: db.Name,
SportFK: db.SportFk,
TournamentFK: db.TournamentFk.String,
TournamentTemplateFK: db.TournamentTemplateFk.String,
// TournamentStageFK: db.TournamentStageFk.String,
// TournamentStageName: db.TournamentStageName.String,
TournamentName: db.TournamentName.String,
TournamentTemplateName: db.TournamentTemplateName.String,
SportName: db.SportName.String,
StartDate: db.StartDate.Time,
StatusType: db.StatusType.String,
StatusDescFK: db.StatusDescFk.String,
RoundTypeFK: db.RoundTypeFk.String,
UpdatesCount: db.UpdatesCount.Int32,
LastUpdatedAt: &db.LastUpdatedAt.Time,
Round: db.Round.String,
Live: db.Live.String,
VenueName: db.VenueName.String,
LivestatsPlus: db.LivestatsPlus.String,
LivestatsType: db.LivestatsType.String,
Commentary: db.Commentary.String,
LineupConfirmed: db.LineupConfirmed.Bool,
Verified: db.Verified.Bool,
Spectators: db.Spectators.Int32,
GameStarted: &db.GameStarted.Time,
FirstHalfEnded: &db.FirstHalfEnded.Time,
SecondHalfStarted: &db.SecondHalfStarted.Time,
SecondHalfEnded: &db.SecondHalfEnded.Time,
GameEnded: &db.GameEnded.Time,
CreatedAt: db.CreatedAt.Time,
UpdatedAt: &db.UpdatedAt.Time,
}
}
// ConvertCreateEnetpulseOutcomeType converts the domain struct to SQLC params.
func ConvertCreateEnetpulseOutcomeType(o domain.CreateEnetpulseOutcomeType) dbgen.CreateEnetpulseOutcomeTypeParams {
return dbgen.CreateEnetpulseOutcomeTypeParams{
OutcomeTypeID: o.OutcomeTypeID,
Name: o.Name,
Description: pgtype.Text{String: o.Description, Valid: o.Description != ""}, // TODO: thiso.Description,
UpdatesCount: pgtype.Int4{Int32: int32(o.UpdatesCount), Valid: true},
LastUpdatedAt: pgtype.Timestamptz{Time: o.LastUpdatedAt, Valid: !o.LastUpdatedAt.IsZero()},
}
}
// ConvertDBEnetpulseOutcomeType converts SQLC DB model to domain model.
func ConvertDBEnetpulseOutcomeType(dbO dbgen.EnetpulseOutcomeType) domain.EnetpulseOutcomeType {
return domain.EnetpulseOutcomeType{
ID: dbO.ID,
OutcomeTypeID: dbO.OutcomeTypeID,
Name: dbO.Name,
Description: dbO.Description.String,
UpdatesCount: dbO.UpdatesCount.Int32,
LastUpdatedAt: dbO.LastUpdatedAt.Time,
CreatedAt: dbO.CreatedAt.Time,
UpdatedAt: dbO.UpdatedAt.Time,
}
}
func ConvertCreateEnetpulsePreodds(p domain.CreateEnetpulsePreodds) (dbgen.CreateEnetpulsePreoddsParams, error) {
eventFK, err := strconv.ParseInt(p.EventFK, 10, 64)
if err != nil {
return dbgen.CreateEnetpulsePreoddsParams{}, fmt.Errorf("invalid EventFK: %w", err)
}
outcomeTypeFK, err := strconv.ParseInt(p.OutcomeTypeFK, 10, 32)
if err != nil {
return dbgen.CreateEnetpulsePreoddsParams{}, fmt.Errorf("invalid OutcomeTypeFK: %w", err)
}
outcomeScopeFK, err := strconv.ParseInt(p.OutcomeScopeFK, 10, 32)
if err != nil {
return dbgen.CreateEnetpulsePreoddsParams{}, fmt.Errorf("invalid OutcomeScopeFK: %w", err)
}
outcomeSubtypeFK, err := strconv.ParseInt(p.OutcomeSubtypeFK, 10, 32)
if err != nil {
return dbgen.CreateEnetpulsePreoddsParams{}, fmt.Errorf("invalid OutcomeSubtypeFK: %w", err)
}
return dbgen.CreateEnetpulsePreoddsParams{
PreoddsID: p.PreoddsID,
EventFk: eventFK,
OutcomeTypeFk: pgtype.Int4{Int32: int32(outcomeTypeFK), Valid: true},
OutcomeScopeFk: pgtype.Int4{Int32: int32(outcomeScopeFK), Valid: true},
OutcomeSubtypeFk: pgtype.Int4{Int32: int32(outcomeSubtypeFK), Valid: true},
EventParticipantNumber: pgtype.Int4{Int32: int32(p.EventParticipantNumber), Valid: true},
Iparam: pgtype.Text{String: p.IParam, Valid: p.IParam != ""},
Iparam2: pgtype.Text{String: p.IParam2, Valid: p.IParam2 != ""},
Dparam: pgtype.Text{String: p.DParam, Valid: p.DParam != ""},
Dparam2: pgtype.Text{String: p.DParam2, Valid: p.DParam2 != ""},
Sparam: pgtype.Text{String: p.SParam, Valid: p.SParam != ""},
UpdatesCount: pgtype.Int4{Int32: int32(p.UpdatesCount), Valid: true},
LastUpdatedAt: pgtype.Timestamptz{Time: p.LastUpdatedAt, Valid: !p.LastUpdatedAt.IsZero()},
}, nil
}
func ConvertDBEnetpulsePreodds(dbP dbgen.EnetpulsePreodd) domain.EnetpulsePreodds {
return domain.EnetpulsePreodds{
PreoddsID: dbP.PreoddsID,
EventFK: dbP.EventFk,
OutcomeTypeFK: dbP.OutcomeTypeFk.Int32,
OutcomeScopeFK: dbP.OutcomeScopeFk.Int32,
OutcomeSubtypeFK: dbP.OutcomeSubtypeFk.Int32,
EventParticipantNumber: dbP.EventParticipantNumber.Int32,
IParam: dbP.Iparam.String,
IParam2: dbP.Iparam2.String,
DParam: dbP.Dparam.String,
DParam2: dbP.Dparam2.String,
SParam: dbP.Sparam.String,
UpdatesCount: dbP.UpdatesCount.Int32,
LastUpdatedAt: dbP.LastUpdatedAt.Time,
CreatedAt: dbP.CreatedAt.Time,
UpdatedAt: dbP.UpdatedAt.Time,
}
}
func ConvertCreateEnetpulsePreoddsBettingOffer(o domain.CreateEnetpulsePreoddsBettingOffer) dbgen.CreateEnetpulsePreoddsBettingOfferParams {
// Convert float64 to int64 with scale 2
oddsInt := big.NewInt(int64(math.Round(o.Odds * 100)))
oddsOldInt := big.NewInt(int64(math.Round(o.OddsOld * 100)))
return dbgen.CreateEnetpulsePreoddsBettingOfferParams{
BettingofferID: o.BettingOfferID,
PreoddsFk: o.PreoddsFK,
BettingofferStatusFk: pgtype.Int4{Int32: o.BettingOfferStatusFK, Valid: true},
OddsProviderFk: pgtype.Int4{Int32: o.OddsProviderFK, Valid: true},
Odds: pgtype.Numeric{
Int: oddsInt,
Exp: -2, // scale 2 decimal places
Valid: true,
},
OddsOld: pgtype.Numeric{
Int: oddsOldInt,
Exp: -2,
Valid: true,
},
Active: pgtype.Bool{Bool: o.Active == "yes", Valid: true},
CouponKey: pgtype.Text{
String: o.CouponKey,
Valid: o.CouponKey != "",
},
UpdatesCount: pgtype.Int4{Int32: int32(o.UpdatesCount), Valid: true},
LastUpdatedAt: pgtype.Timestamptz{Time: o.LastUpdatedAt, Valid: !o.LastUpdatedAt.IsZero()},
}
}
// Convert DB result to domain struct
func ConvertDBEnetpulsePreoddsBettingOffer(o dbgen.EnetpulsePreoddsBettingoffer) domain.EnetpulsePreoddsBettingOffer {
var odds, oddsOld float64
if o.Odds.Valid {
odds, _ = o.Odds.Int.Float64() // Convert pgtype.Numeric to float64
}
if o.OddsOld.Valid {
oddsOld, _ = o.OddsOld.Int.Float64()
}
active := "no"
if o.Active.Valid && o.Active.Bool {
active = "yes"
}
return domain.EnetpulsePreoddsBettingOffer{
ID: o.ID,
BettingOfferID: o.BettingofferID,
PreoddsFK: o.PreoddsFk,
BettingOfferStatusFK: o.BettingofferStatusFk.Int32,
OddsProviderFK: o.OddsProviderFk.Int32,
Odds: odds,
OddsOld: oddsOld,
Active: active,
CouponKey: o.CouponKey.String,
UpdatesCount: int(o.UpdatesCount.Int32),
LastUpdatedAt: o.LastUpdatedAt.Time,
CreatedAt: o.CreatedAt.Time,
UpdatedAt: o.UpdatedAt.Time,
}
}
func (s *Store) CreateEnetpulseResultParticipant(
ctx context.Context,
participant domain.CreateEnetpulseResultParticipant,
) (domain.EnetpulseResultParticipant, error) {
dbParticipant, err := s.queries.CreateEnetpulseResultParticipant(
ctx,
ConvertCreateEnetpulseResultParticipant(participant),
)
if err != nil {
return domain.EnetpulseResultParticipant{}, err
}
return ConvertDBEnetpulseResultParticipant(dbParticipant), nil
}
func (s *Store) GetEnetpulseResultParticipantsByResultFK(
ctx context.Context,
resultFK string,
) ([]domain.EnetpulseResultParticipant, error) {
dbParticipants, err := s.queries.GetEnetpulseResultParticipantsByResultFK(ctx, resultFK)
if err != nil {
return nil, err
}
participants := make([]domain.EnetpulseResultParticipant, 0, len(dbParticipants))
for _, dbp := range dbParticipants {
participants = append(participants, ConvertDBEnetpulseResultParticipant(dbp))
}
return participants, nil
}
func (s *Store) CreateEnetpulseResultReferee(
ctx context.Context,
referee domain.CreateEnetpulseResultReferee,
) (domain.EnetpulseResultReferee, error) {
dbReferee, err := s.queries.CreateEnetpulseResultReferee(
ctx,
ConvertCreateEnetpulseResultReferee(referee),
)
if err != nil {
return domain.EnetpulseResultReferee{}, err
}
return ConvertDBEnetpulseResultReferee(dbReferee), nil
}
func (s *Store) GetEnetpulseResultRefereesByResultFK(
ctx context.Context,
resultFK string,
) ([]domain.EnetpulseResultReferee, error) {
dbReferees, err := s.queries.GetEnetpulseResultRefereesByResultFK(ctx, resultFK)
if err != nil {
return nil, err
}
referees := make([]domain.EnetpulseResultReferee, 0, len(dbReferees))
for _, dbr := range dbReferees {
referees = append(referees, ConvertDBEnetpulseResultReferee(dbr))
}
return referees, nil
}
func ConvertCreateEnetpulseResultParticipant(p domain.CreateEnetpulseResultParticipant) dbgen.CreateEnetpulseResultParticipantParams {
return dbgen.CreateEnetpulseResultParticipantParams{
ParticipantMapID: p.ParticipantMapID,
ResultFk: p.ResultFk,
ParticipantFk: p.ParticipantFk,
Number: pgtype.Int4{Int32: p.Number},
Name: pgtype.Text{String: p.Name},
Gender: pgtype.Text{String: p.Gender},
Type: pgtype.Text{String: p.Type},
CountryFk: pgtype.Text{String: p.CountryFk},
CountryName: pgtype.Text{String: p.CountryName},
OrdinaryTime: pgtype.Text{String: p.OrdinaryTime},
RunningScore: pgtype.Text{String: p.RunningScore},
Halftime: pgtype.Text{String: p.Halftime},
FinalResult: pgtype.Text{String: p.FinalResult},
LastUpdatedAt: pgtype.Timestamptz{Time: p.LastUpdatedAt, Valid: !p.LastUpdatedAt.IsZero()},
}
}
func ConvertDBEnetpulseResultParticipant(p dbgen.EnetpulseResultParticipant) domain.EnetpulseResultParticipant {
return domain.EnetpulseResultParticipant{
ID: p.ID,
ParticipantMapID: p.ParticipantMapID,
ResultFk: p.ResultFk,
ParticipantFk: p.ParticipantFk,
Number: p.Number.Int32,
Name: p.Name.String,
Gender: p.Gender.String,
Type: p.Type.String,
CountryFk: p.CountryFk.String,
CountryName: p.CountryName.String,
OrdinaryTime: p.OrdinaryTime.String,
RunningScore: p.RunningScore.String,
Halftime: p.Halftime.String,
FinalResult: p.FinalResult.String,
LastUpdatedAt: p.LastUpdatedAt.Time,
CreatedAt: p.CreatedAt.Time,
}
}
func ConvertCreateEnetpulseResultReferee(r domain.CreateEnetpulseResultReferee) dbgen.CreateEnetpulseResultRefereeParams {
return dbgen.CreateEnetpulseResultRefereeParams{
ResultFk: r.ResultFk,
RefereeFk: pgtype.Text{String: r.RefereeFk},
Assistant1RefereeFk: pgtype.Text{String: r.Assistant1RefereeFk},
Assistant2RefereeFk: pgtype.Text{String: r.Assistant2RefereeFk},
FourthRefereeFk: pgtype.Text{String: r.FourthRefereeFk},
Var1RefereeFk: pgtype.Text{String: r.Var1RefereeFk},
Var2RefereeFk: pgtype.Text{String: r.Var2RefereeFk},
LastUpdatedAt: pgtype.Timestamptz{Time: r.LastUpdatedAt},
}
}
func ConvertDBEnetpulseResultReferee(r dbgen.EnetpulseResultReferee) domain.EnetpulseResultReferee {
return domain.EnetpulseResultReferee{
ID: r.ID,
ResultFk: r.ResultFk,
RefereeFk: r.RefereeFk.String,
Assistant1RefereeFk: r.Assistant1RefereeFk.String,
Assistant2RefereeFk: r.Assistant2RefereeFk.String,
FourthRefereeFk: r.FourthRefereeFk.String,
Var1RefereeFk: r.Var1RefereeFk.String,
Var2RefereeFk: r.Var2RefereeFk.String,
LastUpdatedAt: r.LastUpdatedAt.Time,
CreatedAt: r.CreatedAt.Time,
}
}

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -19,8 +21,9 @@ type VirtualGameRepository interface {
ListVirtualGameProviders(ctx context.Context, limit, offset int32) ([]dbgen.VirtualGameProvider, error) ListVirtualGameProviders(ctx context.Context, limit, offset int32) ([]dbgen.VirtualGameProvider, error)
UpdateVirtualGameProviderEnabled(ctx context.Context, providerID string, enabled bool) (dbgen.VirtualGameProvider, error) UpdateVirtualGameProviderEnabled(ctx context.Context, providerID string, enabled bool) (dbgen.VirtualGameProvider, error)
CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error
GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (*domain.VirtualGameSession, error)
GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error) GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error)
UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error // UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error
CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error
GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error)
UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error
@ -36,6 +39,12 @@ type VirtualGameRepository interface {
CreateVirtualGame(ctx context.Context, arg dbgen.CreateVirtualGameParams) (dbgen.VirtualGame, error) CreateVirtualGame(ctx context.Context, arg dbgen.CreateVirtualGameParams) (dbgen.VirtualGame, error)
ListAllVirtualGames(ctx context.Context, arg dbgen.GetAllVirtualGamesParams) ([]dbgen.GetAllVirtualGamesRow, error) ListAllVirtualGames(ctx context.Context, arg dbgen.GetAllVirtualGamesParams) ([]dbgen.GetAllVirtualGamesRow, error)
RemoveAllVirtualGames(ctx context.Context) error RemoveAllVirtualGames(ctx context.Context) error
CreateVirtualGameProviderReport(ctx context.Context, report domain.CreateVirtualGameProviderReport) (domain.VirtualGameProviderReport, error)
CreateVirtualGameReport(ctx context.Context, report domain.CreateVirtualGameReport) (domain.VirtualGameReport, error)
GetVirtualGameProviderReportByProviderAndDate(ctx context.Context, providerID string, createdAt time.Time, reportType string) (domain.VirtualGameProviderReport, error)
UpdateVirtualGameProviderReportByDate(ctx context.Context, providerID string, reportDate time.Time, reportType string, totalGamesPlayed int64, totalBets float64, totalPayouts float64, totalPlayers int64) error
ListVirtualGameProviderReportsByGamesPlayedAsc(ctx context.Context) ([]domain.VirtualGameProviderReport, error)
ListVirtualGameProviderReportsByGamesPlayedDesc(ctx context.Context) ([]domain.VirtualGameProviderReport, error)
} }
type VirtualGameRepo struct { type VirtualGameRepo struct {
@ -158,14 +167,33 @@ func (r *VirtualGameRepo) CreateVirtualGameSession(ctx context.Context, session
UserID: session.UserID, UserID: session.UserID,
GameID: session.GameID, GameID: session.GameID,
SessionToken: session.SessionToken, SessionToken: session.SessionToken,
Currency: session.Currency, // Currency: session.Currency,
Status: session.Status, // Status: session.Status,
ExpiresAt: pgtype.Timestamptz{Time: session.ExpiresAt, Valid: true}, // ExpiresAt: pgtype.Timestamptz{Time: session.ExpiresAt, Valid: true},
} }
_, err := r.store.queries.CreateVirtualGameSession(ctx, params) _, err := r.store.queries.CreateVirtualGameSession(ctx, params)
return err return err
} }
func (r *VirtualGameRepo) GetVirtualGameSessionByUserID(ctx context.Context, userID int64) (*domain.VirtualGameSession, error) {
dbSession, err := r.store.queries.GetVirtualGameSessionByUserID(ctx, userID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &domain.VirtualGameSession{
ID: dbSession.ID,
UserID: dbSession.UserID,
GameID: dbSession.GameID,
SessionToken: dbSession.SessionToken,
CreatedAt: dbSession.CreatedAt.Time,
UpdatedAt: dbSession.UpdatedAt.Time,
}, nil
}
func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error) { func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error) {
dbSession, err := r.store.queries.GetVirtualGameSessionByToken(ctx, token) dbSession, err := r.store.queries.GetVirtualGameSessionByToken(ctx, token)
if err != nil { if err != nil {
@ -179,20 +207,20 @@ func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, toke
UserID: dbSession.UserID, UserID: dbSession.UserID,
GameID: dbSession.GameID, GameID: dbSession.GameID,
SessionToken: dbSession.SessionToken, SessionToken: dbSession.SessionToken,
Currency: dbSession.Currency, // Currency: dbSession.Currency,
Status: dbSession.Status, // Status: dbSession.Status,
CreatedAt: dbSession.CreatedAt.Time, CreatedAt: dbSession.CreatedAt.Time,
UpdatedAt: dbSession.UpdatedAt.Time, UpdatedAt: dbSession.UpdatedAt.Time,
ExpiresAt: dbSession.ExpiresAt.Time, // ExpiresAt: dbSession.ExpiresAt.Time,
}, nil }, nil
} }
func (r *VirtualGameRepo) UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error { // func (r *VirtualGameRepo) UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error {
return r.store.queries.UpdateVirtualGameSessionStatus(ctx, dbgen.UpdateVirtualGameSessionStatusParams{ // return r.store.queries.UpdateVirtualGameSessionStatus(ctx, dbgen.UpdateVirtualGameSessionStatusParams{
ID: id, // ID: id,
Status: status, // Status: status,
}) // })
} // }
func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error { func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error {
params := dbgen.CreateVirtualGameTransactionParams{ params := dbgen.CreateVirtualGameTransactionParams{
@ -314,3 +342,183 @@ func (r *VirtualGameRepo) ListAllVirtualGames(ctx context.Context, arg dbgen.Get
func (r *VirtualGameRepo) RemoveAllVirtualGames(ctx context.Context) error { func (r *VirtualGameRepo) RemoveAllVirtualGames(ctx context.Context) error {
return r.store.queries.DeleteAllVirtualGames(ctx) return r.store.queries.DeleteAllVirtualGames(ctx)
} }
func (r *VirtualGameRepo) CreateVirtualGameProviderReport(
ctx context.Context,
report domain.CreateVirtualGameProviderReport,
) (domain.VirtualGameProviderReport, error) {
dbReport, err := r.store.queries.CreateVirtualGameProviderReport(
ctx,
ConvertCreateVirtualGameProviderReport(report),
)
if err != nil {
return domain.VirtualGameProviderReport{}, err
}
return ConvertDBVirtualGameProviderReport(dbReport), nil
}
func (r *VirtualGameRepo) CreateVirtualGameReport(
ctx context.Context,
report domain.CreateVirtualGameReport,
) (domain.VirtualGameReport, error) {
dbReport, err := r.store.queries.CreateVirtualGameReport(
ctx,
ConvertCreateVirtualGameReport(report),
)
if err != nil {
return domain.VirtualGameReport{}, err
}
return ConvertDBVirtualGameReport(dbReport), nil
}
func (r *VirtualGameRepo) GetVirtualGameProviderReportByProviderAndDate(
ctx context.Context,
providerID string,
reportDate time.Time,
reportType string,
) (domain.VirtualGameProviderReport, error) {
arg := dbgen.GetVirtualGameProviderReportByProviderAndDateParams{
ProviderID: providerID,
ReportDate: pgtype.Date{Time: reportDate, Valid: true},
ReportType: pgtype.Text{String: reportType, Valid: true},
}
dbReport, err := r.store.queries.GetVirtualGameProviderReportByProviderAndDate(ctx, arg)
if err != nil {
return domain.VirtualGameProviderReport{}, err
}
return ConvertDBVirtualGameProviderReport(dbReport), nil
}
func (r *VirtualGameRepo) UpdateVirtualGameProviderReportByDate(
ctx context.Context,
providerID string,
reportDate time.Time,
reportType string,
totalGamesPlayed int64,
totalBets float64,
totalPayouts float64,
totalPlayers int64,
) error {
arg := dbgen.UpdateVirtualGameProviderReportByDateParams{
ProviderID: providerID,
ReportDate: pgtype.Date{Time: reportDate, Valid: true},
ReportType: pgtype.Text{String: reportType, Valid: true},
TotalGamesPlayed: pgtype.Int8{Int64: totalGamesPlayed, Valid: true},
TotalBets: pgtype.Numeric{},
TotalPayouts: pgtype.Numeric{},
TotalPlayers: pgtype.Int8{Int64: totalPlayers, Valid: true},
}
// Safely convert float64 → pgtype.Numeric
if err := arg.TotalBets.Scan(totalBets); err != nil {
return fmt.Errorf("failed to set total_bets: %w", err)
}
if err := arg.TotalPayouts.Scan(totalPayouts); err != nil {
return fmt.Errorf("failed to set total_payouts: %w", err)
}
if err := r.store.queries.UpdateVirtualGameProviderReportByDate(ctx, arg); err != nil {
return fmt.Errorf("failed to update provider report for %s: %w", providerID, err)
}
return nil
}
func (r *VirtualGameRepo) ListVirtualGameProviderReportsByGamesPlayedAsc(
ctx context.Context,
) ([]domain.VirtualGameProviderReport, error) {
dbReports, err := r.store.queries.ListVirtualGameProviderReportsByGamesPlayedAsc(ctx)
if err != nil {
return nil, err
}
reports := make([]domain.VirtualGameProviderReport, len(dbReports))
for i, r := range dbReports {
reports[i] = ConvertDBVirtualGameProviderReport(r)
}
return reports, nil
}
func (r *VirtualGameRepo) ListVirtualGameProviderReportsByGamesPlayedDesc(
ctx context.Context,
) ([]domain.VirtualGameProviderReport, error) {
dbReports, err := r.store.queries.ListVirtualGameProviderReportsByGamesPlayedDesc(ctx)
if err != nil {
return nil, err
}
reports := make([]domain.VirtualGameProviderReport, len(dbReports))
for i, r := range dbReports {
reports[i] = ConvertDBVirtualGameProviderReport(r)
}
return reports, nil
}
func ConvertCreateVirtualGameProviderReport(r domain.CreateVirtualGameProviderReport) dbgen.CreateVirtualGameProviderReportParams {
// var totalBets, totalPayouts pgtype.Numeric
// _ = r.TotalBets.
// _ = totalPayouts.Set(r.TotalPayouts)
return dbgen.CreateVirtualGameProviderReportParams{
ProviderID: r.ProviderID,
ReportDate: pgtype.Date{Time: r.ReportDate, Valid: true},
TotalGamesPlayed: pgtype.Int8{Int64: r.TotalGamesPlayed, Valid: true},
TotalBets: pgtype.Numeric{Exp: int32(r.TotalBets)},
TotalPayouts: pgtype.Numeric{Exp: int32(r.TotalPayouts)},
TotalPlayers: pgtype.Int8{Int64: r.TotalPlayers, Valid: true},
Column7: pgtype.Text{String: r.ReportType, Valid: true},
}
}
func ConvertDBVirtualGameProviderReport(db dbgen.VirtualGameProviderReport) domain.VirtualGameProviderReport {
return domain.VirtualGameProviderReport{
ID: db.ID,
ProviderID: db.ProviderID,
ReportDate: db.ReportDate.Time,
TotalGamesPlayed: db.TotalGamesPlayed.Int64,
TotalBets: float64(db.TotalBets.Exp),
TotalPayouts: float64(db.TotalPayouts.Exp),
TotalProfit: float64(db.TotalProfit.Exp),
TotalPlayers: db.TotalPlayers.Int64,
ReportType: db.ReportType.String,
CreatedAt: db.CreatedAt.Time,
UpdatedAt: db.UpdatedAt.Time,
}
}
func ConvertCreateVirtualGameReport(r domain.CreateVirtualGameReport) dbgen.CreateVirtualGameReportParams {
return dbgen.CreateVirtualGameReportParams{
GameID: r.GameID,
ProviderID: r.ProviderID,
ReportDate: pgtype.Date{Time: r.ReportDate},
TotalRounds: pgtype.Int8{Int64: r.TotalRounds},
TotalBets: pgtype.Numeric{Exp: int32(r.TotalBets)},
TotalPayouts: pgtype.Numeric{Exp: int32(r.TotalPayouts)},
TotalPlayers: pgtype.Int8{Int64: r.TotalPlayers},
Column8: r.ReportType,
}
}
func ConvertDBVirtualGameReport(db dbgen.VirtualGameReport) domain.VirtualGameReport {
return domain.VirtualGameReport{
ID: db.ID,
GameID: db.GameID,
ProviderID: db.ProviderID,
ReportDate: db.ReportDate.Time,
TotalRounds: db.TotalRounds.Int64,
TotalBets: float64(db.TotalBets.Exp),
TotalPayouts: float64(db.TotalPayouts.Exp),
TotalProfit: float64(db.TotalProfit.Exp),
TotalPlayers: db.TotalPlayers.Int64,
ReportType: db.ReportType.String,
CreatedAt: db.CreatedAt.Time,
UpdatedAt: db.UpdatedAt.Time,
}
}

View File

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -32,7 +33,7 @@ func NewArifpayService(cfg *config.Config, transferStore ports.TransferStore, wa
} }
} }
func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool) (map[string]any, error) { func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool, userId int64) (map[string]any, error) {
// Generate unique nonce // Generate unique nonce
nonce := uuid.NewString() nonce := uuid.NewString()
@ -130,6 +131,10 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR
ReferenceNumber: nonce, ReferenceNumber: nonce,
SessionID: fmt.Sprintf("%v", data["sessionId"]), SessionID: fmt.Sprintf("%v", data["sessionId"]),
Status: string(domain.PaymentStatusPending), Status: string(domain.PaymentStatusPending),
CashierID: domain.ValidInt64{
Value: userId,
Valid: true,
},
} }
if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil { if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil {
@ -139,7 +144,7 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR
return data, nil return data, nil
} }
func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID string) (*domain.CancelCheckoutSessionResponse, error) { func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID string) (any, error) {
// Build the cancel URL // Build the cancel URL
url := fmt.Sprintf("%s/api/sandbox/checkout/session/%s", s.cfg.ARIFPAY.BaseURL, sessionID) url := fmt.Sprintf("%s/api/sandbox/checkout/session/%s", s.cfg.ARIFPAY.BaseURL, sessionID)
@ -177,17 +182,19 @@ func (s *ArifpayService) CancelCheckoutSession(ctx context.Context, sessionID st
return nil, fmt.Errorf("failed to unmarshal cancel response: %w", err) return nil, fmt.Errorf("failed to unmarshal cancel response: %w", err)
} }
return &cancelResp, nil return cancelResp.Data, nil
} }
func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRequest, userId int64, isDepost bool) error { func (s *ArifpayService) ProcessWebhook(ctx context.Context, req domain.WebhookRequest, isDeposit bool) error {
// 1. Get transfer by SessionID // 1. Get transfer by SessionID
transfer, err := s.transferStore.GetTransferByReference(ctx, req.Transaction.TransactionID) transfer, err := s.transferStore.GetTransferByReference(ctx, req.Nonce)
if err != nil { if err != nil {
return err return err
} }
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) userId := transfer.DepositorID.Value
wallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
if err != nil { if err != nil {
return err return err
} }
@ -197,7 +204,7 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe
} }
// 2. Update transfer status // 2. Update transfer status
newStatus := req.Transaction.TransactionStatus newStatus := strings.ToLower(req.Transaction.TransactionStatus)
// if req.Transaction.TransactionStatus != "" { // if req.Transaction.TransactionStatus != "" {
// newStatus = req.Transaction.TransactionStatus // newStatus = req.Transaction.TransactionStatus
// } // }
@ -213,10 +220,10 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe
} }
// 3. If SUCCESS -> update customer wallet balance // 3. If SUCCESS -> update customer wallet balance
if (newStatus == "SUCCESS" && isDepost) || (newStatus == "FAILED" && !isDepost) { if (newStatus == "success" && isDeposit) || (newStatus == "failed" && !isDeposit) {
_, err = s.walletSvc.AddToWallet(ctx, wallets[0].ID, domain.Currency(req.TotalAmount), domain.ValidInt64{}, transfer.PaymentMethod, domain.PaymentDetails{ _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.TotalAmount), domain.ValidInt64{}, transfer.PaymentMethod, domain.PaymentDetails{
ReferenceNumber: domain.ValidString{ ReferenceNumber: domain.ValidString{
Value: req.Transaction.TransactionID, Value: req.Nonce,
Valid: true, Valid: true,
}, },
BankNumber: domain.ValidString{ BankNumber: domain.ValidString{
@ -232,35 +239,94 @@ func (s *ArifpayService) HandleWebhook(ctx context.Context, req domain.WebhookRe
return nil return nil
} }
func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error {
// Step 1: Create Session // Step 1: Create Session
userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
if err != nil {
return fmt.Errorf("failed to get user wallets: %w", err)
}
// if len(userWallets) == 0 {
// return fmt.Errorf("no wallet found for user %d", userId)
// }
_, err = s.walletSvc.DeductFromWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
referenceNum := uuid.NewString() referenceNum := uuid.NewString()
sessionReq := domain.CheckoutSessionClientRequest{ sessionReq := domain.CheckoutSessionClientRequest{
Amount: req.Amount, Amount: req.Amount,
CustomerEmail: req.CustomerEmail, CustomerEmail: req.CustomerEmail,
CustomerPhone: req.CustomerPhone, CustomerPhone: "251" + req.CustomerPhone[:9],
} }
sessionResp, err := s.CreateCheckoutSession(sessionReq, false) sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId)
if err != nil { if err != nil {
_, err = s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
return fmt.Errorf("failed to create session: %w", err) return fmt.Errorf("failed to create session: %w", err)
} }
sessionRespData := sessionResp["data"].(map[string]any)
// Step 2: Execute Transfer // Step 2: Execute Transfer
transferURL := fmt.Sprintf("%s/api/Telebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL) transferURL := fmt.Sprintf("%s/api/Telebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
reqBody := map[string]any{ reqBody := map[string]any{
"Sessionid": sessionResp["sessionId"], "Sessionid": sessionRespData["sessionId"],
"Phonenumber": req.PhoneNumber, "Phonenumber": "251" + req.CustomerPhone[:9],
} }
payload, err := json.Marshal(reqBody) payload, err := json.Marshal(reqBody)
if err != nil { if err != nil {
_, err = s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
return fmt.Errorf("failed to marshal transfer request: %w", err) return fmt.Errorf("failed to marshal transfer request: %w", err)
} }
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
if err != nil { if err != nil {
_, err = s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
return fmt.Errorf("failed to build transfer request: %w", err) return fmt.Errorf("failed to build transfer request: %w", err)
} }
transferReq.Header.Set("Content-Type", "application/json") transferReq.Header.Set("Content-Type", "application/json")
@ -268,11 +334,35 @@ func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req dom
transferResp, err := s.httpClient.Do(transferReq) transferResp, err := s.httpClient.Do(transferReq)
if err != nil { if err != nil {
_, err = s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
return fmt.Errorf("failed to execute transfer request: %w", err) return fmt.Errorf("failed to execute transfer request: %w", err)
} }
defer transferResp.Body.Close() defer transferResp.Body.Close()
if transferResp.StatusCode >= 300 { if transferResp.StatusCode != http.StatusOK {
_, err = s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
body, _ := io.ReadAll(transferResp.Body) body, _ := io.ReadAll(transferResp.Body)
return fmt.Errorf("transfer failed with status %d: %s", transferResp.StatusCode, string(body)) return fmt.Errorf("transfer failed with status %d: %s", transferResp.StatusCode, string(body))
} }
@ -283,109 +373,33 @@ func (s *ArifpayService) ExecuteTelebirrB2CTransfer(ctx context.Context, req dom
Verified: false, Verified: false,
Type: domain.WITHDRAW, // B2C = payout Type: domain.WITHDRAW, // B2C = payout
ReferenceNumber: referenceNum, ReferenceNumber: referenceNum,
SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]), SessionID: fmt.Sprintf("%v", sessionRespData["sessionId"]),
Status: string(domain.PaymentStatusPending), Status: string(domain.PaymentStatusPending),
PaymentMethod: domain.TRANSFER_ARIFPAY, PaymentMethod: domain.TRANSFER_ARIFPAY,
CashierID: domain.ValidInt64{
Value: userId,
Valid: true,
},
} }
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return fmt.Errorf("failed to store transfer: %w", err) return fmt.Errorf("failed to store transfer: %w", err)
} }
// Step 4: Deduct from wallet // Step 4: Deduct from wallet
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId)
if err != nil {
return fmt.Errorf("failed to get user wallets: %w", err)
}
if len(userWallets) == 0 {
return fmt.Errorf("no wallet found for user %d", userId)
}
_, err = s.walletSvc.DeductFromWallet(
ctx,
userWallets[0].ID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
"",
)
if err != nil {
return fmt.Errorf("failed to deduct from wallet: %w", err)
}
return nil return nil
} }
func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error { func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error {
// Step 1: Create Session // Step 1: Deduct from user wallet first
referenceNum := uuid.NewString() userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
sessionReq := domain.CheckoutSessionClientRequest{
Amount: req.Amount,
CustomerEmail: req.CustomerEmail,
CustomerPhone: req.CustomerPhone,
}
sessionResp, err := s.CreateCheckoutSession(sessionReq, false)
if err != nil { if err != nil {
return fmt.Errorf("cbebirr: failed to create session: %w", err) return fmt.Errorf("cbebirr: failed to get user wallet: %w", err)
}
// Step 2: Execute Transfer
transferURL := fmt.Sprintf("%s/api/Cbebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
reqBody := map[string]any{
"Sessionid": sessionResp["sessionId"],
"Phonenumber": req.PhoneNumber,
}
payload, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("cbebirr: failed to marshal transfer request: %w", err)
}
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("cbebirr: failed to build transfer request: %w", err)
}
transferReq.Header.Set("Content-Type", "application/json")
transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
transferResp, err := s.httpClient.Do(transferReq)
if err != nil {
return fmt.Errorf("cbebirr: failed to execute transfer request: %w", err)
}
defer transferResp.Body.Close()
if transferResp.StatusCode >= 300 {
body, _ := io.ReadAll(transferResp.Body)
return fmt.Errorf("cbebirr: transfer failed with status %d: %s", transferResp.StatusCode, string(body))
}
// Step 3: Store transfer in DB
transfer := domain.CreateTransfer{
Amount: domain.Currency(req.Amount),
Verified: false,
Type: domain.WITHDRAW, // B2C = payout
ReferenceNumber: referenceNum,
SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]),
Status: string(domain.PaymentStatusPending),
PaymentMethod: domain.TRANSFER_ARIFPAY,
}
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return fmt.Errorf("cbebirr: failed to store transfer: %w", err)
}
// Step 4: Deduct from user wallet
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId)
if err != nil {
return fmt.Errorf("cbebirr: failed to get user wallets: %w", err)
}
if len(userWallets) == 0 {
return fmt.Errorf("cbebirr: no wallet found for user %d", userId)
} }
_, err = s.walletSvc.DeductFromWallet( _, err = s.walletSvc.DeductFromWallet(
ctx, ctx,
userWallets[0].ID, userWallet.RegularID,
domain.Currency(req.Amount), domain.Currency(req.Amount),
domain.ValidInt64{}, domain.ValidInt64{},
domain.TRANSFER_ARIFPAY, domain.TRANSFER_ARIFPAY,
@ -395,55 +409,68 @@ func (s *ArifpayService) ExecuteCBEB2CTransfer(ctx context.Context, req domain.A
return fmt.Errorf("cbebirr: failed to deduct from wallet: %w", err) return fmt.Errorf("cbebirr: failed to deduct from wallet: %w", err)
} }
return nil
}
func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain.ArifpayB2CRequest, userId int64) error {
// Step 1: Create Session
referenceNum := uuid.NewString() referenceNum := uuid.NewString()
// Step 2: Create Session
sessionReq := domain.CheckoutSessionClientRequest{ sessionReq := domain.CheckoutSessionClientRequest{
Amount: req.Amount, Amount: req.Amount,
CustomerEmail: req.CustomerEmail, CustomerEmail: req.CustomerEmail,
CustomerPhone: req.CustomerPhone, CustomerPhone: "251" + req.CustomerPhone[:9],
} }
sessionResp, err := s.CreateCheckoutSession(sessionReq, false) sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId)
if err != nil { if err != nil {
return fmt.Errorf("Mpesa: failed to create session: %w", err) // refund wallet if session creation fails
_, refundErr := s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if refundErr != nil {
return fmt.Errorf("cbebirr: refund failed after session creation error: %v", refundErr)
}
return fmt.Errorf("cbebirr: failed to create session: %w", err)
} }
// Step 2: Execute Transfer // Step 3: Execute Transfer
transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL) transferURL := fmt.Sprintf("%s/api/Cbebirr/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
reqBody := map[string]any{ reqBody := map[string]any{
"Sessionid": sessionResp["sessionId"], "Sessionid": sessionResp["sessionId"],
"Phonenumber": req.PhoneNumber, "Phonenumber": "251" + req.CustomerPhone[:9],
} }
payload, err := json.Marshal(reqBody) payload, err := json.Marshal(reqBody)
if err != nil { if err != nil {
return fmt.Errorf("Mpesa: failed to marshal transfer request: %w", err) s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("cbebirr: failed to marshal transfer request: %w", err)
} }
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload)) transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
if err != nil { if err != nil {
return fmt.Errorf("Mpesa: failed to build transfer request: %w", err) s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("cbebirr: failed to build transfer request: %w", err)
} }
transferReq.Header.Set("Content-Type", "application/json") transferReq.Header.Set("Content-Type", "application/json")
transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
transferResp, err := s.httpClient.Do(transferReq) transferResp, err := s.httpClient.Do(transferReq)
if err != nil { if err != nil {
return fmt.Errorf("Mpesa: failed to execute transfer request: %w", err) s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("cbebirr: failed to execute transfer request: %w", err)
} }
defer transferResp.Body.Close() defer transferResp.Body.Close()
if transferResp.StatusCode >= 300 { if transferResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(transferResp.Body) body, _ := io.ReadAll(transferResp.Body)
return fmt.Errorf("Mpesa: transfer failed with status %d: %s", transferResp.StatusCode, string(body)) s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("cbebirr: transfer failed with status %d: %s", transferResp.StatusCode, string(body))
} }
// Step 3: Store transfer in DB // Step 4: Store transfer in DB
transfer := domain.CreateTransfer{ transfer := domain.CreateTransfer{
Amount: domain.Currency(req.Amount), Amount: domain.Currency(req.Amount),
Verified: false, Verified: false,
@ -452,30 +479,116 @@ func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain
SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]), SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]),
Status: string(domain.PaymentStatusPending), Status: string(domain.PaymentStatusPending),
PaymentMethod: domain.TRANSFER_ARIFPAY, PaymentMethod: domain.TRANSFER_ARIFPAY,
} CashierID: domain.ValidInt64{
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { Value: userId,
return fmt.Errorf("Mpesa: failed to store transfer: %w", err) Valid: true,
},
} }
// Step 4: Deduct from user wallet if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, userId) return fmt.Errorf("cbebirr: failed to store transfer: %w", err)
if err != nil {
return fmt.Errorf("Mpesa: failed to get user wallets: %w", err)
} }
if len(userWallets) == 0 {
return fmt.Errorf("Mpesa: no wallet found for user %d", userId) return nil
}
func (s *ArifpayService) ExecuteMPesaB2CTransfer(ctx context.Context, req domain.CheckoutSessionClientRequest, userId int64) error {
// Step 1: Deduct from user wallet first
userWallet, err := s.walletSvc.GetCustomerWallet(ctx, userId)
if err != nil {
return fmt.Errorf("mpesa: failed to get user wallet: %w", err)
} }
_, err = s.walletSvc.DeductFromWallet( _, err = s.walletSvc.DeductFromWallet(
ctx, ctx,
userWallets[0].ID, userWallet.RegularID,
domain.Currency(req.Amount), domain.Currency(req.Amount),
domain.ValidInt64{}, domain.ValidInt64{},
domain.TRANSFER_ARIFPAY, domain.TRANSFER_ARIFPAY,
"", "",
) )
if err != nil { if err != nil {
return fmt.Errorf("Mpesa: failed to deduct from wallet: %w", err) return fmt.Errorf("mpesa: failed to deduct from wallet: %w", err)
}
referenceNum := uuid.NewString()
// Step 2: Create Session
sessionReq := domain.CheckoutSessionClientRequest{
Amount: req.Amount,
CustomerEmail: req.CustomerEmail,
CustomerPhone: "251" + req.CustomerPhone[:9],
}
sessionResp, err := s.CreateCheckoutSession(sessionReq, false, userId)
if err != nil {
// Refund wallet if session creation fails
_, refundErr := s.walletSvc.AddToWallet(
ctx,
userWallet.RegularID,
domain.Currency(req.Amount),
domain.ValidInt64{},
domain.TRANSFER_ARIFPAY,
domain.PaymentDetails{},
"",
)
if refundErr != nil {
return fmt.Errorf("mpesa: refund failed after session creation error: %v", refundErr)
}
return fmt.Errorf("mpesa: failed to create session: %w", err)
}
// Step 3: Execute Transfer
transferURL := fmt.Sprintf("%s/api/Mpesa/b2c/transfer", s.cfg.ARIFPAY.BaseURL)
reqBody := map[string]any{
"Sessionid": sessionResp["sessionId"],
"Phonenumber": "251" + req.CustomerPhone[:9],
}
payload, err := json.Marshal(reqBody)
if err != nil {
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("mpesa: failed to marshal transfer request: %w", err)
}
transferReq, err := http.NewRequestWithContext(ctx, http.MethodPost, transferURL, bytes.NewBuffer(payload))
if err != nil {
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("mpesa: failed to build transfer request: %w", err)
}
transferReq.Header.Set("Content-Type", "application/json")
transferReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey)
transferResp, err := s.httpClient.Do(transferReq)
if err != nil {
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("mpesa: failed to execute transfer request: %w", err)
}
defer transferResp.Body.Close()
if transferResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(transferResp.Body)
s.walletSvc.AddToWallet(ctx, userWallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_ARIFPAY, domain.PaymentDetails{}, "")
return fmt.Errorf("mpesa: transfer failed with status %d: %s", transferResp.StatusCode, string(body))
}
// Step 4: Store transfer in DB
transfer := domain.CreateTransfer{
Amount: domain.Currency(req.Amount),
Verified: false,
Type: domain.WITHDRAW, // B2C = payout
ReferenceNumber: referenceNum,
SessionID: fmt.Sprintf("%v", sessionResp["sessionId"]),
Status: string(domain.PaymentStatusPending),
PaymentMethod: domain.TRANSFER_ARIFPAY,
CashierID: domain.ValidInt64{
Value: userId,
Valid: true,
},
}
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return fmt.Errorf("mpesa: failed to store transfer: %w", err)
} }
return nil return nil

View File

@ -933,6 +933,7 @@ func (s *Service) GetBetOutcomeByBetID(ctx context.Context, UserID int64) ([]dom
func (s *Service) GetBetOutcomeViewByEventID(ctx context.Context, eventID int64, filter domain.BetOutcomeViewFilter) ([]domain.BetOutcomeViewRes, int64, error) { func (s *Service) GetBetOutcomeViewByEventID(ctx context.Context, eventID int64, filter domain.BetOutcomeViewFilter) ([]domain.BetOutcomeViewRes, int64, error) {
return s.betStore.GetBetOutcomeViewByEventID(ctx, eventID, filter) return s.betStore.GetBetOutcomeViewByEventID(ctx, eventID, filter)
} }
func (s *Service) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) { func (s *Service) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) {
return s.betStore.GetBetOutcomeByEventID(ctx, eventID, is_filtered) return s.betStore.GetBetOutcomeByEventID(ctx, eventID, is_filtered)
} }

View File

@ -29,16 +29,17 @@ func NewClient(baseURL, secretKey string) *Client {
} }
} }
func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) { func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error) {
payload := map[string]interface{}{ payload := map[string]interface{}{
"amount": fmt.Sprintf("%.2f", float64(req.Amount)), "amount": fmt.Sprintf("%.2f", float64(req.Amount)),
"currency": req.Currency, "currency": req.Currency,
// "email": req.Email, "email": req.Email,
"first_name": req.FirstName, "first_name": req.FirstName,
"last_name": req.LastName, "last_name": req.LastName,
"tx_ref": req.TxRef, "tx_ref": req.TxRef,
"callback_url": req.CallbackURL, // "callback_url": req.CallbackURL,
"return_url": req.ReturnURL, "return_url": req.ReturnURL,
"phone_number": req.PhoneNumber,
} }
fmt.Printf("\n\nChapa Payload: %+v\n\n", payload) fmt.Printf("\n\nChapa Payload: %+v\n\n", payload)
@ -69,9 +70,9 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR
return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) // <-- Log it return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body)) // <-- Log it
} }
if resp.StatusCode != http.StatusOK { // if resp.StatusCode != http.StatusOK {
return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) // return domain.ChapaDepositResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
} // }
var response struct { var response struct {
Message string `json:"message"` Message string `json:"message"`
@ -130,15 +131,16 @@ func (c *Client) VerifyPayment(ctx context.Context, reference string) (domain.Ch
}, nil }, nil
} }
func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain.ChapaPaymentVerificationResponse, error) {
url := fmt.Sprintf("%s/transaction/verify/%s", c.baseURL, txRef) url := fmt.Sprintf("%s/transaction/verify/%s", c.baseURL, txRef)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err) return nil, fmt.Errorf("failed to create request: %w", err)
} }
req.Header.Set("Authorization", "Bearer "+c.secretKey) req.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
@ -147,35 +149,27 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
} }
var response struct { var verification domain.ChapaPaymentVerificationResponse
Status string `json:"status"` if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil {
Amount float64 `json:"amount"`
Currency string `json:"currency"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err) return nil, fmt.Errorf("failed to decode response: %w", err)
} }
var status domain.PaymentStatus // Normalize payment status for internal use
switch response.Status { // switch strings.ToLower(verification.Data.Status) {
case "success": // case "success":
status = domain.PaymentStatusCompleted // verification.Status = string(domain.PaymentStatusCompleted)
default: // default:
status = domain.PaymentStatusFailed // verification.Status = string(domain.PaymentStatusFailed)
// }
return &verification, nil
} }
return &domain.ChapaVerificationResponse{ func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaTransferVerificationResponse, error) {
Status: string(status),
Amount: response.Amount,
Currency: response.Currency,
}, nil
}
func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
url := fmt.Sprintf("%s/transfers/verify/%s", c.baseURL, txRef) url := fmt.Sprintf("%s/transfers/verify/%s", c.baseURL, txRef)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
@ -213,14 +207,77 @@ func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domai
status = domain.PaymentStatusFailed status = domain.PaymentStatusFailed
} }
return &domain.ChapaVerificationResponse{ return &domain.ChapaTransferVerificationResponse{
Status: string(status), Status: string(status),
Amount: response.Amount, // Amount: response.Amount,
Currency: response.Currency, // Currency: response.Currency,
}, nil }, nil
} }
func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) { func (c *Client) GetAllTransactions(ctx context.Context) (domain.ChapaAllTransactionsResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/transactions", nil)
if err != nil {
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
var response domain.ChapaAllTransactionsResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return domain.ChapaAllTransactionsResponse{}, fmt.Errorf("failed to decode response: %w", err)
}
return response, nil
}
func (c *Client) GetTransactionEvents(ctx context.Context, refId string) ([]domain.ChapaTransactionEvent, error) {
url := fmt.Sprintf("%s/transaction/events/%s", c.baseURL, refId)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
var response struct {
Message string `json:"message"`
Status string `json:"status"`
Data []domain.ChapaTransactionEvent `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return response.Data, nil
}
func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.BankData, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil) req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err) return nil, fmt.Errorf("failed to create request: %w", err)
@ -243,9 +300,9 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
return nil, fmt.Errorf("failed to decode response: %w", err) return nil, fmt.Errorf("failed to decode response: %w", err)
} }
var banks []domain.Bank var banks []domain.BankData
for _, bankData := range bankResponse.Data { for _, bankData := range bankResponse.Data {
bank := domain.Bank{ bank := domain.BankData{
ID: bankData.ID, ID: bankData.ID,
Slug: bankData.Slug, Slug: bankData.Slug,
Swift: bankData.Swift, Swift: bankData.Swift,
@ -267,7 +324,7 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
return banks, nil return banks, nil
} }
func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) { func (c *Client) InitializeTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) {
endpoint := c.baseURL + "/transfers" endpoint := c.baseURL + "/transfers"
fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint) fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint)
@ -304,7 +361,7 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
return response.Status == string(domain.WithdrawalStatusSuccessful), nil return response.Status == string(domain.WithdrawalStatusSuccessful), nil
} }
func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) { func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaTransferVerificationResponse, error) {
base, err := url.Parse(c.baseURL) base, err := url.Parse(c.baseURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid base URL: %w", err) return nil, fmt.Errorf("invalid base URL: %w", err)
@ -328,7 +385,7 @@ func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.
return nil, fmt.Errorf("chapa api returned status: %d", resp.StatusCode) return nil, fmt.Errorf("chapa api returned status: %d", resp.StatusCode)
} }
var verification domain.ChapaVerificationResponse var verification domain.ChapaTransferVerificationResponse
if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil { if err := json.NewDecoder(resp.Body).Decode(&verification); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err) return nil, fmt.Errorf("failed to decode response: %w", err)
} }
@ -336,6 +393,62 @@ func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.
return &verification, nil return &verification, nil
} }
func (c *Client) CancelTransaction(ctx context.Context, txRef string) (domain.ChapaCancelResponse, error) {
// Construct URL for the cancel transaction endpoint
url := fmt.Sprintf("%s/transaction/cancel/%s", c.baseURL, txRef)
// Create HTTP request with context
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, nil)
if err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to create request: %w", err)
}
// Set authorization header
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
// Execute the HTTP request
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
// Handle non-OK responses
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return domain.ChapaCancelResponse{}, fmt.Errorf("unexpected status code: %d - %s", resp.StatusCode, string(body))
}
// Decode successful response
var response struct {
Message string `json:"message"`
Status string `json:"status"`
Data struct {
TxRef string `json:"tx_ref"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to decode response: %w", err)
}
// Return mapped domain response
return domain.ChapaCancelResponse{
Message: response.Message,
Status: response.Status,
TxRef: response.Data.TxRef,
Amount: response.Data.Amount,
Currency: response.Data.Currency,
CreatedAt: response.Data.CreatedAt,
UpdatedAt: response.Data.UpdatedAt,
}, nil
}
func (c *Client) setHeaders(req *http.Request) { func (c *Client) setHeaders(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+c.secretKey) req.Header.Set("Authorization", "Bearer "+c.secretKey)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")

View File

@ -15,11 +15,14 @@ import (
// } // }
type ChapaStore interface { type ChapaStore interface {
InitializePayment(request domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) InitializePayment(request domain.ChapaInitDepositRequest) (domain.ChapaDepositResponse, error)
// VerifyPayment(reference string) (domain.ChapaDepositVerification, error) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaTransferVerificationResponse, error)
ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error)
FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
CreateWithdrawal(userID string, amount float64, accountNumber, bankCode string) (*domain.ChapaWithdrawal, error) CreateWithdrawal(userID string, amount float64, accountNumber, bankCode string) (*domain.ChapaWithdrawal, error)
HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebhookTransfer) error
HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebhookPayment) error
GetPaymentReceiptURL(ctx context.Context, chapaRef string) (string, error)
GetAllTransfers(ctx context.Context) ([]domain.Transfer, error)
GetAccountBalance(ctx context.Context, currencyCode string) ([]domain.Balance, error)
InitiateSwap(ctx context.Context, amount float64, from, to string) (*domain.SwapResponse, error)
} }

View File

@ -1,9 +1,16 @@
package chapa package chapa
import ( import (
"bytes"
"context" "context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http"
"strconv" "strconv"
"strings" "strings"
@ -40,6 +47,31 @@ func NewService(
} }
} }
func (s *Service) VerifyWebhookSignature(ctx context.Context, payload []byte, chapaSignature, xChapaSignature string) (bool, error) {
secret := s.cfg.CHAPA_WEBHOOK_SECRET // or os.Getenv("CHAPA_SECRET_KEY")
if secret == "" {
return false, fmt.Errorf("missing Chapa secret key in configuration")
}
// Compute expected signature using HMAC SHA256
h := hmac.New(sha256.New, []byte(secret))
h.Write(payload)
expected := hex.EncodeToString(h.Sum(nil))
// Check either header
if chapaSignature == expected || xChapaSignature == expected {
return true, nil
}
// Optionally log for debugging
var pretty map[string]interface{}
_ = json.Unmarshal(payload, &pretty)
fmt.Printf("[Webhook Verification Failed]\nExpected: %s\nGot chapa-signature: %s\nGot x-chapa-signature: %s\nPayload: %+v\n",
expected, chapaSignature, xChapaSignature, pretty)
return false, fmt.Errorf("invalid webhook signature")
}
// InitiateDeposit starts a new deposit process // InitiateDeposit starts a new deposit process
func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) { func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
// Validate amount // Validate amount
@ -53,22 +85,22 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
return "", fmt.Errorf("failed to get user: %w", err) return "", fmt.Errorf("failed to get user: %w", err)
} }
var senderWallet domain.Wallet // var senderWallet domain.Wallet
// Generate unique reference // Generate unique reference
// reference := uuid.New().String() // reference := uuid.New().String()
reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, uuid.New().String()) reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, uuid.New().String())
senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID) senderWallet, err := s.walletStore.GetCustomerWallet(ctx, userID)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get sender wallets: %w", err) return "", fmt.Errorf("failed to get sender wallet: %w", err)
}
for _, wallet := range senderWallets {
if wallet.IsWithdraw {
senderWallet = wallet
break
}
} }
// for _, wallet := range senderWallets {
// if wallet.IsTransferable {
// senderWallet = wallet
// break
// }
// }
// Check if payment with this reference already exists // Check if payment with this reference already exists
// if transfer, err := s.transferStore.GetTransferByReference(ctx, reference); err == nil { // if transfer, err := s.transferStore.GetTransferByReference(ctx, reference); err == nil {
@ -85,13 +117,20 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
ReferenceNumber: reference, ReferenceNumber: reference,
// ReceiverWalletID: 1, // ReceiverWalletID: 1,
SenderWalletID: domain.ValidInt64{ SenderWalletID: domain.ValidInt64{
Value: senderWallet.ID, Value: senderWallet.RegularID,
Valid: true, Valid: true,
}, },
Verified: false, Verified: false,
Status: string(domain.STATUS_PENDING),
} }
payload := domain.ChapaDepositRequest{ userPhoneNum := user.PhoneNumber[len(user.PhoneNumber)-9:]
if len(user.PhoneNumber) >= 9 {
userPhoneNum = "0" + userPhoneNum
}
payload := domain.ChapaInitDepositRequest{
Amount: amount, Amount: amount,
Currency: "ETB", Currency: "ETB",
Email: user.Email, Email: user.Email,
@ -100,6 +139,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
TxRef: reference, TxRef: reference,
CallbackURL: s.cfg.CHAPA_CALLBACK_URL, CallbackURL: s.cfg.CHAPA_CALLBACK_URL,
ReturnURL: s.cfg.CHAPA_RETURN_URL, ReturnURL: s.cfg.CHAPA_RETURN_URL,
PhoneNumber: userPhoneNum,
} }
// Initialize payment with Chapa // Initialize payment with Chapa
@ -124,170 +164,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
return response.CheckoutURL, nil return response.CheckoutURL, nil
} }
func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) { func (s *Service) ProcessVerifyDepositWebhook(ctx context.Context, req domain.ChapaWebhookPayment) error {
// Parse and validate amount
amount, err := strconv.ParseInt(req.Amount, 10, 64)
if err != nil || amount <= 0 {
return nil, domain.ErrInvalidWithdrawalAmount
}
// Get user's wallet
wallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to get user wallets: %w", err)
}
var withdrawWallet domain.Wallet
for _, wallet := range wallets {
if wallet.IsWithdraw {
withdrawWallet = wallet
break
}
}
if withdrawWallet.ID == 0 {
return nil, errors.New("withdrawal wallet not found")
}
// Check balance
if withdrawWallet.Balance < domain.Currency(amount) {
return nil, domain.ErrInsufficientBalance
}
// Generate unique reference
reference := uuid.New().String()
createTransfer := domain.CreateTransfer{
Message: fmt.Sprintf("Withdrawing %d into wallet using chapa. Reference Number %s", amount, reference),
Amount: domain.Currency(amount),
Type: domain.WITHDRAW,
SenderWalletID: domain.ValidInt64{
Value: withdrawWallet.ID,
Valid: true,
},
Status: string(domain.PaymentStatusPending),
Verified: false,
ReferenceNumber: reference,
PaymentMethod: domain.TRANSFER_CHAPA,
}
transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer)
if err != nil {
return nil, fmt.Errorf("failed to create transfer record: %w", err)
}
// Initiate transfer with Chapa
transferReq := domain.ChapaWithdrawalRequest{
AccountName: req.AccountName,
AccountNumber: req.AccountNumber,
Amount: fmt.Sprintf("%d", amount),
Currency: req.Currency,
Reference: reference,
// BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
BankCode: req.BankCode,
}
success, err := s.chapaClient.InitiateTransfer(ctx, transferReq)
if err != nil {
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
return nil, fmt.Errorf("failed to initiate transfer: %w", err)
}
if !success {
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
return nil, errors.New("chapa rejected the transfer request")
}
// Update withdrawal status to processing
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil {
return nil, fmt.Errorf("failed to update withdrawal status: %w", err)
}
// Deduct from wallet (or wait for webhook confirmation depending on your flow)
newBalance := withdrawWallet.Balance - domain.Currency(amount)
if err := s.walletStore.UpdateBalance(ctx, withdrawWallet.ID, newBalance); err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
}
return &transfer, nil
}
func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
banks, err := s.chapaClient.FetchSupportedBanks(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch banks: %w", err)
}
return banks, nil
}
func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
// Lookup transfer by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err)
}
if transfer.Verified {
return &domain.ChapaVerificationResponse{
Status: string(domain.PaymentStatusCompleted),
Amount: float64(transfer.Amount) / 100,
Currency: "ETB",
}, nil
}
// Validate sender wallet
if !transfer.SenderWalletID.Valid {
return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
}
var verification *domain.ChapaVerificationResponse
// Decide verification method based on type
switch strings.ToLower(string(transfer.Type)) {
case "deposit":
// Use Chapa Payment Verification
verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err)
}
if verification.Status == string(domain.PaymentStatusSuccessful) {
// Mark verified
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err)
}
// Credit wallet
_, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value,
transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{},
fmt.Sprintf("Added %v to wallet using Chapa", transfer.Amount.Float32()))
if err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err)
}
}
case "withdraw":
// Use Chapa Transfer Verification
verification, err = s.chapaClient.ManualVerifyTransfer(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err)
}
if verification.Status == string(domain.PaymentStatusSuccessful) {
// Mark verified (withdraw doesn't affect balance)
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return nil, fmt.Errorf("failed to mark withdrawal transfer as verified: %w", err)
}
}
default:
return nil, fmt.Errorf("unsupported transfer type: %s", transfer.Type)
}
return verification, nil
}
func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error {
// Find payment by reference // Find payment by reference
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference) payment, err := s.transferStore.GetTransferByReference(ctx, req.TxRef)
if err != nil { if err != nil {
return domain.ErrPaymentNotFound return domain.ErrPaymentNotFound
} }
@ -309,15 +188,19 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
// } // }
// If payment is completed, credit user's wallet // If payment is completed, credit user's wallet
if transfer.Status == string(domain.PaymentStatusSuccessful) { if req.Status == string(domain.PaymentStatusSuccessful) {
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil { if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
return fmt.Errorf("failed to update is payment verified value: %w", err)
}
if err := s.transferStore.UpdateTransferStatus(ctx, payment.ID, string(domain.DepositStatusCompleted)); err != nil {
return fmt.Errorf("failed to update payment status: %w", err) return fmt.Errorf("failed to update payment status: %w", err)
} }
if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{ if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{
ReferenceNumber: domain.ValidString{ ReferenceNumber: domain.ValidString{
Value: transfer.Reference, Value: req.TxRef,
}, },
}, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil { }, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err) return fmt.Errorf("failed to credit user wallet: %w", err)
@ -327,9 +210,209 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
return nil return nil
} }
func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domain.ChapaWebHookPayment) error { func (s *Service) CancelDeposit(ctx context.Context, userID int64, txRef string) (domain.ChapaCancelResponse, error) {
// Validate input
if txRef == "" {
return domain.ChapaCancelResponse{}, fmt.Errorf("transaction reference is required")
}
// Retrieve user to verify ownership / context (optional but good practice)
user, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to get user: %w", err)
}
fmt.Printf("\n\nAttempting to cancel Chapa transaction: %s for user %s (%d)\n\n", txRef, user.Email, userID)
// Call Chapa API to cancel transaction
cancelResp, err := s.chapaClient.CancelTransaction(ctx, txRef)
if err != nil {
return domain.ChapaCancelResponse{}, fmt.Errorf("failed to cancel transaction via Chapa: %w", err)
}
// Update transfer/payment status locally
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err != nil {
// Log but do not block cancellation if remote succeeded
fmt.Printf("Warning: unable to find local transfer for txRef %s: %v\n", txRef, err)
} else {
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.STATUS_CANCELLED)); err != nil {
fmt.Printf("Warning: failed to update transfer status for txRef %s: %v\n", txRef, err)
}
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, false); err != nil {
fmt.Printf("Warning: failed to update transfer status for txRef %s: %v\n", txRef, err)
}
}
fmt.Printf("\n\nChapa cancellation response: %+v\n\n", cancelResp)
return cancelResp, nil
}
func (s *Service) FetchAllTransactions(ctx context.Context) ([]domain.ChapaTransaction, error) {
// Call Chapa API to get all transactions
resp, err := s.chapaClient.GetAllTransactions(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch transactions from Chapa: %w", err)
}
if resp.Status != "success" {
return nil, fmt.Errorf("chapa API returned non-success status: %s", resp.Status)
}
transactions := make([]domain.ChapaTransaction, 0, len(resp.Data.Transactions))
// Map API transactions to domain transactions
for _, t := range resp.Data.Transactions {
tx := domain.ChapaTransaction{
Status: t.Status,
RefID: t.RefID,
Type: t.Type,
CreatedAt: t.CreatedAt,
Currency: t.Currency,
Amount: t.Amount,
Charge: t.Charge,
TransID: t.TransID,
PaymentMethod: t.PaymentMethod,
Customer: domain.ChapaCustomer{
ID: t.Customer.ID,
Email: t.Customer.Email,
FirstName: t.Customer.FirstName,
LastName: t.Customer.LastName,
Mobile: t.Customer.Mobile,
},
}
transactions = append(transactions, tx)
}
return transactions, nil
}
func (s *Service) FetchTransactionEvents(ctx context.Context, refID string) ([]domain.ChapaTransactionEvent, error) {
if refID == "" {
return nil, fmt.Errorf("transaction reference ID is required")
}
// Call Chapa client to fetch transaction events
events, err := s.chapaClient.GetTransactionEvents(ctx, refID)
if err != nil {
return nil, fmt.Errorf("failed to fetch transaction events from Chapa: %w", err)
}
// Optional: Transform or filter events if needed
transformedEvents := make([]domain.ChapaTransactionEvent, 0, len(events))
for _, e := range events {
transformedEvents = append(transformedEvents, domain.ChapaTransactionEvent{
Item: e.Item,
Message: e.Message,
Type: e.Type,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
})
}
return transformedEvents, nil
}
func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) {
// Parse and validate amount
amount, err := strconv.ParseFloat(req.Amount, 64)
if err != nil || amount <= 0 {
return nil, domain.ErrInvalidWithdrawalAmount
}
// Get user's wallet
wallet, err := s.walletStore.GetCustomerWallet(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to get user wallets: %w", err)
}
// var withdrawWallet domain.Wallet
// for _, wallet := range wallets {
// if wallet.IsWithdraw {
// withdrawWallet = wallet
// break
// }
// }
// if withdrawWallet.ID == 0 {
// return nil, errors.New("withdrawal wallet not found")
// }
// Check balance
if float64(wallet.RegularBalance) < float64(amount) {
return nil, domain.ErrInsufficientBalance
}
// Generate unique reference
reference := uuid.New().String()
createTransfer := domain.CreateTransfer{
Message: fmt.Sprintf("Withdrawing %f into wallet using chapa. Reference Number %s", amount, reference),
Amount: domain.Currency(amount),
Type: domain.WITHDRAW,
SenderWalletID: domain.ValidInt64{
Value: wallet.RegularID,
Valid: true,
},
Status: string(domain.PaymentStatusPending),
Verified: false,
ReferenceNumber: reference,
PaymentMethod: domain.TRANSFER_CHAPA,
}
transfer, err := s.transferStore.CreateTransfer(ctx, createTransfer)
if err != nil {
return nil, fmt.Errorf("failed to create transfer record: %w", err)
}
// Initiate transfer with Chapa
transferReq := domain.ChapaWithdrawalRequest{
AccountName: req.AccountName,
AccountNumber: req.AccountNumber,
Amount: fmt.Sprintf("%f", amount),
Currency: req.Currency,
Reference: reference,
// BeneficiaryName: fmt.Sprintf("%s %s", user.FirstName, user.LastName),
BankCode: req.BankCode,
}
newBalance := float64(wallet.RegularBalance) - float64(amount)
if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
}
success, err := s.chapaClient.InitializeTransfer(ctx, transferReq)
if err != nil {
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
newBalance := float64(wallet.RegularBalance) + float64(amount)
if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
}
return nil, fmt.Errorf("failed to initiate transfer: %w", err)
}
if !success {
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
newBalance := float64(wallet.RegularBalance) + float64(amount)
if err := s.walletStore.UpdateBalance(ctx, wallet.RegularID, domain.Currency(newBalance)); err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err)
}
return nil, errors.New("chapa rejected the transfer request")
}
// Update withdrawal status to processing
// if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil {
// return nil, fmt.Errorf("failed to update withdrawal status: %w", err)
// }
// Deduct from wallet (or wait for webhook confirmation depending on your flow)
return &transfer, nil
}
func (s *Service) ProcessVerifyWithdrawWebhook(ctx context.Context, req domain.ChapaWebhookTransfer) error {
// Find payment by reference // Find payment by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference) transfer, err := s.transferStore.GetTransferByReference(ctx, req.Reference)
if err != nil { if err != nil {
return domain.ErrPaymentNotFound return domain.ErrPaymentNotFound
} }
@ -350,7 +433,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
// verified = true // verified = true
// } // }
if payment.Status == string(domain.PaymentStatusSuccessful) { if req.Status == string(domain.PaymentStatusSuccessful) {
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return fmt.Errorf("failed to update payment status: %w", err) return fmt.Errorf("failed to update payment status: %w", err)
} // If payment is completed, credit user's walle } // If payment is completed, credit user's walle
@ -365,3 +448,274 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
return nil return nil
} }
func (s *Service) GetPaymentReceiptURL(refId string) (string, error) {
if refId == "" {
return "", fmt.Errorf("reference ID cannot be empty")
}
receiptURL := s.cfg.CHAPA_RECEIPT_URL + refId
return receiptURL, nil
}
func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (any, error) {
// Lookup transfer by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("transfer not found for reference %s: %w", txRef, err)
}
// If already verified, just return a completed response
if transfer.Verified {
return map[string]any{}, errors.New("transfer already verified")
}
// Validate sender wallet
if !transfer.SenderWalletID.Valid {
return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
}
var verification any
switch strings.ToLower(string(transfer.Type)) {
case string(domain.DEPOSIT):
// Verify Chapa payment
verification, err := s.chapaClient.ManualVerifyPayment(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err)
}
if strings.ToLower(verification.Data.Status) == "success" ||
verification.Status == string(domain.PaymentStatusCompleted) {
// Credit wallet
_, err := s.walletStore.AddToWallet(ctx,
transfer.SenderWalletID.Value,
transfer.Amount,
domain.ValidInt64{},
domain.TRANSFER_CHAPA,
domain.PaymentDetails{},
fmt.Sprintf("Added %.2f ETB to wallet using Chapa", transfer.Amount.Float32()))
if err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err)
}
// Mark verified in DB
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return nil, fmt.Errorf("failed to mark deposit transfer as verified: %w", err)
}
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.DepositStatusCompleted)); err != nil {
return nil, fmt.Errorf("failed to update deposit transfer status: %w", err)
}
}
case string(domain.WITHDRAW):
// Verify Chapa transfer
verification, err := s.chapaClient.ManualVerifyTransfer(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify withdrawal with Chapa: %w", err)
}
if strings.ToLower(verification.Data.Status) == "success" ||
verification.Status == string(domain.PaymentStatusCompleted) {
// Deduct wallet
_, err := s.walletStore.DeductFromWallet(ctx,
transfer.SenderWalletID.Value,
transfer.Amount,
domain.ValidInt64{},
domain.TRANSFER_CHAPA,
fmt.Sprintf("Deducted %.2f ETB from wallet using Chapa", transfer.Amount.Float32()))
if err != nil {
return nil, fmt.Errorf("failed to debit wallet: %w", err)
}
// Mark verified in DB
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return nil, fmt.Errorf("failed to mark withdraw transfer as verified: %w", err)
}
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusCompleted)); err != nil {
return nil, fmt.Errorf("failed to update withdraw transfer status: %w", err)
}
}
default:
return nil, fmt.Errorf("unsupported transfer type: %s", transfer.Type)
}
return verification, nil
}
func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.BankData, error) {
banks, err := s.chapaClient.FetchSupportedBanks(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch banks: %w", err)
}
return banks, nil
}
func (s *Service) GetAllTransfers(ctx context.Context) (*domain.ChapaTransfersListResponse, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.cfg.CHAPA_BASE_URL+"/transfers", nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
resp, err := s.chapaClient.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch transfers: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
}
var result domain.ChapaTransfersListResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
// Return the decoded result directly; no intermediate dynamic map needed
return &result, nil
}
func (s *Service) GetAccountBalance(ctx context.Context, currencyCode string) ([]domain.Balance, error) {
URL := s.cfg.CHAPA_BASE_URL + "/balances"
if currencyCode != "" {
URL = fmt.Sprintf("%s/%s", URL, strings.ToLower(currencyCode))
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create balance request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
resp, err := s.chapaClient.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute balance request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
}
var result struct {
Status string `json:"status"`
Message string `json:"message"`
Data []domain.Balance `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode balance response: %w", err)
}
return result.Data, nil
}
func (s *Service) SwapCurrency(ctx context.Context, reqBody domain.SwapRequest) (*domain.SwapResponse, error) {
URL := s.cfg.CHAPA_BASE_URL + "/swap"
// Normalize currency codes
reqBody.From = strings.ToUpper(reqBody.From)
reqBody.To = strings.ToUpper(reqBody.To)
// Marshal request body
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal swap payload: %w", err)
}
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("failed to create swap request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
// Execute request
resp, err := s.chapaClient.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute swap request: %w", err)
}
defer resp.Body.Close()
// Handle unexpected status
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
}
// Decode response
var result domain.SwapResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode swap response: %w", err)
}
return &result, nil
}
// func (s *Service) InitiateSwap(ctx context.Context, amount float64, from, to string) (*domain.SwapResponse, error) {
// if amount < 1 {
// return nil, fmt.Errorf("amount must be at least 1 USD")
// }
// if strings.ToUpper(from) != "USD" || strings.ToUpper(to) != "ETB" {
// return nil, fmt.Errorf("only USD to ETB swap is supported")
// }
// payload := domain.SwapRequest{
// Amount: amount,
// From: strings.ToUpper(from),
// To: strings.ToUpper(to),
// }
// // payload := map[string]any{
// // "amount": amount,
// // "from": strings.ToUpper(from),
// // "to": strings.ToUpper(to),
// // }
// body, err := json.Marshal(payload)
// if err != nil {
// return nil, fmt.Errorf("failed to encode swap payload: %w", err)
// }
// req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.chapa.co/v1/swap", bytes.NewBuffer(body))
// if err != nil {
// return nil, fmt.Errorf("failed to create swap request: %w", err)
// }
// req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.cfg.CHAPA_SECRET_KEY))
// req.Header.Set("Content-Type", "application/json")
// resp, err := s.chapaClient.httpClient.Do(req)
// if err != nil {
// return nil, fmt.Errorf("failed to execute swap request: %w", err)
// }
// defer resp.Body.Close()
// if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
// bodyBytes, _ := io.ReadAll(resp.Body)
// return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(bodyBytes))
// }
// var result struct {
// Message string `json:"message"`
// Status string `json:"status"`
// Data domain.SwapResponse `json:"data"`
// }
// if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
// return nil, fmt.Errorf("failed to decode swap response: %w", err)
// }
// return &result.Data, nil
// }

View File

@ -14,4 +14,5 @@ type EnetPulseService interface {
FetchTournamentParticipants(ctx context.Context, tournamentID string) error FetchTournamentParticipants(ctx context.Context, tournamentID string) error
FetchPreMatchOdds(ctx context.Context, params domain.PreMatchOddsRequest) (*domain.PreMatchOddsResponse, error) FetchPreMatchOdds(ctx context.Context, params domain.PreMatchOddsRequest) (*domain.PreMatchOddsResponse, error)
FetchCountryFlag(ctx context.Context, countryFK int64) (*domain.ImageResponse, error) FetchCountryFlag(ctx context.Context, countryFK int64) (*domain.ImageResponse, error)
GetAllPreoddsWithBettingOffers(ctx context.Context) ([]domain.EnetpulsePreodds, error)
} }

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
@ -142,6 +143,10 @@ func (s *Service) FetchAndStoreTournamentTemplates(ctx context.Context) error {
} }
for _, sport := range sports { for _, sport := range sports {
if sport.SportID != "1" {
continue
} else {
// 2⃣ Compose URL for each sport using its Enetpulse sportFK // 2⃣ Compose URL for each sport using its Enetpulse sportFK
url := fmt.Sprintf( url := fmt.Sprintf(
"http://eapi.enetpulse.com/tournament_template/list/?sportFK=%s&username=%s&token=%s", "http://eapi.enetpulse.com/tournament_template/list/?sportFK=%s&username=%s&token=%s",
@ -232,9 +237,11 @@ func (s *Service) FetchAndStoreTournamentTemplates(ctx context.Context) error {
continue continue
} }
} }
break
}
} }
fmt.Println("✅ Successfully fetched and stored all tournament templates") // fmt.Println("✅ Successfully fetched and stored all tournament templates")
return nil return nil
} }
@ -360,7 +367,7 @@ func (s *Service) FetchAndStoreTournamentStages(ctx context.Context) error {
for _, t := range tournaments { for _, t := range tournaments {
// Compose URL for each tournament // Compose URL for each tournament
url := fmt.Sprintf( url := fmt.Sprintf(
"http://eapi.enetpulse.com/tournament_stage/list/?language_typeFK=3&tz=Europe/Sofia&tournamentFK=%s&username=%s&token=%s", "https://eapi.enetpulse.com/tournament_stage/list/?language_typeFK=3&tz=Europe/Sofia&tournamentFK=%s&username=%s&token=%s",
t.TournamentID, t.TournamentID,
s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.UserName,
s.cfg.EnetPulseConfig.Token, s.cfg.EnetPulseConfig.Token,
@ -452,6 +459,691 @@ func (s *Service) GetAllTournamentStages(ctx context.Context) ([]domain.Enetpuls
return stages, nil return stages, nil
} }
func (s *Service) FetchAndStoreFixtures(ctx context.Context, date string) error {
// 1⃣ Fetch all sports from DB
sports, err := s.store.GetAllEnetpulseSports(ctx)
if err != nil {
return fmt.Errorf("failed to fetch sports from DB: %w", err)
}
// Define API fixture struct
type Fixture struct {
FixtureID string `json:"id"`
Name string `json:"name"`
SportFK string `json:"sportFK"`
TournamentFK string `json:"tournamentFK"`
TournamentTemplateFK string `json:"tournament_templateFK"`
TournamentStageFK string `json:"tournament_stageFK"`
TournamentStageName string `json:"tournament_stage_name"`
TournamentName string `json:"tournament_name"`
TournamentTemplateName string `json:"tournament_template_name"`
SportName string `json:"sport_name"`
Gender string `json:"gender"`
StartDate string `json:"startdate"` // ISO 8601
StatusType string `json:"status_type"`
StatusDescFK string `json:"status_descFK"`
RoundTypeFK string `json:"round_typeFK"`
UpdatesCount string `json:"n"` // convert to int
LastUpdatedAt string `json:"ut"` // parse to time.Time
}
// 2⃣ Loop through each sport
for _, sport := range sports {
if sport.SportID != "1" {
continue
}
url := fmt.Sprintf(
"https://eapi.enetpulse.com/event/fixtures/?username=%s&token=%s&sportFK=%s&language_typeFK=3&date=%s",
s.cfg.EnetPulseConfig.UserName,
s.cfg.EnetPulseConfig.Token,
sport.SportID,
date,
)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
fmt.Printf("creating request for sport %s: %v\n", sport.SportID, err)
continue
}
resp, err := s.httpClient.Do(req)
if err != nil {
fmt.Printf("requesting fixtures for sport %s: %v\n", sport.SportID, err)
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
fmt.Printf("failed to fetch fixtures for sport %s (status %d): %s\n",
sport.SportID, resp.StatusCode, string(body))
continue
}
// 3⃣ Decode API response
var fixturesResp struct {
Events map[string]Fixture `json:"events"`
}
if err := json.NewDecoder(resp.Body).Decode(&fixturesResp); err != nil {
fmt.Printf("decoding fixtures for sport %s: %v\n", sport.SportID, err)
continue
}
// 4⃣ Iterate and upsert fixtures
for _, fx := range fixturesResp.Events {
// Parse StartDate and LastUpdatedAt
startDate, err := time.Parse(time.RFC3339, fx.StartDate)
if err != nil {
fmt.Printf("invalid startDate for fixture %s: %v\n", fx.FixtureID, err)
continue
}
lastUpdated, err := time.Parse(time.RFC3339, fx.LastUpdatedAt)
if err != nil {
fmt.Printf("invalid lastUpdatedAt for fixture %s: %v\n", fx.FixtureID, err)
continue
}
// Convert UpdatesCount
updatesCount, err := strconv.Atoi(fx.UpdatesCount)
if err != nil {
fmt.Printf("invalid updatesCount for fixture %s: %v\n", fx.FixtureID, err)
updatesCount = 0
}
fixture := domain.CreateEnetpulseFixture{
FixtureID: fx.FixtureID,
Name: fx.Name,
SportFK: fx.SportFK,
TournamentFK: fx.TournamentFK,
TournamentTemplateFK: fx.TournamentTemplateFK,
TournamentStageFK: fx.TournamentStageFK,
TournamentStageName: fx.TournamentStageName,
TournamentName: fx.TournamentName,
TournamentTemplateName: fx.TournamentTemplateName,
SportName: fx.SportName,
Gender: fx.Gender,
StartDate: startDate,
StatusType: fx.StatusType,
StatusDescFK: fx.StatusDescFK,
RoundTypeFK: fx.RoundTypeFK,
UpdatesCount: updatesCount,
LastUpdatedAt: lastUpdated,
}
// 5⃣ Save fixture using UPSERT repository method
if _, err := s.store.CreateEnetpulseFixture(ctx, fixture); err != nil {
fmt.Printf("failed upserting fixture %s: %v\n", fx.FixtureID, err)
continue
}
}
fmt.Printf("✅ Successfully fetched and stored fixtures for sport %s\n", sport.SportID)
break
}
return nil
}
func (s *Service) GetAllFixtures(ctx context.Context) ([]domain.EnetpulseFixture, error) {
// 1⃣ Fetch all from store
fixtures, err := s.store.GetAllEnetpulseFixtures(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch fixtures from DB: %w", err)
}
return fixtures, nil
}
func (s *Service) FetchAndStoreResults(ctx context.Context) error {
sports, err := s.store.GetAllEnetpulseSports(ctx)
if err != nil {
return fmt.Errorf("failed to fetch sports from DB: %w", err)
}
for _, sport := range sports {
if sport.SportID != "1" {
continue
}
today := time.Now().Format("2006-01-02")
url := fmt.Sprintf(
"http://eapi.enetpulse.com/event/results/?sportFK=%s&date=%s&username=%s&token=%s",
sport.SportID,
today,
s.cfg.EnetPulseConfig.UserName,
s.cfg.EnetPulseConfig.Token,
)
fmt.Println("Fetching results:", url)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("creating results request for sport %s: %w", sport.SportID, err)
}
resp, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("requesting results for sport %s: %w", sport.SportID, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to fetch results for sport %s (status %d): %s",
sport.SportID, resp.StatusCode, string(body))
}
var data struct {
Events []struct {
ID string `json:"id"`
Name string `json:"name"`
SportFK string `json:"sportFK"`
TournamentFK string `json:"tournamentFK"`
TournamentTemplateFK string `json:"tournament_templateFK"`
TournamentStageFK string `json:"tournament_stageFK"`
TournamentStageName string `json:"tournament_stage_name"`
TournamentName string `json:"tournament_name"`
TournamentTemplateName string `json:"tournament_template_name"`
SportName string `json:"sport_name"`
StartDate string `json:"startdate"`
StatusType string `json:"status_type"`
StatusDescFK string `json:"status_descFK"`
RoundTypeFK string `json:"round_typeFK"`
N string `json:"n"`
UT string `json:"ut"`
Property map[string]struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
N string `json:"n"`
UT string `json:"ut"`
} `json:"property"`
EventParticipants map[string]struct {
ID string `json:"id"`
Number string `json:"number"`
ParticipantFK string `json:"participantFK"`
EventFK string `json:"eventFK"`
Result map[string]struct {
ID string `json:"id"`
EventParticipantsFK string `json:"event_participantsFK"`
ResultTypeFK string `json:"result_typeFK"`
ResultCode string `json:"result_code"`
Value string `json:"value"`
N string `json:"n"`
UT string `json:"ut"`
} `json:"result"`
Participant struct {
ID string `json:"id"`
Name string `json:"name"`
Gender string `json:"gender"`
Type string `json:"type"`
CountryFK string `json:"countryFK"`
CountryName string `json:"country_name"`
} `json:"participant"`
} `json:"event_participants"`
} `json:"events"`
}
bodyBytes, _ := io.ReadAll(resp.Body)
if err := json.Unmarshal(bodyBytes, &data); err != nil {
return fmt.Errorf("decoding results failed: %w", err)
}
for _, event := range data.Events {
// 1⃣ Create result record
lastUpdatedAt, _ := time.Parse(time.RFC3339, event.UT)
startDate, _ := time.Parse(time.RFC3339, event.StartDate)
createResult := domain.CreateEnetpulseResult{
ResultID: event.ID,
Name: event.Name,
SportFK: event.SportFK,
TournamentFK: event.TournamentFK,
TournamentTemplateFK: event.TournamentTemplateFK,
TournamentStageName: event.TournamentStageName,
TournamentName: event.TournamentName,
TournamentTemplateName: event.TournamentTemplateName,
SportName: event.SportName,
StartDate: startDate,
StatusType: event.StatusType,
StatusDescFK: event.StatusDescFK,
RoundTypeFK: event.RoundTypeFK,
LastUpdatedAt: lastUpdatedAt,
}
if _, err := s.store.CreateEnetpulseResult(ctx, createResult); err != nil {
fmt.Printf("❌ failed to store result %s: %v\n", event.ID, err)
continue
}
// 2⃣ Create referees (type == "ref:participant")
for _, prop := range event.Property {
if strings.HasPrefix(prop.Type, "ref:participant") {
refCreatedAt, _ := time.Parse(time.RFC3339, prop.UT)
ref := domain.CreateEnetpulseResultReferee{
ResultFk: event.ID,
RefereeFk: prop.Value,
LastUpdatedAt: refCreatedAt,
}
if _, err := s.store.CreateEnetpulseResultReferee(ctx, ref); err != nil {
fmt.Printf("⚠️ failed to create referee %s: %v\n", prop.Name, err)
}
}
}
// 3⃣ Create participants + their results
for _, ep := range event.EventParticipants {
p := domain.CreateEnetpulseResultParticipant{
ParticipantMapID: ep.ID,
ResultFk: ep.EventFK,
ParticipantFk: ep.ParticipantFK,
Name: ep.Participant.Name,
CountryFk: ep.Participant.CountryFK,
CountryName: ep.Participant.CountryName,
}
if _, err := s.store.CreateEnetpulseResultParticipant(ctx, p); err != nil {
fmt.Printf("⚠️ failed to create participant %s: %v\n", ep.Participant.Name, err)
continue
}
}
}
break // stop after the first sport (football)
}
fmt.Println("✅ Successfully fetched and stored EnetPulse results + participants + referees")
return nil
}
func (s *Service) GetAllResults(ctx context.Context) ([]domain.EnetpulseResult, error) {
results, err := s.store.GetAllEnetpulseResults(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch results from DB: %w", err)
}
fmt.Printf("✅ Retrieved %d results from DB\n", len(results))
return results, nil
}
// FetchAndStoreOutcomeTypes fetches outcome types from EnetPulse API and stores them in the DB.
func (s *Service) FetchAndStoreOutcomeTypes(ctx context.Context) error {
// 1⃣ Compose EnetPulse API URL
url := fmt.Sprintf(
"http://eapi.enetpulse.com/static/outcome_type/?language_typeFK=3&username=%s&token=%s",
s.cfg.EnetPulseConfig.UserName,
s.cfg.EnetPulseConfig.Token,
)
// 2⃣ Create HTTP request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("failed to create outcome types request: %w", err)
}
// 3⃣ Execute request
resp, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to call EnetPulse outcome_type API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("unexpected status %d fetching outcome types: %s", resp.StatusCode, string(body))
}
// 4⃣ Decode JSON response
var outcomeResp struct {
OutcomeTypes map[string]struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
N string `json:"n"`
UT string `json:"ut"`
} `json:"outcome_type"`
}
if err := json.NewDecoder(resp.Body).Decode(&outcomeResp); err != nil {
return fmt.Errorf("failed to decode outcome types JSON: %w", err)
}
// 5⃣ Iterate and store each outcome type
for _, ot := range outcomeResp.OutcomeTypes {
updatesCount := 0
if ot.N != "" {
if n, err := strconv.Atoi(ot.N); err == nil {
updatesCount = n
}
}
lastUpdatedAt, err := time.Parse(time.RFC3339, ot.UT)
if err != nil {
lastUpdatedAt = time.Time{}
}
createOutcome := domain.CreateEnetpulseOutcomeType{
OutcomeTypeID: ot.ID,
Name: ot.Name,
Description: ot.Description,
UpdatesCount: int32(updatesCount),
LastUpdatedAt: lastUpdatedAt,
}
// 6⃣ Save to DB (upsert)
if _, err := s.store.CreateEnetpulseOutcomeType(ctx, createOutcome); err != nil {
// Optionally log the failure, continue to next
continue
}
}
// s.logger.Info("✅ Successfully fetched and stored all EnetPulse outcome types")
return nil
}
// GetAllOutcomeTypes retrieves all stored outcome types from the DB.
func (s *Service) GetAllOutcomeTypes(ctx context.Context) ([]domain.EnetpulseOutcomeType, error) {
outcomes, err := s.store.GetAllEnetpulseOutcomeTypes(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch outcome types from DB: %w", err)
}
// s.logger.Info("✅ Fetched outcome types from DB", zap.Int("count", len(outcomes)))
return outcomes, nil
}
func (s *Service) FetchAndStorePreodds(ctx context.Context) error {
// 1⃣ Fetch all fixtures
fixtures, err := s.store.GetAllEnetpulseFixtures(ctx)
if err != nil {
return fmt.Errorf("failed to fetch fixtures: %w", err)
}
// 2⃣ Fetch all outcome types
outcomeTypes, err := s.store.GetAllEnetpulseOutcomeTypes(ctx)
if err != nil {
return fmt.Errorf("failed to fetch outcome types: %w", err)
}
// 3⃣ Loop through each fixture
for _, fixture := range fixtures {
// 4⃣ Loop through each outcome type
for _, outcome := range outcomeTypes {
url := fmt.Sprintf(
"http://eapi.enetpulse.com/preodds/event/?objectFK=%s&odds_providerFK=%s&outcome_typeFK=%s&username=%s&token=%s",
fixture.FixtureID,
s.cfg.EnetPulseConfig.ProviderID,
outcome.OutcomeTypeID,
s.cfg.EnetPulseConfig.UserName,
s.cfg.EnetPulseConfig.Token,
)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
continue
}
resp, err := s.httpClient.Do(req)
if err != nil {
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
continue
}
// Struct adjusted exactly to match JSON structure
var preoddsResp struct {
Preodds map[string]struct {
ID string `json:"id"`
OutcomeTypeFK string `json:"outcome_typeFK"`
OutcomeScopeFK string `json:"outcome_scopeFK"`
OutcomeSubtypeFK string `json:"outcome_subtypeFK"`
EventParticipantNumber string `json:"event_participant_number"`
Iparam string `json:"iparam"`
Iparam2 string `json:"iparam2"`
Dparam string `json:"dparam"`
Dparam2 string `json:"dparam2"`
Sparam string `json:"sparam"`
N string `json:"n"`
UT string `json:"ut"`
PreoddsBettingOffers map[string]struct {
ID string `json:"id"`
BettingOfferStatusFK string `json:"bettingoffer_statusFK"`
OddsProviderFK string `json:"odds_providerFK"`
Odds string `json:"odds"`
OddsOld string `json:"odds_old"`
Active string `json:"active"`
CouponKey string `json:"couponKey"`
N string `json:"n"`
UT string `json:"ut"`
} `json:"preodds_bettingoffers"`
} `json:"preodds"`
}
if err := json.NewDecoder(resp.Body).Decode(&preoddsResp); err != nil {
continue
}
for _, p := range preoddsResp.Preodds {
// Convert numeric/string fields safely
updatesCount, _ := strconv.Atoi(defaultIfEmpty(p.N, "0"))
eventParticipantNumber, _ := strconv.Atoi(defaultIfEmpty(p.EventParticipantNumber, "0"))
lastUpdatedAt := parseTimeOrNow(p.UT)
createPreodds := domain.CreateEnetpulsePreodds{
PreoddsID: p.ID,
EventFK: fixture.FixtureID,
OutcomeTypeFK: p.OutcomeTypeFK,
OutcomeScopeFK: p.OutcomeScopeFK,
OutcomeSubtypeFK: p.OutcomeSubtypeFK,
EventParticipantNumber: eventParticipantNumber,
IParam: p.Iparam,
IParam2: p.Iparam2,
DParam: p.Dparam,
DParam2: p.Dparam2,
SParam: p.Sparam,
UpdatesCount: updatesCount,
LastUpdatedAt: lastUpdatedAt,
}
// Store preodds in DB
_, err := s.store.CreateEnetpulsePreodds(ctx, createPreodds)
if err != nil {
continue
}
// 5⃣ Loop through betting offers map
for _, o := range p.PreoddsBettingOffers {
bettingUpdates, _ := strconv.Atoi(defaultIfEmpty(o.N, "0"))
bettingLastUpdatedAt := parseTimeOrNow(o.UT)
odds, _ := strconv.ParseFloat(defaultIfEmpty(o.Odds, "0"), 64)
oddsOld, _ := strconv.ParseFloat(defaultIfEmpty(o.OddsOld, "0"), 64)
bettingOfferStatusFK, _ := strconv.Atoi(defaultIfEmpty(o.BettingOfferStatusFK, "0"))
oddsProviderFK, _ := strconv.Atoi(defaultIfEmpty(o.OddsProviderFK, "0"))
createOffer := domain.CreateEnetpulsePreoddsBettingOffer{
BettingOfferID: o.ID,
PreoddsFK: createPreodds.PreoddsID,
BettingOfferStatusFK: int32(bettingOfferStatusFK),
OddsProviderFK: int32(oddsProviderFK),
Odds: odds,
OddsOld: oddsOld,
Active: o.Active,
CouponKey: o.CouponKey,
UpdatesCount: bettingUpdates,
LastUpdatedAt: bettingLastUpdatedAt,
}
_, _ = s.store.CreateEnetpulsePreoddsBettingOffer(ctx, createOffer)
}
}
}
}
return nil
}
// Utility helpers
func defaultIfEmpty(val, def string) string {
if val == "" {
return def
}
return val
}
func parseTimeOrNow(t string) time.Time {
parsed, err := time.Parse(time.RFC3339, t)
if err != nil {
return time.Now().UTC()
}
return parsed
}
// helper function to parse string to int32 safely
func ParseStringToInt32(s string) int32 {
if s == "" {
return 0
}
i, _ := strconv.Atoi(s)
return int32(i)
}
func (s *Service) GetAllPreodds(ctx context.Context) ([]domain.EnetpulsePreodds, error) {
preodds, err := s.store.GetAllEnetpulsePreodds(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch preodds from DB: %w", err)
}
fmt.Printf("\n\nFetched Preodds are:%v\n\n", preodds)
return preodds, nil
}
// FetchAndStoreBettingOffers fetches betting offers from EnetPulse API and stores them in the DB.
func (s *Service) StoreBettingOffers(ctx context.Context, preoddsID string, oddsProviderIDs []int32) error {
// 1⃣ Compose API URL
providers := make([]string, len(oddsProviderIDs))
for i, p := range oddsProviderIDs {
providers[i] = strconv.Itoa(int(p))
}
url := fmt.Sprintf(
"http://eapi.enetpulse.com/preodds_bettingoffer/?preoddsFK=%s&odds_providerFK=%s&username=%s&token=%s",
preoddsID,
strings.Join(providers, ","),
s.cfg.EnetPulseConfig.UserName,
s.cfg.EnetPulseConfig.Token,
)
// 2⃣ Create HTTP request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("failed to create betting offer request: %w", err)
}
// 3⃣ Execute request
resp, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to call EnetPulse betting offer API: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("unexpected status %d fetching betting offers: %s", resp.StatusCode, string(body))
}
// 4⃣ Decode JSON response
var offerResp struct {
BettingOffers map[string]struct {
ID string `json:"id"`
PreoddsFK string `json:"preodds_fk"`
BettingOfferStatusFK int32 `json:"bettingoffer_status_fk"`
OddsProviderFK int32 `json:"odds_provider_fk"`
Odds float64 `json:"odds"`
OddsOld float64 `json:"odds_old"`
Active string `json:"active"`
CouponKey string `json:"coupon_key"`
N string `json:"n"`
UT string `json:"ut"`
} `json:"bettingoffer"`
}
if err := json.NewDecoder(resp.Body).Decode(&offerResp); err != nil {
return fmt.Errorf("failed to decode betting offers JSON: %w", err)
}
// 5⃣ Iterate and store each betting offer
for _, o := range offerResp.BettingOffers {
updatesCount := 0
if o.N != "" {
if n, err := strconv.Atoi(o.N); err == nil {
updatesCount = n
}
}
lastUpdatedAt, err := time.Parse(time.RFC3339, o.UT)
if err != nil {
lastUpdatedAt = time.Time{}
}
createOffer := domain.CreateEnetpulsePreoddsBettingOffer{
BettingOfferID: o.ID,
PreoddsFK: preoddsID,
BettingOfferStatusFK: o.BettingOfferStatusFK,
OddsProviderFK: o.OddsProviderFK,
Odds: o.Odds,
OddsOld: o.OddsOld,
Active: o.Active,
CouponKey: o.CouponKey,
UpdatesCount: int(updatesCount),
LastUpdatedAt: lastUpdatedAt,
}
// 6⃣ Save to DB
if _, err := s.store.CreateEnetpulsePreoddsBettingOffer(ctx, createOffer); err != nil {
// optionally log the failure and continue
continue
}
}
return nil
}
// GetAllBettingOffers retrieves all stored betting offers from the DB.
func (s *Service) GetAllBettingOffers(ctx context.Context) ([]domain.EnetpulsePreoddsBettingOffer, error) {
offers, err := s.store.GetAllEnetpulsePreoddsBettingOffers(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch betting offers from DB: %w", err)
}
return offers, nil
}
func (s *Service) GetAllPreoddsWithBettingOffers(ctx context.Context) ([]domain.EnetpulsePreodds, error) {
preodds, err := s.store.GetAllEnetpulsePreoddsWithBettingOffers(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch preodds with betting offers from DB: %w", err)
}
return preodds, nil
}
func (s *Service) GetFixturesWithPreodds(ctx context.Context) ([]domain.EnetpulseFixtureWithPreodds, error) {
// 1⃣ Fetch fixtures and their associated preodds from the repository
fixtures, err := s.store.GetFixturesWithPreodds(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch fixtures with preodds from DB: %w", err)
}
return fixtures, nil
}
func (s *Service) FetchTournamentTemplates(ctx context.Context) (*domain.TournamentTemplatesResponse, error) { func (s *Service) FetchTournamentTemplates(ctx context.Context) (*domain.TournamentTemplatesResponse, error) {
url := fmt.Sprintf( url := fmt.Sprintf(
"http://eapi.enetpulse.com/tournamenttemplate/list/?username=%s&token=%s", "http://eapi.enetpulse.com/tournamenttemplate/list/?username=%s&token=%s",
@ -701,9 +1393,9 @@ func (s *Service) FetchDailyEvents(ctx context.Context, req domain.DailyEventsRe
if req.TournamentTemplateFK != 0 { if req.TournamentTemplateFK != 0 {
query += fmt.Sprintf("&tournament_templateFK=%d", req.TournamentTemplateFK) query += fmt.Sprintf("&tournament_templateFK=%d", req.TournamentTemplateFK)
} }
if req.TournamentStageFK != 0 { // if req.TournamentStageFK != 0 {
query += fmt.Sprintf("&tournament_stageFK=%d", req.TournamentStageFK) // query += fmt.Sprintf("&tournament_stageFK=%d", req.TournamentStageFK)
} // }
// Optionals // Optionals
if req.Date != "" { if req.Date != "" {
@ -757,73 +1449,6 @@ func (s *Service) FetchDailyEvents(ctx context.Context, req domain.DailyEventsRe
return &dailyResp, nil return &dailyResp, nil
} }
func (s *Service) FetchFixtures(ctx context.Context, params domain.FixturesRequest) (*domain.FixturesResponse, error) {
// Build base URL
url := fmt.Sprintf("http://eapi.enetpulse.com/event/fixtures/?username=%s&token=%s",
s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token)
// Required filter: one of sportFK | tournament_templateFK | tournament_stageFK
if params.SportFK != 0 {
url += fmt.Sprintf("&sportFK=%d", params.SportFK)
}
if params.TournamentTemplateFK != 0 {
url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK)
}
if params.TournamentStageFK != 0 {
url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK)
}
// Optional filters
if params.LanguageTypeFK != 0 {
url += fmt.Sprintf("&language_typeFK=%d", params.LanguageTypeFK)
} else {
url += "&language_typeFK=3" // default to English
}
if params.Date != "" {
url += fmt.Sprintf("&date=%s", params.Date)
}
if params.Live != "" {
url += fmt.Sprintf("&live=%s", params.Live)
}
if params.IncludeVenue {
url += "&includeVenue=yes"
}
if !params.IncludeEventProperties {
url += "&includeEventProperties=no"
}
if params.IncludeCountryCodes {
url += "&includeCountryCodes=yes"
}
if params.IncludeFirstLastName {
url += "&includeFirstLastName=yes"
}
// Make request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("creating fixtures request: %w", err)
}
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("requesting fixtures: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
}
// Decode response
var fixturesResp domain.FixturesResponse
if err := json.NewDecoder(resp.Body).Decode(&fixturesResp); err != nil {
return nil, fmt.Errorf("decoding fixtures response: %w", err)
}
return &fixturesResp, nil
}
func (s *Service) FetchResults(ctx context.Context, params domain.ResultsRequest) (*domain.ResultsResponse, error) { func (s *Service) FetchResults(ctx context.Context, params domain.ResultsRequest) (*domain.ResultsResponse, error) {
// Build base URL // Build base URL
url := fmt.Sprintf("http://eapi.enetpulse.com/event/results/?username=%s&token=%s", url := fmt.Sprintf("http://eapi.enetpulse.com/event/results/?username=%s&token=%s",
@ -836,9 +1461,9 @@ func (s *Service) FetchResults(ctx context.Context, params domain.ResultsRequest
if params.TournamentTemplateFK != 0 { if params.TournamentTemplateFK != 0 {
url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK) url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK)
} }
if params.TournamentStageFK != 0 { // if params.TournamentStageFK != 0 {
url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) // url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK)
} // }
// Optional filters // Optional filters
if params.LanguageTypeFK != 0 { if params.LanguageTypeFK != 0 {
@ -974,7 +1599,7 @@ func (s *Service) FetchEventDetails(ctx context.Context, params domain.EventDeta
func (s *Service) FetchEventList(ctx context.Context, params domain.EventListRequest) (*domain.EventListResponse, error) { func (s *Service) FetchEventList(ctx context.Context, params domain.EventListRequest) (*domain.EventListResponse, error) {
// You must provide either TournamentFK or TournamentStageFK // You must provide either TournamentFK or TournamentStageFK
if params.TournamentFK == 0 && params.TournamentStageFK == 0 { if params.TournamentFK == 0 {
return nil, fmt.Errorf("either TournamentFK or TournamentStageFK is required") return nil, fmt.Errorf("either TournamentFK or TournamentStageFK is required")
} }
@ -986,9 +1611,9 @@ func (s *Service) FetchEventList(ctx context.Context, params domain.EventListReq
if params.TournamentFK != 0 { if params.TournamentFK != 0 {
url += fmt.Sprintf("&tournamentFK=%d", params.TournamentFK) url += fmt.Sprintf("&tournamentFK=%d", params.TournamentFK)
} }
if params.TournamentStageFK != 0 { // if params.TournamentStageFK != 0 {
url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) // url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK)
} // }
// Optional parameters // Optional parameters
if !params.IncludeEventProperties { if !params.IncludeEventProperties {
@ -1059,9 +1684,9 @@ func (s *Service) FetchParticipantFixtures(ctx context.Context, params domain.Pa
if params.TournamentTemplateFK != 0 { if params.TournamentTemplateFK != 0 {
url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK) url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK)
} }
if params.TournamentStageFK != 0 { // if params.TournamentStageFK != 0 {
url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) // url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK)
} // }
if params.Date != "" { if params.Date != "" {
url += "&date=" + params.Date url += "&date=" + params.Date
} }

View File

@ -87,9 +87,9 @@ func (s *ServiceImpl) ProcessBet365Odds(ctx context.Context) error {
Value: domain.STATUS_PENDING, Value: domain.STATUS_PENDING,
Valid: true, Valid: true,
}, },
Source: domain.ValidEventSource{ // Source: domain.ValidEventSource{
Value: domain.EVENT_SOURCE_BET365, // Value: domain.EVENT_SOURCE_BET365,
}, // },
}) })
if err != nil { if err != nil {
s.mongoLogger.Error( s.mongoLogger.Error(

View File

@ -237,10 +237,10 @@ func (s *Service) FetchB365ResultAndUpdateBets(ctx context.Context) error {
Value: time.Now(), Value: time.Now(),
Valid: true, Valid: true,
}, },
Source: domain.ValidEventSource{ // Source: domain.ValidEventSource{
Value: domain.EVENT_SOURCE_BET365, // Value: domain.EVENT_SOURCE_BET365,
Valid: true, // Valid: true,
}, // },
}) })
if err != nil { if err != nil {
@ -463,10 +463,10 @@ func (s *Service) CheckAndUpdateExpiredB365Events(ctx context.Context) (int64, e
Value: time.Now(), Value: time.Now(),
Valid: true, Valid: true,
}, },
Source: domain.ValidEventSource{ // Source: domain.ValidEventSource{
Value: domain.EVENT_SOURCE_BET365, // Value: domain.EVENT_SOURCE_BET365,
Valid: true, // Valid: true,
}, // },
}) })
if err != nil { if err != nil {
s.mongoLogger.Error( s.mongoLogger.Error(
@ -685,7 +685,7 @@ func (s *Service) GetBet365ResultForEvent(ctx context.Context, b365EventID strin
zap.String("b365EventID", b365EventID), zap.String("b365EventID", b365EventID),
zap.Error(err), 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 var commonResp domain.CommonResultResponse

View File

@ -135,16 +135,16 @@ func (s *SantimPayService) ProcessCallback(ctx context.Context, payload domain.S
return fmt.Errorf("invalid ThirdPartyId '%s': %w", payload.ThirdPartyId, err) return fmt.Errorf("invalid ThirdPartyId '%s': %w", payload.ThirdPartyId, err)
} }
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) wallet, err := s.walletSvc.GetCustomerWallet(ctx, userID)
if err != nil { if err != nil {
return fmt.Errorf("failed to get wallets for user %d: %w", userID, err) return fmt.Errorf("failed to get wallets for customer %d: %w", userID, err)
} }
// Optionally, credit user wallet // Optionally, credit user wallet
if transfer.Type == domain.DEPOSIT { if transfer.Type == domain.DEPOSIT {
if _, err := s.walletSvc.AddToWallet( if _, err := s.walletSvc.AddToWallet(
ctx, ctx,
wallets[0].ID, wallet.RegularID,
domain.Currency(amount), domain.Currency(amount),
domain.ValidInt64{}, domain.ValidInt64{},
domain.TRANSFER_SANTIMPAY, domain.TRANSFER_SANTIMPAY,

View File

@ -102,13 +102,13 @@ func (s *AleaPlayService) HandleCallback(ctx context.Context, callback *domain.A
} }
// Update session status using the proper repository method // Update session status using the proper repository method
if callback.Type == "SESSION_END" { // if callback.Type == "SESSION_END" {
if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil { // if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil {
s.logger.Error("failed to update session status", // s.logger.Error("failed to update session status",
"sessionID", session.ID, // "sessionID", session.ID,
"error", err) // "error", err)
} // }
} // }
return nil return nil
} }

View File

@ -52,7 +52,6 @@ func (c *Client) generateHash(body []byte, timestamp string) string {
func (c *Client) post(ctx context.Context, path string, body map[string]any, result any) error { func (c *Client) post(ctx context.Context, path string, body map[string]any, result any) error {
// Add timestamp first // Add timestamp first
timestamp := nowTimestamp() timestamp := nowTimestamp()
body["timestamp"] = timestamp
// Marshal without hash first // Marshal without hash first
tmp, _ := json.Marshal(body) tmp, _ := json.Marshal(body)
@ -61,12 +60,14 @@ func (c *Client) post(ctx context.Context, path string, body map[string]any, res
hash := c.generateHash(tmp, timestamp) hash := c.generateHash(tmp, timestamp)
body["hash"] = hash body["hash"] = hash
body["timestamp"] = timestamp
fmt.Printf("atlasPost: %v \n", body) fmt.Printf("atlasPost: %v \n", body)
// Marshal final body // Marshal final body
data, _ := json.Marshal(body) data, _ := json.Marshal(body)
req, _ := http.NewRequestWithContext(ctx, "POST", c.BaseURL+path, bytes.NewReader(data)) req, _ := http.NewRequestWithContext(ctx, "POST", c.BaseURL+path, bytes.NewReader(data))
req.Header.Set("Content-Type", "text/javascript") req.Header.Set("Content-Type", "application/json")
// Debug // Debug
fmt.Println("Request URL:", c.BaseURL+path) fmt.Println("Request URL:", c.BaseURL+path)

View File

@ -117,7 +117,7 @@ func (s *Service) ProcessBet(ctx context.Context, req domain.AtlasBetRequest) (*
} }
// 6. Deduct amount from wallet (record transaction) // 6. Deduct amount from wallet (record transaction)
_, err = s.walletSvc.DeductFromWallet(ctx, wallet.ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), "") err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)-req.Amount))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to debit wallet: %w", err) return nil, fmt.Errorf("failed to debit wallet: %w", err)
} }
@ -158,13 +158,13 @@ func (s *Service) ProcessBetWin(ctx context.Context, req domain.AtlasBetWinReque
} }
// 6. Deduct amount from wallet (record transaction) // 6. Deduct amount from wallet (record transaction)
_, err = s.walletSvc.DeductFromWallet(ctx, wallet.ID, domain.Currency(req.BetAmount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), "") err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)-req.BetAmount))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to debit wallet: %w", err) return nil, fmt.Errorf("failed to debit wallet: %w", err)
} }
if req.WinAmount > 0 { if req.WinAmount > 0 {
_, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(req.WinAmount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+req.WinAmount))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err) return nil, fmt.Errorf("failed to credit wallet: %w", err)
} }
@ -173,7 +173,7 @@ func (s *Service) ProcessBetWin(ctx context.Context, req domain.AtlasBetWinReque
// 8. Build response // 8. Build response
res := &domain.AtlasBetWinResponse{ res := &domain.AtlasBetWinResponse{
PlayerID: req.PlayerID, PlayerID: req.PlayerID,
Balance: float64(wallet.RegularBalance) - req.BetAmount + req.WinAmount, Balance: float64(wallet.RegularBalance),
} }
return res, nil return res, nil
@ -197,7 +197,7 @@ func (s *Service) ProcessRoundResult(ctx context.Context, req domain.RoundResult
return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err)
} }
_, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+req.Amount))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err) return nil, fmt.Errorf("failed to credit wallet: %w", err)
} }
@ -229,7 +229,7 @@ func (s *Service) ProcessRollBack(ctx context.Context, req domain.RollbackReques
return nil, fmt.Errorf("failed to fetch transfer for reference %s: %w", req.BetTransactionID, err) return nil, fmt.Errorf("failed to fetch transfer for reference %s: %w", req.BetTransactionID, err)
} }
_, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(transfer.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+float64(transfer.Amount)))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err) return nil, fmt.Errorf("failed to credit wallet: %w", err)
} }
@ -284,7 +284,7 @@ func (s *Service) ProcessFreeSpinResult(ctx context.Context, req domain.FreeSpin
return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err)
} }
_, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+req.Amount))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err) return nil, fmt.Errorf("failed to credit wallet: %w", err)
} }
@ -313,7 +313,7 @@ func (s *Service) ProcessJackPot(ctx context.Context, req domain.JackpotRequest)
return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err)
} }
_, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err) return nil, fmt.Errorf("failed to credit wallet: %w", err)
} }

View File

@ -1,81 +0,0 @@
package virtualgameservice
import (
"context"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
// Remove a provider by provider_id
func (s *service) RemoveProvider(ctx context.Context, providerID string) error {
return s.repo.DeleteVirtualGameProvider(ctx, providerID)
}
// Fetch provider by provider_id
func (s *service) GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error) {
return s.repo.GetVirtualGameProviderByID(ctx, providerID)
}
// List providers with pagination
func (s *service) ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error) {
providers, err := s.repo.ListVirtualGameProviders(ctx, limit, offset)
if err != nil {
return nil, 0, err
}
total, err := s.repo.CountVirtualGameProviders(ctx)
if err != nil {
return nil, 0, err
}
// Convert []dbgen.VirtualGameProvider to []domain.VirtualGameProvider
domainProviders := make([]domain.VirtualGameProvider, len(providers))
for i, p := range providers {
var logoDark *string
if p.LogoDark.Valid {
logoDark = &p.LogoDark.String
}
var logoLight *string
if p.LogoLight.Valid {
logoLight = &p.LogoLight.String
}
domainProviders[i] = domain.VirtualGameProvider{
ProviderID: p.ProviderID,
ProviderName: p.ProviderName,
Enabled: p.Enabled,
LogoDark: logoDark,
LogoLight: logoLight,
CreatedAt: p.CreatedAt.Time,
UpdatedAt: &p.UpdatedAt.Time,
// Add other fields as needed
}
}
return domainProviders, total, nil
}
// Enable/Disable a provider
func (s *service) SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error) {
provider, err := s.repo.UpdateVirtualGameProviderEnabled(ctx, providerID, enabled)
if err != nil {
s.logger.Error("Failed to update provider enabled status", "provider_id", providerID, "enabled", enabled, "error", err)
return nil, err
}
now := time.Now()
provider.UpdatedAt.Time = now
domainProvider := &domain.VirtualGameProvider{
ProviderID: provider.ProviderID,
ProviderName: provider.ProviderName,
Enabled: provider.Enabled,
CreatedAt: provider.CreatedAt.Time,
UpdatedAt: &provider.UpdatedAt.Time,
// Add other fields as needed
}
return domainProvider, nil
}

View File

@ -0,0 +1,14 @@
package orchestration
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type OrchestrationService interface {
FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error)
GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error)
AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error)
}

View File

@ -0,0 +1,526 @@
package orchestration
import (
"context"
"fmt"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
"github.com/jackc/pgx/v5/pgtype"
)
type Service struct {
virtualGameSvc virtualgameservice.VirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService
repo repository.VirtualGameRepository
cfg *config.Config
client *veli.Client
}
func New(virtualGameSvc virtualgameservice.VirtualGameService, repo repository.VirtualGameRepository, cfg *config.Config, client *veli.Client) *Service {
return &Service{
virtualGameSvc: virtualGameSvc,
repo: repo,
cfg: cfg,
client: client,
}
}
func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) {
// logger := s.mongoLogger.With(zap.String("service", "AddProviders"), zap.Any("ProviderRequest", req))
// 0. Remove all existing providers first
if err := s.repo.DeleteAllVirtualGameProviders(ctx); err != nil {
// logger.Error("failed to delete all virtual game providers", zap.Error(err))
return nil, fmt.Errorf("failed to clear existing providers: %w", err)
}
// 1. Prepare signature parameters
sigParams := map[string]any{
"brandId": req.BrandID,
}
// Optional fields
sigParams["extraData"] = fmt.Sprintf("%t", req.ExtraData) // false is still included
if req.Size > 0 {
sigParams["size"] = fmt.Sprintf("%d", req.Size)
} else {
sigParams["size"] = ""
}
if req.Page > 0 {
sigParams["page"] = fmt.Sprintf("%d", req.Page)
} else {
sigParams["page"] = ""
}
// 2. Call external API
var res domain.ProviderResponse
if err := s.client.Post(ctx, "/game-lists/public/providers", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to fetch providers: %w", err)
}
// 3. Loop through fetched providers and insert into DB
for _, p := range res.Items {
createParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: p.ProviderID,
ProviderName: p.ProviderName,
LogoDark: pgtype.Text{String: p.LogoForDark, Valid: p.LogoForDark != ""},
LogoLight: pgtype.Text{String: p.LogoForLight, Valid: p.LogoForLight != ""},
Enabled: true,
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, createParams); err != nil {
// logger.Error("failed to add provider", zap.Error(err))
return nil, fmt.Errorf("failed to add provider %s: %w", p.ProviderID, err)
}
}
// 4. Always add "popok" provider manually
popokParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: "popok",
ProviderName: "Popok Gaming",
LogoDark: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-dark.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed
LogoLight: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-light.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed
Enabled: true,
}
atlasParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: "atlas",
ProviderName: "Atlas Gaming",
LogoDark: pgtype.Text{String: "/static/logos/atlas-dark.png", Valid: true}, // adjust as needed
LogoLight: pgtype.Text{String: "/static/logos/atlas-light.png", Valid: true}, // adjust as needed
Enabled: true,
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, popokParams); err != nil {
// logger.Error("failed to add popok provider", zap.Any("popokParams", popokParams), zap.Error(err))
return nil, fmt.Errorf("failed to add popok provider: %w", err)
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, atlasParams); err != nil {
return nil, fmt.Errorf("failed to add atlas provider: %w", err)
}
// Optionally also append it to the response for consistency
// res.Items = append(res.Items, domain.VirtualGameProvider{
// ProviderID: uuid.New().String(),
// ProviderName: "Popok Gaming",
// LogoForDark: "/static/logos/popok-dark.png",
// LogoForLight: "/static/logos/popok-light.png",
// })
return &res, nil
}
func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) {
// Build params for repo call
// logger := s.mongoLogger.With(zap.String("service", "GetAllVirtualGames"), zap.Any("params", params))
rows, err := s.repo.ListAllVirtualGames(ctx, params)
if err != nil {
// logger.Error("[GetAllVirtualGames] Failed to fetch virtual games", zap.Error(err))
return nil, fmt.Errorf("failed to fetch virtual games: %w", err)
}
var allGames []domain.UnifiedGame
for _, r := range rows {
// --- Convert nullable Rtp to *float64 ---
var rtpPtr *float64
if r.Rtp.Valid {
rtpFloat, err := r.Rtp.Float64Value()
if err == nil {
rtpPtr = new(float64)
*rtpPtr = rtpFloat.Float64
}
}
var betsFloat64 []float64
for _, bet := range r.Bets {
if bet.Valid {
betFloat, err := bet.Float64Value()
if err == nil {
betsFloat64 = append(betsFloat64, betFloat.Float64)
}
}
}
allGames = append(allGames, domain.UnifiedGame{
GameID: r.GameID,
ProviderID: r.ProviderID,
Provider: r.ProviderName,
Name: r.Name,
Category: r.Category.String,
DeviceType: r.DeviceType.String,
Volatility: r.Volatility.String,
RTP: rtpPtr,
HasDemo: r.HasDemo.Bool,
HasFreeBets: r.HasFreeBets.Bool,
Bets: betsFloat64,
Thumbnail: r.Thumbnail.String,
Status: int(r.Status.Int32), // nullable status
})
}
return allGames, nil
}
func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) {
// logger := s.mongoLogger.With(
// zap.String("service", "FetchAndStoreAllVirtualGames"),
// zap.Any("ProviderRequest", req),
// )
// This is necessary since the provider is a foreign key
_, err := s.AddProviders(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to add providers to database: %w", err)
}
var allGames []domain.UnifiedGame
// --- 1. Existing providers (Veli Games) ---
providersRes, err := s.veliVirtualGameSvc.GetProviders(ctx, req)
if err != nil {
// logger.Error("Failed to fetch provider", zap.Error(err))
return nil, fmt.Errorf("failed to fetch providers: %w", err)
}
// --- 2. Fetch games for each provider (Veli Games) ---
for _, p := range providersRes.Items {
games, err := s.veliVirtualGameSvc.GetGames(ctx, domain.GameListRequest{
BrandID: s.cfg.VeliGames.BrandID,
ProviderID: p.ProviderID,
Page: req.Page,
Size: req.Size,
})
if err != nil {
// logger.Error("failed to get veli games", zap.String("ProviderID", p.ProviderID), zap.Error(err))
continue // skip failing provider but continue others
}
for _, g := range games {
unified := domain.UnifiedGame{
GameID: g.GameID,
ProviderID: g.ProviderID,
Provider: p.ProviderName,
Name: g.Name,
Category: g.Category,
DeviceType: g.DeviceType,
HasDemo: g.HasDemoMode,
HasFreeBets: g.HasFreeBets,
}
allGames = append(allGames, unified)
// Save to DB
_, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: g.GameID,
ProviderID: g.ProviderID,
Name: g.Name,
Category: pgtype.Text{
String: g.Category,
Valid: g.Category != "",
},
DeviceType: pgtype.Text{
String: g.DeviceType,
Valid: g.DeviceType != "",
},
HasDemo: pgtype.Bool{
Bool: g.HasDemoMode,
Valid: true,
},
HasFreeBets: pgtype.Bool{
Bool: g.HasFreeBets,
Valid: true,
},
})
if err != nil {
// logger.Error("failed to create virtual game", zap.Error(err))
}
}
}
// --- 3. Fetch Atlas-V games ---
atlasGames, err := s.veliVirtualGameSvc.GetAtlasVGames(ctx)
if err != nil {
// logger.Error("failed to fetch Atlas-V games", zap.Error(err))
} else {
for _, g := range atlasGames {
unified := domain.UnifiedGame{
GameID: g.GameID,
ProviderID: "atlasv",
Provider: "Atlas-V Gaming", // "Atlas-V"
Name: g.Name,
Category: g.Category, // using Type as Category
Thumbnail: g.Thumbnail,
HasDemo: true,
DemoURL: g.DemoURL,
}
allGames = append(allGames, unified)
// Save to DB
_, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: g.GameID,
ProviderID: "atlasv",
Name: g.Name,
Category: pgtype.Text{
String: g.Category,
Valid: g.Category != "",
},
Thumbnail: pgtype.Text{
String: g.Thumbnail,
Valid: g.Thumbnail != "",
},
HasDemo: pgtype.Bool{
Bool: g.HasDemoMode,
Valid: true,
},
})
if err != nil {
// logger.Error("failed to create Atlas-V virtual game", zap.Error(err))
}
}
}
// --- 4. Handle PopOK separately ---
popokGames, err := s.virtualGameSvc.ListGames(ctx, currency)
if err != nil {
// logger.Error("failed to fetch PopOk games", zap.Error(err))
return nil, fmt.Errorf("failed to fetch PopOK games: %w", err)
}
for _, g := range popokGames {
unified := domain.UnifiedGame{
GameID: fmt.Sprintf("%d", g.ID),
ProviderID: "popok",
Provider: "PopOK",
Name: g.GameName,
Category: "Crash",
Bets: g.Bets,
Thumbnail: g.Thumbnail,
Status: g.Status,
}
allGames = append(allGames, unified)
// Convert []float64 to []pgtype.Numeric
var betsNumeric []pgtype.Numeric
for _, bet := range g.Bets {
var num pgtype.Numeric
_ = num.Scan(bet)
betsNumeric = append(betsNumeric, num)
}
// Save to DB
_, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: fmt.Sprintf("%d", g.ID),
ProviderID: "popok",
Name: g.GameName,
Bets: betsNumeric,
Thumbnail: pgtype.Text{
String: g.Thumbnail,
Valid: g.Thumbnail != "",
},
Status: pgtype.Int4{
Int32: int32(g.Status),
Valid: true,
},
HasDemo: pgtype.Bool{
Bool: true,
Valid: true,
},
Category: pgtype.Text{
String: "Crash",
Valid: true,
},
})
if err != nil {
// logger.Error("failed to create PopOK virtual game", zap.Error(err))
}
}
return allGames, nil
}
func (s *Service) RemoveProvider(ctx context.Context, providerID string) error {
return s.repo.DeleteVirtualGameProvider(ctx, providerID)
}
// Fetch provider by provider_id
func (s *Service) GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error) {
return s.repo.GetVirtualGameProviderByID(ctx, providerID)
}
// List providers with pagination
func (s *Service) ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error) {
providers, err := s.repo.ListVirtualGameProviders(ctx, limit, offset)
if err != nil {
return nil, 0, err
}
total, err := s.repo.CountVirtualGameProviders(ctx)
if err != nil {
return nil, 0, err
}
// Convert []dbgen.VirtualGameProvider to []domain.VirtualGameProvider
domainProviders := make([]domain.VirtualGameProvider, len(providers))
for i, p := range providers {
var logoDark *string
if p.LogoDark.Valid {
logoDark = &p.LogoDark.String
}
var logoLight *string
if p.LogoLight.Valid {
logoLight = &p.LogoLight.String
}
domainProviders[i] = domain.VirtualGameProvider{
ProviderID: p.ProviderID,
ProviderName: p.ProviderName,
Enabled: p.Enabled,
LogoDark: logoDark,
LogoLight: logoLight,
CreatedAt: p.CreatedAt.Time,
UpdatedAt: &p.UpdatedAt.Time,
// Add other fields as needed
}
}
return domainProviders, total, nil
}
// Enable/Disable a provider
func (s *Service) SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error) {
provider, err := s.repo.UpdateVirtualGameProviderEnabled(ctx, providerID, enabled)
if err != nil {
//s.logger.Error("Failed to update provider enabled status", "provider_id", providerID, "enabled", enabled, "error", err)
return nil, err
}
now := time.Now()
provider.UpdatedAt.Time = now
domainProvider := &domain.VirtualGameProvider{
ProviderID: provider.ProviderID,
ProviderName: provider.ProviderName,
Enabled: provider.Enabled,
CreatedAt: provider.CreatedAt.Time,
UpdatedAt: &provider.UpdatedAt.Time,
// Add other fields as needed
}
return domainProvider, nil
}
func (s *Service) CreateVirtualGameProviderReport(ctx context.Context, report domain.CreateVirtualGameProviderReport) (domain.VirtualGameProviderReport, error) {
// Example: logger := s.mongoLogger.With(zap.String("service", "CreateVirtualGameProviderReport"), zap.Any("Report", report))
// Call repository to create the report
created, err := s.repo.CreateVirtualGameProviderReport(ctx, report)
if err != nil {
// logger.Error("failed to create provider report", zap.Error(err), zap.Any("report", report))
return domain.VirtualGameProviderReport{}, fmt.Errorf("failed to create provider report for provider %s: %w", report.ProviderID, err)
}
// Return the created report
return created, nil
}
func (s *Service) CreateVirtualGameReport(ctx context.Context, report domain.CreateVirtualGameReport) (domain.VirtualGameReport, error) {
// Example: logger := s.mongoLogger.With(zap.String("service", "CreateVirtualGameReport"), zap.Any("Report", report))
// Call repository to create the report
created, err := s.repo.CreateVirtualGameReport(ctx, report)
if err != nil {
// logger.Error("failed to create game report", zap.Error(err), zap.Any("report", report))
return domain.VirtualGameReport{}, fmt.Errorf("failed to create game report for game %s: %w", report.GameID, err)
}
// Return the created report
return created, nil
}
func (s *Service) GetVirtualGameProviderReportByProviderAndDate(
ctx context.Context,
providerID string,
reportDate time.Time,
reportType string,
) (domain.VirtualGameProviderReport, error) {
// Example logger if needed
// logger := s.mongoLogger.With(zap.String("service", "GetVirtualGameProviderReportByProviderAndDate"),
// zap.String("provider_id", providerID),
// zap.Time("report_date", reportDate),
// zap.String("report_type", reportType),
// )
report, err := s.repo.GetVirtualGameProviderReportByProviderAndDate(ctx, providerID, reportDate, reportType)
if err != nil {
// logger.Error("failed to retrieve virtual game provider report", zap.Error(err))
return domain.VirtualGameProviderReport{}, fmt.Errorf(
"failed to retrieve provider report for provider %s on %s (%s): %w",
providerID, reportDate.Format("2006-01-02"), reportType, err,
)
}
return report, nil
}
func (s *Service) UpdateVirtualGameProviderReportByDate(
ctx context.Context,
providerID string,
reportDate time.Time,
reportType string,
totalGamesPlayed int64,
totalBets float64,
totalPayouts float64,
totalPlayers int64,
) error {
// Optionally log or trace the update
// Example: s.mongoLogger.Info("Updating virtual game provider report",
// zap.String("provider_id", providerID),
// zap.Time("report_date", reportDate),
// zap.String("report_type", reportType),
// )
err := s.repo.UpdateVirtualGameProviderReportByDate(
ctx,
providerID,
reportDate,
reportType,
totalGamesPlayed,
totalBets,
totalPayouts,
totalPlayers,
)
if err != nil {
return fmt.Errorf("failed to update provider report for provider %s on %s (%s): %w",
providerID,
reportDate.Format("2006-01-02"),
reportType,
err,
)
}
return nil
}
func (s *Service) ListVirtualGameProviderReportsByGamesPlayedAsc(ctx context.Context) ([]domain.VirtualGameProviderReport, error) {
reports, err := s.repo.ListVirtualGameProviderReportsByGamesPlayedAsc(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch virtual game provider reports ascending: %w", err)
}
return reports, nil
}
// ListVirtualGameProviderReportsByGamesPlayedDesc fetches all reports sorted by total_games_played descending.
func (s *Service) ListVirtualGameProviderReportsByGamesPlayedDesc(ctx context.Context) ([]domain.VirtualGameProviderReport, error) {
reports, err := s.repo.ListVirtualGameProviderReportsByGamesPlayedDesc(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch virtual game provider reports descending: %w", err)
}
return reports, nil
}

View File

@ -3,16 +3,15 @@ package virtualgameservice
import ( import (
"context" "context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
type VirtualGameService interface { type VirtualGameService interface {
// AddProvider(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) // AddProvider(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error)
RemoveProvider(ctx context.Context, providerID string) error // RemoveProvider(ctx context.Context, providerID string) error
GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error) // GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error)
ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error) // ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error)
SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error) // SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error)
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error

View File

@ -224,16 +224,16 @@ func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfo
return nil, fmt.Errorf("invalid token") return nil, fmt.Errorf("invalid token")
} }
wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID)
if err != nil || len(wallets) == 0 { if err != nil {
s.logger.Error("No wallets found for user", "userID", claims.UserID) s.logger.Error("No wallets found for user", "userID", claims.UserID)
return nil, fmt.Errorf("no wallet found") return nil, err
} }
return &domain.PopOKPlayerInfoResponse{ return &domain.PopOKPlayerInfoResponse{
Country: "ET", Country: "ET",
Currency: claims.Currency, Currency: claims.Currency,
Balance: float64(wallets[0].Balance), // Convert cents to currency Balance: float64(wallet.RegularBalance), // Convert cents to currency
PlayerID: fmt.Sprintf("%d", claims.UserID), PlayerID: fmt.Sprintf("%d", claims.UserID),
}, nil }, nil
} }
@ -246,17 +246,17 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
} }
// Convert amount to cents (assuming wallet uses cents) // Convert amount to cents (assuming wallet uses cents)
amountCents := int64(req.Amount) // amount := int64(req.Amount)
// Deduct from wallet // Deduct from wallet
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID)
if err != nil { if err != nil {
return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets") return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets")
} }
_, err = s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents), _, err = s.walletSvc.DeductFromWallet(ctx, wallet.RegularID, domain.Currency(req.Amount),
domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
fmt.Sprintf("Deducted %v amount from wallet by system while placing virtual game bet", amountCents)) fmt.Sprintf("Deducted %v amount from wallet by system while placing virtual game bet", req.Amount))
if err != nil { if err != nil {
return nil, fmt.Errorf("insufficient balance") return nil, fmt.Errorf("insufficient balance")
} }
@ -268,7 +268,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
Provider: string(domain.PROVIDER_POPOK), Provider: string(domain.PROVIDER_POPOK),
GameID: req.GameID, GameID: req.GameID,
TransactionType: "BET", TransactionType: "BET",
Amount: amountCents, // Negative for bets Amount: int64(req.Amount), // Negative for bets
Currency: req.Currency, Currency: req.Currency,
ExternalTransactionID: req.TransactionID, ExternalTransactionID: req.TransactionID,
Status: "COMPLETED", Status: "COMPLETED",
@ -283,7 +283,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
return &domain.PopOKBetResponse{ return &domain.PopOKBetResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID), // Your internal transaction ID ExternalTrxID: fmt.Sprintf("%v", tx.ID), // Your internal transaction ID
Balance: float64(userWallets[0].Balance), Balance: float64(wallet.RegularBalance),
}, nil }, nil
} }
@ -319,10 +319,15 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
} }
// 3. Convert amount to cents // 3. Convert amount to cents
amountCents := int64(req.Amount) // amountCents := int64(req.Amount)
wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID)
if err != nil {
return nil, fmt.Errorf("Failed to read user wallets")
}
// 4. Credit to wallet // 4. Credit to wallet
_, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{},
domain.TRANSFER_DIRECT, domain.PaymentDetails{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
fmt.Sprintf("Added %v to wallet for winning PopOkBet", req.Amount), fmt.Sprintf("Added %v to wallet for winning PopOkBet", req.Amount),
) )
@ -331,10 +336,10 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
return nil, fmt.Errorf("wallet credit failed") return nil, fmt.Errorf("wallet credit failed")
} }
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) // userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
if err != nil { // if err != nil {
return &domain.PopOKWinResponse{}, fmt.Errorf("Failed to read user wallets") // return &domain.PopOKWinResponse{}, fmt.Errorf("Failed to read user wallets")
} // }
// 5. Create transaction record // 5. Create transaction record
tx := &domain.VirtualGameTransaction{ tx := &domain.VirtualGameTransaction{
UserID: claims.UserID, UserID: claims.UserID,
@ -342,7 +347,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
Provider: string(domain.PROVIDER_POPOK), Provider: string(domain.PROVIDER_POPOK),
GameID: req.GameID, GameID: req.GameID,
TransactionType: "WIN", TransactionType: "WIN",
Amount: amountCents, Amount: int64(req.Amount),
Currency: req.Currency, Currency: req.Currency,
ExternalTransactionID: req.TransactionID, ExternalTransactionID: req.TransactionID,
Status: "COMPLETED", Status: "COMPLETED",
@ -354,12 +359,12 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
return nil, fmt.Errorf("transaction recording failed") return nil, fmt.Errorf("transaction recording failed")
} }
fmt.Printf("\n\n Win balance is:%v\n\n", float64(userWallets[0].Balance)) fmt.Printf("\n\n Win balance is:%v\n\n", float64(wallet.RegularBalance))
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID), ExternalTrxID: fmt.Sprintf("%v", tx.ID),
Balance: float64(userWallets[0].Balance), Balance: float64(wallet.RegularBalance),
}, nil }, nil
} }
@ -371,6 +376,11 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin
return nil, fmt.Errorf("invalid token") return nil, fmt.Errorf("invalid token")
} }
wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID)
if err != nil {
return nil, fmt.Errorf("Failed to read user wallets")
}
// 2. Check for duplicate tournament win transaction // 2. Check for duplicate tournament win transaction
existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil { if err != nil {
@ -379,15 +389,15 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin
} }
if existingTx != nil && existingTx.TransactionType == "TOURNAMENT_WIN" { if existingTx != nil && existingTx.TransactionType == "TOURNAMENT_WIN" {
s.logger.Warn("Duplicate tournament win", "transactionID", req.TransactionID) s.logger.Warn("Duplicate tournament win", "transactionID", req.TransactionID)
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) // wallet, _ := s.walletSvc.GetCustomerWallet(ctx, claims.UserID)
balance := 0.0 // balance := 0.0
if len(wallets) > 0 { // if len(wallets) > 0 {
balance = float64(wallets[0].Balance) // balance = float64(wallets[0].Balance)
} // }
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", existingTx.ID), ExternalTrxID: fmt.Sprintf("%v", existingTx.ID),
Balance: balance, Balance: float64(wallet.RegularBalance),
}, nil }, nil
} }
@ -395,7 +405,7 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin
amountCents := int64(req.Amount) amountCents := int64(req.Amount)
// 4. Credit user wallet // 4. Credit user wallet
_, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
fmt.Sprintf("Added %v to wallet for winning Popok Tournament", req.Amount)) fmt.Sprintf("Added %v to wallet for winning Popok Tournament", req.Amount))
if err != nil { if err != nil {
s.logger.Error("Failed to credit wallet for tournament", "userID", claims.UserID, "error", err) s.logger.Error("Failed to credit wallet for tournament", "userID", claims.UserID, "error", err)
@ -419,15 +429,15 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin
} }
// 6. Fetch updated balance // 6. Fetch updated balance
wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) // wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
if err != nil { // if err != nil {
return nil, fmt.Errorf("Failed to get wallet balance") // return nil, fmt.Errorf("Failed to get wallet balance")
} // }
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID), ExternalTrxID: fmt.Sprintf("%v", tx.ID),
Balance: float64(wallets[0].Balance), Balance: float64(wallet.RegularBalance),
}, nil }, nil
} }
@ -438,6 +448,11 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque
return nil, fmt.Errorf("invalid token") return nil, fmt.Errorf("invalid token")
} }
wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID)
if err != nil {
return nil, fmt.Errorf("Failed to read user wallets")
}
existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil { if err != nil {
s.logger.Error("Failed to check existing promo transaction", "error", err) s.logger.Error("Failed to check existing promo transaction", "error", err)
@ -445,20 +460,20 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque
} }
if existingTx != nil && existingTx.TransactionType == "PROMO_WIN" { if existingTx != nil && existingTx.TransactionType == "PROMO_WIN" {
s.logger.Warn("Duplicate promo win", "transactionID", req.TransactionID) s.logger.Warn("Duplicate promo win", "transactionID", req.TransactionID)
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) // wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
balance := 0.0 // balance := 0.0
if len(wallets) > 0 { // if len(wallets) > 0 {
balance = float64(wallets[0].Balance) // balance = float64(wallets[0].Balance)
} // }
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", existingTx.ID), ExternalTrxID: fmt.Sprintf("%v", existingTx.ID),
Balance: balance, Balance: float64(wallet.RegularBalance),
}, nil }, nil
} }
amountCents := int64(req.Amount * 100) // amountCents := int64(req.Amount * 100)
_, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{},
domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet for winning PopOk Promo Win", req.Amount)) domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet for winning PopOk Promo Win", req.Amount))
if err != nil { if err != nil {
s.logger.Error("Failed to credit wallet for promo", "userID", claims.UserID, "error", err) s.logger.Error("Failed to credit wallet for promo", "userID", claims.UserID, "error", err)
@ -468,7 +483,7 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque
tx := &domain.VirtualGameTransaction{ tx := &domain.VirtualGameTransaction{
UserID: claims.UserID, UserID: claims.UserID,
TransactionType: "PROMO_WIN", TransactionType: "PROMO_WIN",
Amount: amountCents, Amount: int64(wallet.RegularBalance),
Currency: req.Currency, Currency: req.Currency,
ExternalTransactionID: req.TransactionID, ExternalTransactionID: req.TransactionID,
Status: "COMPLETED", Status: "COMPLETED",
@ -480,15 +495,15 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque
return nil, fmt.Errorf("transaction recording failed") return nil, fmt.Errorf("transaction recording failed")
} }
wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) // wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
if err != nil { // if err != nil {
return nil, fmt.Errorf("failed to read wallets") // return nil, fmt.Errorf("failed to read wallets")
} // }
return &domain.PopOKWinResponse{ return &domain.PopOKWinResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID), ExternalTrxID: fmt.Sprintf("%v", tx.ID),
Balance: float64(wallets[0].Balance), Balance: float64(wallet.RegularBalance),
}, nil }, nil
} }
@ -535,6 +550,11 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ
// return nil, fmt.Errorf("invalid token") // return nil, fmt.Errorf("invalid token")
// } // }
wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID)
if err != nil {
return nil, fmt.Errorf("Failed to read user wallets")
}
// 2. Find the original bet transaction // 2. Find the original bet transaction
originalBet, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) originalBet, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil { if err != nil {
@ -551,21 +571,21 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ
// 4. Check if already cancelled // 4. Check if already cancelled
if originalBet.Status == "CANCELLED" { if originalBet.Status == "CANCELLED" {
s.logger.Warn("Transaction already cancelled", "transactionID", req.TransactionID) s.logger.Warn("Transaction already cancelled", "transactionID", req.TransactionID)
wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) // wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
balance := 0.0 // balance := 0.0
if len(wallets) > 0 { // if len(wallets) > 0 {
balance = float64(wallets[0].Balance) // balance = float64(wallets[0].Balance)
} // }
return &domain.PopOKCancelResponse{ return &domain.PopOKCancelResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", originalBet.ID), ExternalTrxID: fmt.Sprintf("%v", originalBet.ID),
Balance: balance, Balance: float64(wallet.RegularBalance),
}, nil }, nil
} }
// 5. Refund the bet amount (absolute value since bet amount is negative) // 5. Refund the bet amount (absolute value since bet amount is negative)
refundAmount := -originalBet.Amount refundAmount := -originalBet.Amount
_, err = s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(refundAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
fmt.Sprintf("Added %v to wallet as refund for cancelling PopOk bet", refundAmount), fmt.Sprintf("Added %v to wallet as refund for cancelling PopOk bet", refundAmount),
) )
if err != nil { if err != nil {
@ -573,10 +593,10 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ
return nil, fmt.Errorf("refund failed") return nil, fmt.Errorf("refund failed")
} }
userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) // userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
if err != nil { // if err != nil {
return &domain.PopOKCancelResponse{}, fmt.Errorf("Failed to read user wallets") // return &domain.PopOKCancelResponse{}, fmt.Errorf("Failed to read user wallets")
} // }
// 6. Mark original bet as cancelled and create cancel record // 6. Mark original bet as cancelled and create cancel record
cancelTx := &domain.VirtualGameTransaction{ cancelTx := &domain.VirtualGameTransaction{
@ -615,7 +635,7 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ
return &domain.PopOKCancelResponse{ return &domain.PopOKCancelResponse{
TransactionID: req.TransactionID, TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", cancelTx.ID), ExternalTrxID: fmt.Sprintf("%v", cancelTx.ID),
Balance: float64(userWallets[0].Balance), Balance: float64(wallet.RegularBalance),
}, nil }, nil
} }
@ -657,10 +677,6 @@ func (s *service) verifySignature(callback *domain.PopOKCallback) bool {
return expected == callback.Signature return expected == callback.Signature
} }
// func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
// return s.repo.GetGameCounts(ctx, filter)
// }
func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) { func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) {
now := time.Now().Format("02-01-2006 15:04:05") // dd-mm-yyyy hh:mm:ss now := time.Now().Format("02-01-2006 15:04:05") // dd-mm-yyyy hh:mm:ss

View File

@ -88,7 +88,7 @@ func (c *Client) generateSignature(params map[string]any) (string, error) {
// POST helper // POST helper
func (c *Client) post(ctx context.Context, path string, body any, sigParams map[string]any, result any) error { func (c *Client) Post(ctx context.Context, path string, body any, sigParams map[string]any, result any) error {
data, _ := json.Marshal(body) data, _ := json.Marshal(body)
sig, err := c.generateSignature(sigParams) sig, err := c.generateSignature(sigParams)
if err != nil { if err != nil {

View File

@ -1,326 +0,0 @@
package veli
import (
"context"
"fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
"go.uber.org/zap"
)
func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) {
logger := s.mongoLogger.With(zap.String("service", "AddProviders"), zap.Any("ProviderRequest", req))
// 0. Remove all existing providers first
if err := s.repo.DeleteAllVirtualGameProviders(ctx); err != nil {
logger.Error("failed to delete all virtual game providers", zap.Error(err))
return nil, fmt.Errorf("failed to clear existing providers: %w", err)
}
// 1. Prepare signature parameters
sigParams := map[string]any{
"brandId": req.BrandID,
}
// Optional fields
sigParams["extraData"] = fmt.Sprintf("%t", req.ExtraData) // false is still included
if req.Size > 0 {
sigParams["size"] = fmt.Sprintf("%d", req.Size)
} else {
sigParams["size"] = ""
}
if req.Page > 0 {
sigParams["page"] = fmt.Sprintf("%d", req.Page)
} else {
sigParams["page"] = ""
}
// 2. Call external API
var res domain.ProviderResponse
if err := s.client.post(ctx, "/game-lists/public/providers", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to fetch providers: %w", err)
}
// 3. Loop through fetched providers and insert into DB
for _, p := range res.Items {
createParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: p.ProviderID,
ProviderName: p.ProviderName,
LogoDark: pgtype.Text{String: p.LogoForDark, Valid: p.LogoForDark != ""},
LogoLight: pgtype.Text{String: p.LogoForLight, Valid: p.LogoForLight != ""},
Enabled: true,
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, createParams); err != nil {
logger.Error("failed to add provider", zap.Error(err))
return nil, fmt.Errorf("failed to add provider %s: %w", p.ProviderID, err)
}
}
// 4. Always add "popok" provider manually
popokParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: "popok",
ProviderName: "Popok Gaming",
LogoDark: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-dark.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed
LogoLight: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-light.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed
Enabled: true,
}
atlasParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: "atlas",
ProviderName: "Atlas Gaming",
LogoDark: pgtype.Text{String: "/static/logos/atlas-dark.png", Valid: true}, // adjust as needed
LogoLight: pgtype.Text{String: "/static/logos/atlas-light.png", Valid: true}, // adjust as needed
Enabled: true,
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, popokParams); err != nil {
logger.Error("failed to add popok provider", zap.Any("popokParams", popokParams), zap.Error(err))
return nil, fmt.Errorf("failed to add popok provider: %w", err)
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, atlasParams); err != nil {
return nil, fmt.Errorf("failed to add atlas provider: %w", err)
}
// Optionally also append it to the response for consistency
// res.Items = append(res.Items, domain.VirtualGameProvider{
// ProviderID: uuid.New().String(),
// ProviderName: "Popok Gaming",
// LogoForDark: "/static/logos/popok-dark.png",
// LogoForLight: "/static/logos/popok-light.png",
// })
return &res, nil
}
func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) {
// Build params for repo call
logger := s.mongoLogger.With(zap.String("service", "GetAllVirtualGames"), zap.Any("params", params))
rows, err := s.repo.ListAllVirtualGames(ctx, params)
if err != nil {
logger.Error("[GetAllVirtualGames] Failed to fetch virtual games", zap.Error(err))
return nil, fmt.Errorf("failed to fetch virtual games: %w", err)
}
var allGames []domain.UnifiedGame
for _, r := range rows {
// --- Convert nullable Rtp to *float64 ---
var rtpPtr *float64
if r.Rtp.Valid {
rtpFloat, err := r.Rtp.Float64Value()
if err == nil {
rtpPtr = new(float64)
*rtpPtr = rtpFloat.Float64
}
}
var betsFloat64 []float64
for _, bet := range r.Bets {
if bet.Valid {
betFloat, err := bet.Float64Value()
if err == nil {
betsFloat64 = append(betsFloat64, betFloat.Float64)
}
}
}
allGames = append(allGames, domain.UnifiedGame{
GameID: r.GameID,
ProviderID: r.ProviderID,
Provider: r.ProviderName,
Name: r.Name,
Category: r.Category.String,
DeviceType: r.DeviceType.String,
Volatility: r.Volatility.String,
RTP: rtpPtr,
HasDemo: r.HasDemo.Bool,
HasFreeBets: r.HasFreeBets.Bool,
Bets: betsFloat64,
Thumbnail: r.Thumbnail.String,
Status: int(r.Status.Int32), // nullable status
})
}
return allGames, nil
}
func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) {
logger := s.mongoLogger.With(
zap.String("service", "FetchAndStoreAllVirtualGames"),
zap.Any("ProviderRequest", req),
)
// This is necessary since the provider is a foreign key
_, err := s.AddProviders(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to add providers to database: %w", err)
}
var allGames []domain.UnifiedGame
// --- 1. Existing providers (Veli Games) ---
providersRes, err := s.GetProviders(ctx, req)
if err != nil {
logger.Error("Failed to fetch provider", zap.Error(err))
return nil, fmt.Errorf("failed to fetch providers: %w", err)
}
// --- 2. Fetch games for each provider (Veli Games) ---
for _, p := range providersRes.Items {
games, err := s.GetGames(ctx, domain.GameListRequest{
BrandID: s.cfg.VeliGames.BrandID,
ProviderID: p.ProviderID,
Page: req.Page,
Size: req.Size,
})
if err != nil {
logger.Error("failed to get veli games", zap.String("ProviderID", p.ProviderID), zap.Error(err))
continue // skip failing provider but continue others
}
for _, g := range games {
unified := domain.UnifiedGame{
GameID: g.GameID,
ProviderID: g.ProviderID,
Provider: p.ProviderName,
Name: g.Name,
Category: g.Category,
DeviceType: g.DeviceType,
HasDemo: g.HasDemoMode,
HasFreeBets: g.HasFreeBets,
}
allGames = append(allGames, unified)
// Save to DB
_, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: g.GameID,
ProviderID: g.ProviderID,
Name: g.Name,
Category: pgtype.Text{
String: g.Category,
Valid: g.Category != "",
},
DeviceType: pgtype.Text{
String: g.DeviceType,
Valid: g.DeviceType != "",
},
HasDemo: pgtype.Bool{
Bool: g.HasDemoMode,
Valid: true,
},
HasFreeBets: pgtype.Bool{
Bool: g.HasFreeBets,
Valid: true,
},
})
if err != nil {
logger.Error("failed to create virtual game", zap.Error(err))
}
}
}
// --- 3. Fetch Atlas-V games ---
atlasGames, err := s.GetAtlasVGames(ctx)
if err != nil {
logger.Error("failed to fetch Atlas-V games", zap.Error(err))
} else {
for _, g := range atlasGames {
unified := domain.UnifiedGame{
GameID: g.GameID,
ProviderID: "atlasv",
Provider: "Atlas-V Gaming", // "Atlas-V"
Name: g.Name,
Category: g.Category, // using Type as Category
Thumbnail: g.Thumbnail,
HasDemo: true,
DemoURL: g.DemoURL,
}
allGames = append(allGames, unified)
// Save to DB
_, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: g.GameID,
ProviderID: "atlasv",
Name: g.Name,
Category: pgtype.Text{
String: g.Category,
Valid: g.Category != "",
},
Thumbnail: pgtype.Text{
String: g.Thumbnail,
Valid: g.Thumbnail != "",
},
HasDemo: pgtype.Bool{
Bool: g.HasDemoMode,
Valid: true,
},
})
if err != nil {
logger.Error("failed to create Atlas-V virtual game", zap.Error(err))
}
}
}
// --- 4. Handle PopOK separately ---
popokGames, err := s.virtualGameSvc.ListGames(ctx, currency)
if err != nil {
logger.Error("failed to fetch PopOk games", zap.Error(err))
return nil, fmt.Errorf("failed to fetch PopOK games: %w", err)
}
for _, g := range popokGames {
unified := domain.UnifiedGame{
GameID: fmt.Sprintf("%d", g.ID),
ProviderID: "popok",
Provider: "PopOK",
Name: g.GameName,
Category: "Crash",
Bets: g.Bets,
Thumbnail: g.Thumbnail,
Status: g.Status,
}
allGames = append(allGames, unified)
// Convert []float64 to []pgtype.Numeric
var betsNumeric []pgtype.Numeric
for _, bet := range g.Bets {
var num pgtype.Numeric
_ = num.Scan(bet)
betsNumeric = append(betsNumeric, num)
}
// Save to DB
_, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: fmt.Sprintf("%d", g.ID),
ProviderID: "popok",
Name: g.GameName,
Bets: betsNumeric,
Thumbnail: pgtype.Text{
String: g.Thumbnail,
Valid: g.Thumbnail != "",
},
Status: pgtype.Int4{
Int32: int32(g.Status),
Valid: true,
},
HasDemo: pgtype.Bool{
Bool: true,
Valid: true,
},
Category: pgtype.Text{
String: "Crash",
Valid: true,
},
})
if err != nil {
logger.Error("failed to create PopOK virtual game", zap.Error(err))
}
}
return allGames, nil
}

View File

@ -16,6 +16,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -112,7 +113,7 @@ func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest)
} }
var res domain.ProviderResponse var res domain.ProviderResponse
err := s.client.post(ctx, "/game-lists/public/providers", req, sigParams, &res) err := s.client.Post(ctx, "/game-lists/public/providers", req, sigParams, &res)
return &res, err return &res, err
} }
@ -140,7 +141,7 @@ func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]d
var res struct { var res struct {
Items []domain.GameEntity `json:"items"` Items []domain.GameEntity `json:"items"`
} }
if err := s.client.post(ctx, "/game-lists/public/games", req, sigParams, &res); err != nil { if err := s.client.Post(ctx, "/game-lists/public/games", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to fetch games for provider %s: %w", req.ProviderID, err) return nil, fmt.Errorf("failed to fetch games for provider %s: %w", req.ProviderID, err)
} }
@ -168,17 +169,32 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*
"playerId": req.PlayerID, "playerId": req.PlayerID,
"currency": req.Currency, "currency": req.Currency,
"deviceType": req.DeviceType, "deviceType": req.DeviceType,
"country": "US", "country": req.Country,
"ip": req.IP, "ip": req.IP,
"brandId": req.BrandID, "brandId": req.BrandID,
} }
// 3. Call external API // 3. Call external API
var res domain.GameStartResponse var res domain.GameStartResponse
if err := s.client.post(ctx, "/unified-api/public/start-game", req, sigParams, &res); err != nil { if err := s.client.Post(ctx, "/unified-api/public/start-game", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to start game with provider %s: %w", req.ProviderID, err) return nil, fmt.Errorf("failed to start game with provider %s: %w", req.ProviderID, err)
} }
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid PlayerID: %w", err)
}
session := &domain.VirtualGameSession{
UserID: playerIDInt64,
GameID: req.GameID,
SessionToken: uuid.NewString(),
}
if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil {
return nil, fmt.Errorf("failed to create virtual game session: %w", err)
}
return &res, nil return &res, nil
} }
@ -206,7 +222,7 @@ func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest)
// 3. Call external API // 3. Call external API
var res domain.GameStartResponse var res domain.GameStartResponse
if err := s.client.post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res); err != nil { if err := s.client.Post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to start demo game with provider %s: %w", req.ProviderID, err) return nil, fmt.Errorf("failed to start demo game with provider %s: %w", req.ProviderID, err)
} }
@ -219,23 +235,24 @@ func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*d
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid PlayerID: %w", err) return nil, fmt.Errorf("invalid PlayerID: %w", err)
} }
playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) // playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64)
// if err != nil {
// return nil, fmt.Errorf("failed to get real balance: %w", err)
// }
// if len(playerWallets) == 0 {
// return nil, fmt.Errorf("PLAYER_NOT_FOUND: no wallet found for player %s", req.PlayerID)
// }
wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get real balance: %w", err) return nil, fmt.Errorf("failed to read user wallets")
}
if len(playerWallets) == 0 {
return nil, fmt.Errorf("PLAYER_NOT_FOUND: no wallet found for player %s", req.PlayerID)
} }
realBalance := playerWallets[0].Balance // realBalance := playerWallets[0].Balance
// Retrieve bonus balance if applicable // Retrieve bonus balance if applicable
var bonusBalance float64 // var bonusBalance float64
if len(playerWallets) > 1 { // bonusBalance := float64(wallet.StaticBalance)
bonusBalance = float64(playerWallets[1].Balance)
} else {
bonusBalance = 0
}
// Build the response // Build the response
res := &domain.BalanceResponse{ res := &domain.BalanceResponse{
@ -244,19 +261,19 @@ func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*d
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
}{ }{
Currency: req.Currency, Currency: req.Currency,
Amount: float64(realBalance), Amount: float64(wallet.RegularBalance),
}, },
} }
if bonusBalance > 0 { // if bonusBalance > 0 {
res.Bonus = &struct { // res.Bonus = &struct {
Currency string `json:"currency"` // Currency string `json:"currency"`
Amount float64 `json:"amount"` // Amount float64 `json:"amount"`
}{ // }{
Currency: req.Currency, // Currency: req.Currency,
Amount: bonusBalance, // Amount: bonusBalance,
} // }
} // }
return res, nil return res, nil
} }
@ -281,91 +298,64 @@ func (s *Service) ProcessBet(ctx context.Context, req domain.BetRequest) (*domai
// } // }
// --- 3. Get player wallets --- // --- 3. Get player wallets ---
playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) // playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64)
// if err != nil {
// return nil, fmt.Errorf("failed to get real balance: %w", err)
// }
// if len(playerWallets) == 0 {
// return nil, fmt.Errorf("no wallets found for player %s", req.PlayerID)
// }
wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get real balance: %w", err) return nil, fmt.Errorf("failed to read user wallets")
}
if len(playerWallets) == 0 {
return nil, fmt.Errorf("no wallets found for player %s", req.PlayerID)
} }
realWallet := playerWallets[0] // realWallet := playerWallets[0]
realBalance := float64(realWallet.Balance) // realBalance := float64(realWallet.Balance)
var bonusBalance float64 // var bonusBalance float64
if len(playerWallets) > 1 { // if len(playerWallets) > 1 {
bonusBalance = float64(playerWallets[1].Balance) // bonusBalance = float64(playerWallets[1].Balance)
} // }
bonusBalance := float64(wallet.StaticBalance)
// --- 4. Check sufficient balance --- // --- 4. Check sufficient balance ---
totalBalance := realBalance + bonusBalance // totalBalance := float64(wallet.RegularBalance) + bonusBalance
if totalBalance < req.Amount.Amount { if float64(wallet.RegularBalance) < req.Amount.Amount {
return nil, fmt.Errorf("INSUFFICIENT_BALANCE") return nil, fmt.Errorf("INSUFFICIENT_BALANCE")
} }
// --- 5. Deduct funds (bonus first, then real) --- // --- 5. Deduct funds (bonus first, then real) ---
remaining := req.Amount.Amount remaining := req.Amount.Amount
var usedBonus, usedReal float64 // var usedBonus, usedReal float64
if bonusBalance > 0 { if remaining > float64(wallet.RegularBalance) {
if bonusBalance >= remaining {
// fully cover from bonus
usedBonus = remaining
bonusBalance -= remaining
remaining = 0
} else {
// partially cover from bonus
usedBonus = bonusBalance
remaining -= bonusBalance
bonusBalance = 0
}
}
if remaining > 0 {
if realBalance >= remaining {
usedReal = remaining
realBalance -= remaining
remaining = 0
} else {
// should never happen because of totalBalance check
return nil, fmt.Errorf("INSUFFICIENT_BALANCE") return nil, fmt.Errorf("INSUFFICIENT_BALANCE")
} }
}
// --- 6. Persist wallet deductions --- // --- 6. Persist wallet deductions ---
if usedBonus > 0 && len(playerWallets) > 1 {
_, err = s.walletSvc.DeductFromWallet(ctx, playerWallets[1].ID, _, err = s.walletSvc.DeductFromWallet(ctx, wallet.RegularID,
domain.Currency(usedBonus), domain.Currency(req.Amount.Amount),
domain.ValidInt64{}, domain.ValidInt64{},
domain.TRANSFER_DIRECT, domain.TRANSFER_DIRECT,
fmt.Sprintf("Deduct bonus %.2f for bet %s", usedBonus, req.TransactionID), fmt.Sprintf("Deduct amount %.2f for bet %s", req.Amount.Amount, req.TransactionID),
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("bonus deduction failed: %w", err) return nil, fmt.Errorf("bonus deduction failed: %w", err)
} }
}
if usedReal > 0 {
_, err = s.walletSvc.DeductFromWallet(ctx, realWallet.ID,
domain.Currency(usedReal),
domain.ValidInt64{},
domain.TRANSFER_DIRECT,
fmt.Sprintf("Deduct real %.2f for bet %s", usedReal, req.TransactionID),
)
if err != nil {
return nil, fmt.Errorf("real deduction failed: %w", err)
}
}
// --- 7. Build response --- // --- 7. Build response ---
res := &domain.BetResponse{ res := &domain.BetResponse{
Real: domain.BalanceDetail{ Real: domain.BalanceDetail{
Currency: "ETB", Currency: "ETB",
Amount: realBalance, Amount: float64(wallet.RegularBalance),
}, },
WalletTransactionID: req.TransactionID, WalletTransactionID: req.TransactionID,
UsedRealAmount: usedReal, UsedRealAmount: req.Amount.Amount,
UsedBonusAmount: usedBonus, UsedBonusAmount: 0,
} }
if bonusBalance > 0 { if bonusBalance > 0 {
@ -386,21 +376,19 @@ func (s *Service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domai
} }
// --- 2. Get player wallets --- // --- 2. Get player wallets ---
playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get wallets: %w", err) return nil, fmt.Errorf("failed to read user wallets")
}
if len(playerWallets) == 0 {
return nil, fmt.Errorf("PLAYER_NOT_FOUND: no wallets for player %s", req.PlayerID)
} }
realWallet := playerWallets[0] // realWallet := playerWallets[0]
realBalance := float64(realWallet.Balance) realBalance := float64(wallet.RegularBalance)
var bonusBalance float64 // var bonusBalance float64
if len(playerWallets) > 1 { // if len(playerWallets) > 1 {
bonusBalance = float64(playerWallets[1].Balance) // bonusBalance = float64(playerWallets[1].Balance)
} // }
bonusBalance := float64(wallet.StaticBalance)
// --- 3. Apply winnings (for now, everything goes to real wallet) --- // --- 3. Apply winnings (for now, everything goes to real wallet) ---
winAmount := req.Amount.Amount winAmount := req.Amount.Amount
@ -412,7 +400,7 @@ func (s *Service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domai
_, err = s.walletSvc.AddToWallet( _, err = s.walletSvc.AddToWallet(
ctx, ctx,
realWallet.ID, wallet.RegularID,
domain.Currency(winAmount), domain.Currency(winAmount),
domain.ValidInt64{}, domain.ValidInt64{},
domain.TRANSFER_DIRECT, domain.TRANSFER_DIRECT,
@ -423,18 +411,18 @@ func (s *Service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domai
return nil, fmt.Errorf("failed to credit real wallet: %w", err) return nil, fmt.Errorf("failed to credit real wallet: %w", err)
} }
// --- 4. Reload balances after credit --- // // --- 4. Reload balances after credit ---
updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) // updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64)
if err != nil { // if err != nil {
return nil, fmt.Errorf("failed to reload balances: %w", err) // return nil, fmt.Errorf("failed to reload balances: %w", err)
} // }
updatedReal := updatedWallets[0] // updatedReal := updatedWallets[0]
realBalance = float64(updatedReal.Balance) // realBalance = float64(wallet.RegularBalance)
if len(updatedWallets) > 1 { // if len(updatedWallets) > 1 {
bonusBalance = float64(updatedWallets[1].Balance) // bonusBalance = float64(updatedWallets[1].Balance)
} // }
// --- 5. Build response --- // --- 5. Build response ---
res := &domain.WinResponse{ res := &domain.WinResponse{
@ -465,21 +453,18 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
} }
// --- 2. Get player wallets --- // --- 2. Get player wallets ---
playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get wallets: %w", err) return nil, fmt.Errorf("failed to read user wallets")
}
if len(playerWallets) == 0 {
return nil, fmt.Errorf("no wallets for player %s", req.PlayerID)
} }
realWallet := playerWallets[0] // realWallet := playerWallets[0]
realBalance := float64(realWallet.Balance) realBalance := float64(wallet.RegularBalance)
var bonusBalance float64 // var bonusBalance float64
if len(playerWallets) > 1 { // if len(playerWallets) > 1 {
bonusBalance = float64(playerWallets[1].Balance) bonusBalance := float64(wallet.StaticBalance)
} // }
// --- 3. Determine refund amount based on IsAdjustment --- // --- 3. Determine refund amount based on IsAdjustment ---
var refundAmount float64 var refundAmount float64
@ -503,7 +488,7 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
_, err = s.walletSvc.AddToWallet( _, err = s.walletSvc.AddToWallet(
ctx, ctx,
realWallet.ID, wallet.RegularID,
domain.Currency(refundAmount), domain.Currency(refundAmount),
domain.ValidInt64{}, domain.ValidInt64{},
domain.TRANSFER_DIRECT, domain.TRANSFER_DIRECT,
@ -521,23 +506,23 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
} }
// --- 5. Reload balances after refund --- // --- 5. Reload balances after refund ---
updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) // updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64)
if err != nil { // if err != nil {
return nil, fmt.Errorf("failed to reload balances: %w", err) // return nil, fmt.Errorf("failed to reload balances: %w", err)
} // }
updatedReal := updatedWallets[0] // updatedReal := updatedWallets[0]
realBalance = float64(updatedReal.Balance) // realBalance = float64(wallet.RegularBalance)
if len(updatedWallets) > 1 { // if len(updatedWallets) > 1 {
bonusBalance = float64(updatedWallets[1].Balance) // bonusBalance = float64(updatedWallets[1].Balance)
} // }
// --- 6. Build response --- // --- 6. Build response ---
res := &domain.CancelResponse{ res := &domain.CancelResponse{
WalletTransactionID: req.TransactionID, WalletTransactionID: req.TransactionID,
Real: domain.BalanceDetail{ Real: domain.BalanceDetail{
Currency: "ETB", Currency: req.AdjustmentRefund.Currency,
Amount: realBalance, Amount: realBalance,
}, },
UsedRealAmount: usedReal, UsedRealAmount: usedReal,
@ -546,7 +531,7 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
if bonusBalance > 0 { if bonusBalance > 0 {
res.Bonus = &domain.BalanceDetail{ res.Bonus = &domain.BalanceDetail{
Currency: "ETB", Currency: req.AdjustmentRefund.Currency,
Amount: bonusBalance, Amount: bonusBalance,
} }
} }
@ -554,12 +539,6 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
return res, nil return res, nil
} }
// Example helper to fetch original bet
// func (s *Service) getOriginalBet(ctx context.Context, transactionID string) (*domain.BetRecord, error) {
// // TODO: implement actual lookup
// return &domain.BetRecord{Amount: 50}, nil
// }
func (s *Service) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) { func (s *Service) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) {
// --- Signature Params (flattened strings for signing) --- // --- Signature Params (flattened strings for signing) ---
sigParams := map[string]any{ sigParams := map[string]any{
@ -599,7 +578,7 @@ func (s *Service) GetGamingActivity(ctx context.Context, req domain.GamingActivi
// --- Actual API Call --- // --- Actual API Call ---
var res domain.GamingActivityResponse var res domain.GamingActivityResponse
err := s.client.post(ctx, "/report-api/public/gaming-activity", req, sigParams, &res) err := s.client.Post(ctx, "/report-api/public/gaming-activity", req, sigParams, &res)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -640,7 +619,7 @@ func (s *Service) GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) (
// --- Actual API Call --- // --- Actual API Call ---
var res domain.HugeWinsResponse var res domain.HugeWinsResponse
err := s.client.post(ctx, "/report-api/public/gaming-activity/huge-wins", req, sigParams, &res) err := s.client.Post(ctx, "/report-api/public/gaming-activity/huge-wins", req, sigParams, &res)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -663,7 +642,7 @@ func (s *Service) GetCreditBalances(ctx context.Context, brandID string) ([]doma
Credits []domain.CreditBalance `json:"credits"` Credits []domain.CreditBalance `json:"credits"`
} }
if err := s.client.post(ctx, "/report-api/public/credit/balances", body, nil, &res); err != nil { if err := s.client.Post(ctx, "/report-api/public/credit/balances", body, nil, &res); err != nil {
return nil, fmt.Errorf("failed to fetch credit balances: %w", err) return nil, fmt.Errorf("failed to fetch credit balances: %w", err)
} }

View File

@ -34,6 +34,7 @@ import (
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/orchestration"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
@ -49,7 +50,8 @@ import (
type App struct { type App struct {
enetPulseSvc *enetpulse.Service enetPulseSvc *enetpulse.Service
atlasVirtualGameService atlas.AtlasVirtualGameService atlasVirtualGameService atlas.AtlasVirtualGameService
veliVirtualGameService veli.VeliVirtualGameService veliVirtualGameService *veli.Service
orchestrationSvc *orchestration.Service
telebirrSvc *telebirr.TelebirrService telebirrSvc *telebirr.TelebirrService
arifpaySvc *arifpay.ArifpayService arifpaySvc *arifpay.ArifpayService
santimpaySvc *santimpay.SantimPayService santimpaySvc *santimpay.SantimPayService
@ -92,7 +94,8 @@ type App struct {
func NewApp( func NewApp(
enetPulseSvc *enetpulse.Service, enetPulseSvc *enetpulse.Service,
atlasVirtualGameService atlas.AtlasVirtualGameService, atlasVirtualGameService atlas.AtlasVirtualGameService,
veliVirtualGameService veli.VeliVirtualGameService, veliVirtualGameService *veli.Service,
orchestrationSvc *orchestration.Service,
telebirrSvc *telebirr.TelebirrService, telebirrSvc *telebirr.TelebirrService,
arifpaySvc *arifpay.ArifpayService, arifpaySvc *arifpay.ArifpayService,
santimpaySvc *santimpay.SantimPayService, santimpaySvc *santimpay.SantimPayService,
@ -149,6 +152,7 @@ func NewApp(
enetPulseSvc: enetPulseSvc, enetPulseSvc: enetPulseSvc,
atlasVirtualGameService: atlasVirtualGameService, atlasVirtualGameService: atlasVirtualGameService,
veliVirtualGameService: veliVirtualGameService, veliVirtualGameService: veliVirtualGameService,
orchestrationSvc: orchestrationSvc,
telebirrSvc: telebirrSvc, telebirrSvc: telebirrSvc,
arifpaySvc: arifpaySvc, arifpaySvc: arifpaySvc,
santimpaySvc: santimpaySvc, santimpaySvc: santimpaySvc,

View File

@ -20,7 +20,7 @@ import (
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/stats" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/stats"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/orchestration"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -347,21 +347,18 @@ func StartReportCrons(reportService report.ReportService, mongoLogger *zap.Logge
func SetupReportandVirtualGameCronJobs( func SetupReportandVirtualGameCronJobs(
ctx context.Context, ctx context.Context,
reportService report.ReportService, reportService report.ReportService,
virtualGameService *veli.Service, // inject your virtual game service virtualGameOrchestrationService *orchestration.Service,
outputDir string, outputDir string,
) { ) {
c := cron.New(cron.WithSeconds()) // use WithSeconds for testing c := cron.New(cron.WithSeconds()) // WithSeconds for testing, remove in prod
schedule := []struct { schedule := []struct {
spec string spec string
period string period string
}{ }{
// { // { spec: "*/60 * * * * *", period: "test" }, // every 60 seconds for testing
// spec: "*/60 * * * * *", // Every 1 minute for testing
// period: "test",
// },
{ {
spec: "0 0 0 * * *", // Daily at midnight spec: "0 0 0 * * *", // daily at midnight
period: "daily", period: "daily",
}, },
} }
@ -370,7 +367,7 @@ func SetupReportandVirtualGameCronJobs(
period := job.period period := job.period
if _, err := c.AddFunc(job.spec, func() { if _, err := c.AddFunc(job.spec, func() {
log.Printf("[%s] Running virtual game fetch & store job...", period) log.Printf("[%s] Running virtual game & provider report job...", period)
brandID := os.Getenv("VELI_BRAND_ID") brandID := os.Getenv("VELI_BRAND_ID")
if brandID == "" { if brandID == "" {
@ -378,6 +375,7 @@ func SetupReportandVirtualGameCronJobs(
return return
} }
// Step 1. Fetch and store all virtual games
req := domain.ProviderRequest{ req := domain.ProviderRequest{
BrandID: brandID, BrandID: brandID,
ExtraData: true, ExtraData: true,
@ -385,7 +383,7 @@ func SetupReportandVirtualGameCronJobs(
Page: 1, Page: 1,
} }
allGames, err := virtualGameService.FetchAndStoreAllVirtualGames(ctx, req, "ETB") allGames, err := virtualGameOrchestrationService.FetchAndStoreAllVirtualGames(ctx, req, "ETB")
if err != nil { if err != nil {
log.Printf("[%s] Error fetching/storing virtual games: %v", period, err) log.Printf("[%s] Error fetching/storing virtual games: %v", period, err)
return return
@ -393,19 +391,42 @@ func SetupReportandVirtualGameCronJobs(
log.Printf("[%s] Successfully fetched & stored %d virtual games", period, len(allGames)) log.Printf("[%s] Successfully fetched & stored %d virtual games", period, len(allGames))
// --- Generate reports only for daily runs --- // Step 2. Fetch all providers
// if period == "daily" { providers, total, err := virtualGameOrchestrationService.ListProviders(ctx, 1000, 0)
// now := time.Now() if err != nil {
// from := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()) log.Printf("[%s] Failed to list providers: %v", period, err)
// to := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()) return
} else if total == 0 {
log.Printf("[%s] No providers found, skipping report generation", period)
return
}
log.Printf("[%s] Found %d total providers", period, total)
// Step 3. Create provider-level daily report entries
reportDate := time.Now().UTC().Truncate(24 * time.Hour)
for _, p := range providers {
createReq := domain.CreateVirtualGameProviderReport{
ProviderID: p.ProviderID,
ReportDate: reportDate,
TotalGamesPlayed: 0,
TotalBets: 0,
TotalPayouts: 0,
TotalPlayers: 0,
ReportType: period, // "daily"
}
_, err := virtualGameOrchestrationService.CreateVirtualGameProviderReport(ctx, createReq)
if err != nil {
log.Printf("[%s] Failed to create report for provider %s: %v", period, p.ProviderID, err)
continue
}
log.Printf("[%s] Created daily report row for provider: %s", period, p.ProviderID)
}
log.Printf("[%s] Daily provider reports created successfully", period)
// log.Printf("Running daily report for period %s -> %s", from.Format(time.RFC3339), to.Format(time.RFC3339))
// if err := reportService.GenerateReport(ctx, from, to); err != nil {
// log.Printf("Error generating daily report: %v", err)
// } else {
// log.Printf("Successfully generated daily report")
// }
// }
}); err != nil { }); err != nil {
log.Fatalf("Failed to schedule %s cron job: %v", period, err) log.Fatalf("Failed to schedule %s cron job: %v", period, err)
} }
@ -453,60 +474,89 @@ func StartEnetPulseCron(enetPulseSvc *enetpulse.Service, mongoLogger *zap.Logger
task func() task func()
}{ }{
{ {
spec: "0 0,10,20,30,40,50 * * * *", // Every 10 minutes spec: "0 0 */2 * * *", // Every 2 hours
task: func() { task: func() {
ctx := context.Background()
// 1⃣ Sports
mongoLogger.Info("Began fetching and storing sports cron task") mongoLogger.Info("Began fetching and storing sports cron task")
if err := enetPulseSvc.FetchAndStoreSports(context.Background()); err != nil { if err := enetPulseSvc.FetchAndStoreSports(ctx); err != nil {
mongoLogger.Error("Failed to fetch and store sports", mongoLogger.Error("Failed to fetch and store sports", zap.Error(err))
zap.Error(err),
)
} else { } else {
mongoLogger.Info("Completed fetching and storing sports without errors") mongoLogger.Info("\n\n✅ Completed fetching and storing sports\n\n")
} }
// 2⃣ Tournament Templates
mongoLogger.Info("Began fetching and storing tournament templates cron task") mongoLogger.Info("Began fetching and storing tournament templates cron task")
if err := enetPulseSvc.FetchAndStoreTournamentTemplates(context.Background()); err != nil { if err := enetPulseSvc.FetchAndStoreTournamentTemplates(ctx); err != nil {
mongoLogger.Error("Failed to fetch and store tournament templates", mongoLogger.Error("Failed to fetch and store tournament templates", zap.Error(err))
zap.Error(err),
)
} else { } else {
mongoLogger.Info("Completed fetching and storing tournament templates without errors") mongoLogger.Info("\n\n✅ Completed fetching and storing tournament templates\n\n")
} }
// 3⃣ Tournaments
mongoLogger.Info("Began fetching and storing tournaments cron task") mongoLogger.Info("Began fetching and storing tournaments cron task")
if err := enetPulseSvc.FetchAndStoreTournaments(context.Background()); err != nil { if err := enetPulseSvc.FetchAndStoreTournaments(ctx); err != nil {
mongoLogger.Error("Failed to fetch and store tournaments", mongoLogger.Error("Failed to fetch and store tournaments", zap.Error(err))
zap.Error(err),
)
} else { } else {
mongoLogger.Info("Completed fetching and storing tournaments without errors") mongoLogger.Info("\n\n✅ Completed fetching and storing tournaments\n\n")
} }
mongoLogger.Info("Began fetching and storing tournament stages cron task") // 4⃣ Tournament Stages
if err := enetPulseSvc.FetchAndStoreTournamentStages(context.Background()); err != nil { // mongoLogger.Info("Began fetching and storing tournament stages cron task")
mongoLogger.Error("Failed to fetch and store tournament stages", // if err := enetPulseSvc.FetchAndStoreTournamentStages(ctx); err != nil {
zap.Error(err), // mongoLogger.Error("Failed to fetch and store tournament stages", zap.Error(err))
) // } else {
// mongoLogger.Info("✅ \n\nCompleted fetching and storing tournament stages\n\n")
// }
// // 5⃣ Fixtures
mongoLogger.Info("Began fetching and storing fixtures cron task")
today := time.Now().Format("2006-01-02")
if err := enetPulseSvc.FetchAndStoreFixtures(ctx, today); err != nil {
mongoLogger.Error("Failed to fetch and store fixtures", zap.Error(err))
} else { } else {
mongoLogger.Info("Completed fetching and storing tournament stages without errors") mongoLogger.Info("\n\n✅ Completed fetching and storing fixtures\n\n")
}
// 6⃣ Results
// mongoLogger.Info("Began fetching and storing results cron task")
// if err := enetPulseSvc.FetchAndStoreResults(ctx); err != nil {
// mongoLogger.Error("Failed to fetch and store results", zap.Error(err))
// } else {
// mongoLogger.Info("\n\n✅ Completed fetching and storing results\n\n")
// }
// 7 Outcome Types
mongoLogger.Info("Began fetching and storing outcome_types cron task")
if err := enetPulseSvc.FetchAndStoreOutcomeTypes(ctx); err != nil {
mongoLogger.Error("Failed to fetch and store outcome_types", zap.Error(err))
} else {
mongoLogger.Info("\n\n✅ Completed fetching and storing outcome_types\n\n")
}
// 8 Preodds
mongoLogger.Info("Began fetching and storing preodds cron task")
if err := enetPulseSvc.FetchAndStorePreodds(ctx); err != nil {
mongoLogger.Error("Failed to fetch and store preodds", zap.Error(err))
} else {
mongoLogger.Info("\n\n✅ Completed fetching and storing preodds\n\n")
} }
}, },
}, },
} }
for _, job := range schedule { for _, job := range schedule {
// Run the task immediately at startup // Run immediately at startup
job.task() job.task()
// Schedule the task // Schedule the task
if _, err := c.AddFunc(job.spec, job.task); err != nil { if _, err := c.AddFunc(job.spec, job.task); err != nil {
mongoLogger.Error("Failed to schedule EnetPulse cron job", mongoLogger.Error("Failed to schedule EnetPulse cron job", zap.Error(err))
zap.Error(err),
)
} }
} }
c.Start() c.Start()
log.Println("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, and tournament stages") mongoLogger.Info("EnetPulse cron jobs started for sports, tournament templates, tournaments, tournament stages, fixtures, and results")
} }

View File

@ -18,6 +18,15 @@ import (
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/arifpay/checkout [post] // @Router /api/v1/arifpay/checkout [post]
func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error { func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error {
userId, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Error: "missing user id",
Message: "Unauthorized",
})
}
var req domain.CheckoutSessionClientRequest var req domain.CheckoutSessionClientRequest
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
@ -26,7 +35,7 @@ func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error {
}) })
} }
data, err := h.arifpaySvc.CreateCheckoutSession(req, true) data, err := h.arifpaySvc.CreateCheckoutSession(req, true, userId)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(), Error: err.Error(),
@ -53,7 +62,7 @@ func (h *Handler) CreateCheckoutSessionHandler(c *fiber.Ctx) error {
// @Success 200 {object} domain.Response // @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/arifpay/checkout/{sessionId}/cancel [post] // @Router /api/v1/arifpay/checkout/cancel/{sessionId} [post]
func (h *Handler) CancelCheckoutSessionHandler(c *fiber.Ctx) error { func (h *Handler) CancelCheckoutSessionHandler(c *fiber.Ctx) error {
sessionID := c.Params("sessionId") sessionID := c.Params("sessionId")
if sessionID == "" { if sessionID == "" {
@ -103,15 +112,15 @@ func (h *Handler) HandleArifpayC2BWebhook(c *fiber.Ctx) error {
// 🚨 Decide how to get userId: // 🚨 Decide how to get userId:
// If you get it from auth context/middleware, extract it here. // If you get it from auth context/middleware, extract it here.
// For now, let's assume userId comes from your auth claims: // For now, let's assume userId comes from your auth claims:
userId, ok := c.Locals("user_id").(int64) // userId, ok := c.Locals("user_id").(int64)
if !ok { // if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ // return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Error: "missing user id", // Error: "missing user id",
Message: "Unauthorized", // Message: "Unauthorized",
}) // })
} // }
err := h.arifpaySvc.HandleWebhook(c.Context(), req, userId, true) err := h.arifpaySvc.ProcessWebhook(c.Context(), req, true)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(), Error: err.Error(),
@ -150,15 +159,15 @@ func (h *Handler) HandleArifpayB2CWebhook(c *fiber.Ctx) error {
// 🚨 Decide how to get userId: // 🚨 Decide how to get userId:
// If you get it from auth context/middleware, extract it here. // If you get it from auth context/middleware, extract it here.
// For now, let's assume userId comes from your auth claims: // For now, let's assume userId comes from your auth claims:
userId, ok := c.Locals("user_id").(int64) // userId, ok := c.Locals("user_id").(int64)
if !ok { // if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ // return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Error: "missing user id", // Error: "missing user id",
Message: "Unauthorized", // Message: "Unauthorized",
}) // })
} // }
err := h.arifpaySvc.HandleWebhook(c.Context(), req, userId, false) err := h.arifpaySvc.ProcessWebhook(c.Context(), req, false)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(), Error: err.Error(),
@ -253,7 +262,7 @@ func (h *Handler) ArifpayVerifyBySessionIDHandler(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param type query string true "Transfer type (telebirr, cbe, mpesa)" // @Param type query string true "Transfer type (telebirr, cbe, mpesa)"
// @Param request body domain.ArifpayB2CRequest true "Transfer request payload" // @Param request body domain.CheckoutSessionClientRequest true "Transfer request payload"
// @Success 200 {object} map[string]string "message: transfer executed successfully" // @Success 200 {object} map[string]string "message: transfer executed successfully"
// @Failure 400 {object} map[string]string "error: invalid request or unsupported transfer type" // @Failure 400 {object} map[string]string "error: invalid request or unsupported transfer type"
// @Failure 500 {object} map[string]string "error: internal server error" // @Failure 500 {object} map[string]string "error: internal server error"
@ -275,7 +284,7 @@ func (h *Handler) ExecuteArifpayB2CTransfer(c *fiber.Ctx) error {
}) })
} }
var req domain.ArifpayB2CRequest var req domain.CheckoutSessionClientRequest
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to process your withdrawal request", Message: "Failed to process your withdrawal request",

View File

@ -7,9 +7,11 @@ import (
"fmt" "fmt"
"log" "log"
"strings" "strings"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"go.uber.org/zap"
) )
// GetAtlasVGames godoc // GetAtlasVGames godoc
@ -52,7 +54,7 @@ func (h *Handler) GetAtlasVGames(c *fiber.Ctx) error {
// @Failure 502 {object} domain.ErrorResponse // @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/atlas/init-game [post] // @Router /api/v1/atlas/init-game [post]
func (h *Handler) InitAtlasGame(c *fiber.Ctx) error { func (h *Handler) InitAtlasGame(c *fiber.Ctx) error {
// Retrieve user ID from context // 1Retrieve user ID from context
userId, ok := c.Locals("user_id").(int64) userId, ok := c.Locals("user_id").(int64)
if !ok { if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
@ -61,6 +63,7 @@ func (h *Handler) InitAtlasGame(c *fiber.Ctx) error {
}) })
} }
// 2⃣ Parse request body
var req domain.AtlasGameInitRequest var req domain.AtlasGameInitRequest
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
@ -69,30 +72,53 @@ func (h *Handler) InitAtlasGame(c *fiber.Ctx) error {
}) })
} }
// Attach user ID to request // 3Attach user ID to request
req.PlayerID = fmt.Sprintf("%d", userId) req.PlayerID = fmt.Sprintf("%d", userId)
// Default language if not provided // 4⃣ Set defaults if not provided
if req.Language == "" { if req.Language == "" {
req.Language = "en" req.Language = "en"
} }
// Default currency if not provided
if req.Currency == "" { if req.Currency == "" {
req.Currency = "USD" req.Currency = "USD"
} }
// Call the service // 5Call the Atlas service
res, err := h.atlasVirtualGameSvc.InitGame(context.Background(), req) res, err := h.atlasVirtualGameSvc.InitGame(context.Background(), req)
if err != nil { if err != nil {
log.Println("InitAtlasGame error:", err) log.Println("InitAtlasGame error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to initialize Atlas game", Message: "Failed to initialize Atlas game",
Error: err.Error(), Error: err.Error(),
}) })
} }
// 6⃣ Update provider report: increment total_games_played
go func() {
ctx := context.Background()
reportDate := time.Now().Truncate(24 * time.Hour)
reportType := "daily"
providerID := "atlas" // all Atlas games belong to this provider
err := h.orchestrationSvc.UpdateVirtualGameProviderReportByDate(
ctx,
providerID,
reportDate,
reportType,
1, // increment total_games_played by 1
0, // total_bets (no change)
0, // total_payouts (no change)
1, // total_players (no change)
)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to update total_games_played for Atlas game",
zap.String("provider_id", providerID),
zap.Error(err),
)
}
}()
// 7⃣ Return response to user
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Game initialized successfully", Message: "Game initialized successfully",
Data: res, Data: res,

View File

@ -1,7 +1,9 @@
package handlers package handlers
import ( import (
"encoding/json"
"fmt" "fmt"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -39,7 +41,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
}) })
} }
amount := domain.Currency(req.Amount * 100) amount := domain.Currency(req.Amount)
fmt.Println("We are here init Chapa payment") fmt.Println("We are here init Chapa payment")
@ -51,40 +53,6 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
}) })
} }
// get static wallet of user
// wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
// Error: err.Error(),
// Message: "Failed to initiate Chapa deposit",
// })
// }
// var multiplier float32 = 1
// bonusMultiplier, err := h.bonusSvc.GetBonusMultiplier(c.Context())
// if err == nil {
// multiplier = bonusMultiplier[0].Multiplier
// }
// var balanceCap int64 = 0
// bonusBalanceCap, err := h.bonusSvc.GetBonusBalanceCap(c.Context())
// if err == nil {
// balanceCap = bonusBalanceCap[0].BalanceCap
// }
// capedBalanceAmount := domain.Currency((math.Min(req.Amount, float64(balanceCap)) * float64(multiplier)) * 100)
// _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, capedBalanceAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
// fmt.Sprintf("Added %v to static wallet because of deposit bonus using multiplier %v", capedBalanceAmount, multiplier),
// )
// if err != nil {
// h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err)
// return err
// }
// if err := h.bonusSvc.ProcessWelcomeBonus(c.Context(), domain.ToCurrency(float32(req.Amount)), 0, userID); err != nil {
// return err
// }
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa deposit process initiated successfully", Message: "Chapa deposit process initiated successfully",
Data: checkoutURL, Data: checkoutURL,
@ -95,69 +63,180 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
// WebhookCallback godoc // WebhookCallback godoc
// @Summary Chapa payment webhook callback (used by Chapa) // @Summary Chapa payment webhook callback (used by Chapa)
// @Description Handles payment notifications from Chapa // @Description Handles payment and transfer notifications from Chapa
// @Tags Chapa // @Tags Chapa
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param request body domain.ChapaWebhookPayload true "Webhook payload" // @Param request body domain.ChapaWebhookPayment true "Webhook payload"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/webhook/verify [post] // @Router /api/v1/chapa/payments/webhook/verify [post]
func (h *Handler) WebhookCallback(c *fiber.Ctx) error { func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
body := c.Body()
chapaTransactionType := new(domain.ChapaTransactionType) // Retrieve signature headers
chapaSignature := c.Get("chapa-signature")
xChapaSignature := c.Get("x-chapa-signature")
if parseTypeErr := c.BodyParser(chapaTransactionType); parseTypeErr != nil { // Verify webhook signature
valid, err := h.chapaSvc.VerifyWebhookSignature(c.Context(), body, chapaSignature, xChapaSignature)
if err != nil || !valid {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Chapa webhook signature",
Error: err.Error(),
})
}
// Try parsing as transfer webhook first
var transfer domain.ChapaWebhookTransfer
if err := json.Unmarshal(body, &transfer); err == nil &&
strings.EqualFold(transfer.Type, "payout") {
if err := h.chapaSvc.ProcessVerifyWithdrawWebhook(c.Context(), transfer); err != nil {
return domain.UnExpectedErrorResponse(c)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200,
Message: "Chapa withdrawal webhook processed successfully",
// Data: transfer,
Success: true,
})
}
// Otherwise, try as payment webhook
var payment domain.ChapaWebhookPayment
if err := json.Unmarshal(body, &payment); err != nil {
return domain.UnProcessableEntityResponse(c) return domain.UnProcessableEntityResponse(c)
} }
switch chapaTransactionType.Type { if err := h.chapaSvc.ProcessVerifyDepositWebhook(c.Context(), payment); err != nil {
case h.Cfg.CHAPA_PAYMENT_TYPE:
chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer)
if err := c.BodyParser(chapaTransferVerificationRequest); err != nil {
return domain.UnProcessableEntityResponse(c)
}
err := h.chapaSvc.HandleVerifyDepositWebhook(c.Context(), *chapaTransferVerificationRequest)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa depposit", Message: "Failed to verify Chapa deposit",
Error: err.Error(), Error: err.Error(),
}) })
} }
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
StatusCode: 200, StatusCode: 200,
Message: "Chapa deposit transaction verified successfully", Message: "Chapa deposit webhook processed successfully",
Data: chapaTransferVerificationRequest, // Data: payment,
Success: true, Success: true,
}) })
case h.Cfg.CHAPA_TRANSFER_TYPE:
chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment)
if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil {
return domain.UnProcessableEntityResponse(c)
} }
err := h.chapaSvc.HandleVerifyWithdrawWebhook(c.Context(), *chapaPaymentVerificationRequest) // CancelDeposit godoc
// @Summary Cancel a Chapa deposit transaction
// @Description Cancels an active Chapa transaction using its transaction reference
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param tx_ref path string true "Transaction Reference"
// @Success 200 {object} domain.ChapaCancelResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transaction/cancel/{tx_ref} [put]
func (h *Handler) CancelDeposit(c *fiber.Ctx) error {
// Get user ID from context (set by your auth middleware)
userID, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: "invalid user ID",
Message: "User ID is required to cancel a deposit",
})
}
// Extract tx_ref from URL path
txRef := c.Params("tx_ref")
if txRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: "missing transaction reference",
Message: "Transaction reference is required in the path",
})
}
fmt.Printf("\n\nReceived request to cancel Chapa transaction: %s (User ID: %d)\n\n", txRef, userID)
// Call the service layer to cancel deposit
cancelResp, err := h.chapaSvc.CancelDeposit(c.Context(), userID, txRef)
if err != nil { if err != nil {
return domain.UnExpectedErrorResponse(c) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to cancel Chapa deposit",
})
} }
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transaction cancelled successfully",
Data: cancelResp,
StatusCode: 200, StatusCode: 200,
Message: "Chapa withdrawal transaction verified successfully",
Data: chapaPaymentVerificationRequest,
Success: true, Success: true,
}) })
} }
// Return a 400 Bad Request if the type does not match any known case // FetchAllTransactions godoc
// @Summary Get all Chapa transactions
// @Description Retrieves all transactions from Chapa payment gateway
// @Tags Chapa
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {array} domain.ChapaTransaction
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transactions [get]
func (h *Handler) FetchAllTransactions(c *fiber.Ctx) error {
transactions, err := h.chapaSvc.FetchAllTransactions(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Error: err.Error(),
Message: "Failed to fetch Chapa transactions",
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transactions retrieved successfully",
Data: transactions,
StatusCode: 200,
Success: true,
})
}
// GetTransactionEvents godoc
// @Summary Fetch transaction events
// @Description Retrieve the timeline of events for a specific Chapa transaction
// @Tags Chapa
// @Accept json
// @Produce json
// @Param ref_id path string true "Transaction Reference"
// @Success 200 {array} domain.ChapaTransactionEvent
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transaction/events/{ref_id} [get]
func (h *Handler) GetTransactionEvents(c *fiber.Ctx) error {
refID := c.Params("ref_id")
if refID == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Chapa webhook type", Message: "Failed to fetch transaction events",
Error: "Unknown transaction type", Error: "Transaction reference is required",
})
}
events, err := h.chapaSvc.FetchTransactionEvents(c.Context(), refID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch transaction events",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Transaction events fetched successfully",
Data: events,
StatusCode: 200,
Success: true,
}) })
} }
@ -168,10 +247,10 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param tx_ref path string true "Transaction Reference" // @Param tx_ref path string true "Transaction Reference"
// @Success 200 {object} domain.ChapaVerificationResponse // @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/manual/verify/{tx_ref} [get] // @Router /api/v1/chapa/transaction/manual/verify/{tx_ref} [get]
func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error { func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error {
txRef := c.Params("tx_ref") txRef := c.Params("tx_ref")
if txRef == "" { if txRef == "" {
@ -189,11 +268,11 @@ func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error {
}) })
} }
return c.Status(fiber.StatusOK).JSON(domain.ChapaVerificationResponse{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Status: string(verification.Status), Message: "Chapa transaction verified successfully",
Amount: verification.Amount, Data: verification,
Currency: verification.Currency, StatusCode: 200,
TxRef: txRef, Success: true,
}) })
} }
@ -231,7 +310,7 @@ func (h *Handler) GetSupportedBanks(c *fiber.Ctx) error {
// @Produce json // @Produce json
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Param request body domain.ChapaWithdrawalRequest true "Withdrawal request details" // @Param request body domain.ChapaWithdrawalRequest true "Withdrawal request details"
// @Success 201 {object} domain.Response "Chapa withdrawal process initiated successfully" // @Success 200 {object} domain.Response "Chapa withdrawal process initiated successfully"
// @Failure 400 {object} domain.ErrorResponse "Invalid request body" // @Failure 400 {object} domain.ErrorResponse "Invalid request body"
// @Failure 401 {object} domain.ErrorResponse "Unauthorized" // @Failure 401 {object} domain.ErrorResponse "Unauthorized"
// @Failure 422 {object} domain.ErrorResponse "Unprocessable entity" // @Failure 422 {object} domain.ErrorResponse "Unprocessable entity"
@ -256,10 +335,151 @@ func (h *Handler) InitiateWithdrawal(c *fiber.Ctx) error {
}) })
} }
return c.Status(fiber.StatusCreated).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa withdrawal process initiated successfully", Message: "Chapa withdrawal process initiated successfully",
StatusCode: 201, StatusCode: 200,
Success: true, Success: true,
Data: withdrawal, Data: withdrawal,
}) })
} }
// GetPaymentReceipt godoc
// @Summary Get Chapa Payment Receipt URL
// @Description Retrieve the Chapa payment receipt URL using the reference ID
// @Tags Chapa
// @Accept json
// @Produce json
// @Param chapa_ref path string true "Chapa Reference ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/payments/receipt/{chapa_ref} [get]
func (h *Handler) GetPaymentReceipt(c *fiber.Ctx) error {
chapaRef := c.Params("chapa_ref")
if chapaRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to get Chapa payment receipt",
Error: "Chapa reference ID is required",
})
}
receiptURL, err := h.chapaSvc.GetPaymentReceiptURL(chapaRef)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get Chapa payment receipt",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Payment receipt URL generated successfully",
Data: receiptURL,
StatusCode: 200,
Success: true,
})
}
// GetAllTransfers godoc
// @Summary Get all Chapa transfers
// @Description Retrieve all transfer records from Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/transfers [get]
func (h *Handler) GetAllTransfers(c *fiber.Ctx) error {
transfers, err := h.chapaSvc.GetAllTransfers(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch Chapa transfers",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transfers retrieved successfully",
Data: transfers,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetAccountBalance godoc
// @Summary Get Chapa account balance
// @Description Retrieve Chapa account balance, optionally filtered by currency code (e.g., ETB, USD)
// @Tags Chapa
// @Accept json
// @Produce json
// @Param currency_code query string false "Currency code (optional)"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/balances [get]
func (h *Handler) GetAccountBalance(c *fiber.Ctx) error {
currencyCode := c.Query("currency_code", "")
balances, err := h.chapaSvc.GetAccountBalance(c.Context(), currencyCode)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch Chapa account balance",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa account balances retrieved successfully",
Data: balances,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// SwapCurrency godoc
// @Summary Swap currency using Chapa API
// @Description Convert an amount from one currency to another using Chapa's currency swap API
// @Tags Chapa
// @Accept json
// @Produce json
// @Param request body domain.SwapRequest true "Swap request payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/chapa/swap [post]
func (h *Handler) SwapCurrency(c *fiber.Ctx) error {
var reqBody domain.SwapRequest
// Parse request body
if err := c.BodyParser(&reqBody); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request payload",
Error: err.Error(),
})
}
// Validate input
if reqBody.From == "" || reqBody.To == "" || reqBody.Amount <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Missing or invalid swap parameters",
Error: "from, to, and amount are required fields",
})
}
// Call service
resp, err := h.chapaSvc.SwapCurrency(c.Context(), reqBody)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to perform currency swap",
Error: err.Error(),
})
}
// Success response
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Currency swapped successfully",
Data: resp,
StatusCode: fiber.StatusOK,
Success: true,
})
}

View File

@ -9,67 +9,10 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// GetPreMatchOdds godoc
// @Summary Get pre-match odds for an event
// @Description Fetches pre-match odds from EnetPulse for a given event
// @Tags EnetPulse - PreMatch
// @Accept json
// @Produce json
// @Param objectFK query int true "Event ID"
// @Param oddsProviderFK query []int false "Odds provider IDs (comma separated)"
// @Param outcomeTypeFK query int false "Outcome type ID"
// @Param outcomeScopeFK query int false "Outcome scope ID"
// @Param outcomeSubtypeFK query int false "Outcome subtype ID"
// @Param limit query int false "Limit results"
// @Param offset query int false "Offset results"
// @Param languageTypeFK query int false "Language type ID"
// @Success 200 {object} domain.Response{data=domain.PreMatchOddsResponse}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/odds/pre-match [get]
func (h *Handler) GetPreMatchOdds(c *fiber.Ctx) error {
// Parse query parameters
objectFK := c.QueryInt("objectFK")
if objectFK == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Event ID (objectFK) is required",
Error: "missing or invalid objectFK",
})
}
params := domain.PreMatchOddsRequest{
ObjectFK: int64(objectFK),
OddsProviderFK: intSliceToInt64Slice(parseIntSlice(c.Query("oddsProviderFK"))), // convert []int to []int64
OutcomeTypeFK: int64(c.QueryInt("outcomeTypeFK")),
OutcomeScopeFK: int64(c.QueryInt("outcomeScopeFK")),
OutcomeSubtypeFK: int64(c.QueryInt("outcomeSubtypeFK")),
Limit: c.QueryInt("limit"),
Offset: c.QueryInt("offset"),
LanguageTypeFK: int64(c.QueryInt("languageTypeFK")),
}
// Call service
res, err := h.enetPulseSvc.FetchPreMatchOdds(c.Context(), params)
if err != nil {
log.Println("FetchPreMatchOdds error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch pre-match odds",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Pre-match odds fetched successfully",
Data: res,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetAllSports godoc // GetAllSports godoc
// @Summary Get all sports // @Summary Get all sports
// @Description Fetches all sports stored in the database // @Description Fetches all sports stored in the database
// @Tags EnetPulse - Sports // @Tags EnetPulse
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulseSport} // @Success 200 {object} domain.Response{data=[]domain.EnetpulseSport}
@ -97,7 +40,7 @@ func (h *Handler) GetAllSports(c *fiber.Ctx) error {
// GetAllTournamentTemplates godoc // GetAllTournamentTemplates godoc
// @Summary Get all tournament templates // @Summary Get all tournament templates
// @Description Fetches all tournament templates stored in the database // @Description Fetches all tournament templates stored in the database
// @Tags EnetPulse - Tournament Templates // @Tags EnetPulse
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulseTournamentTemplate} // @Success 200 {object} domain.Response{data=[]domain.EnetpulseTournamentTemplate}
@ -125,7 +68,7 @@ func (h *Handler) GetAllTournamentTemplates(c *fiber.Ctx) error {
// GetAllTournaments godoc // GetAllTournaments godoc
// @Summary Get all tournaments // @Summary Get all tournaments
// @Description Fetches all tournaments stored in the database // @Description Fetches all tournaments stored in the database
// @Tags EnetPulse - Tournaments // @Tags EnetPulse
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulseTournament} // @Success 200 {object} domain.Response{data=[]domain.EnetpulseTournament}
@ -153,7 +96,7 @@ func (h *Handler) GetAllTournaments(c *fiber.Ctx) error {
// GetAllTournamentStages godoc // GetAllTournamentStages godoc
// @Summary Get all tournament stages // @Summary Get all tournament stages
// @Description Fetches all tournament stages stored in the database // @Description Fetches all tournament stages stored in the database
// @Tags EnetPulse - Tournament Stages // @Tags EnetPulse
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulseTournamentStage} // @Success 200 {object} domain.Response{data=[]domain.EnetpulseTournamentStage}
@ -178,8 +121,177 @@ func (h *Handler) GetAllTournamentStages(c *fiber.Ctx) error {
}) })
} }
// GetFixturesByDate godoc
// @Summary Get all stored fixtures
// @Description Fetches all fixtures stored in the database
// @Tags EnetPulse
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulseFixture}
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/enetpulse/fixtures [get]
func (h *Handler) GetFixturesByDate(c *fiber.Ctx) error {
// Call service to get all fixtures from DB
fixtures, err := h.enetPulseSvc.GetAllFixtures(c.Context())
if err != nil {
log.Println("GetAllFixtures error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch fixtures from database",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Fixtures fetched successfully",
Data: fixtures,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetAllResults godoc
// @Summary Get all results
// @Description Fetches all EnetPulse match results stored in the database
// @Tags EnetPulse
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulseResult}
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/enetpulse/results [get]
func (h *Handler) GetAllResults(c *fiber.Ctx) error {
// Call service
results, err := h.enetPulseSvc.GetAllResults(c.Context())
if err != nil {
log.Println("GetAllResults error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch EnetPulse results",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "EnetPulse results fetched successfully",
Data: results,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetAllPreodds godoc
// @Summary Get all preodds
// @Description Fetches all EnetPulse pre-match odds stored in the database
// @Tags EnetPulse
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulsePreodds}
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/enetpulse/preodds [get]
func (h *Handler) GetAllPreodds(c *fiber.Ctx) error {
// Call service
preodds, err := h.enetPulseSvc.GetAllPreodds(c.Context())
if err != nil {
log.Println("GetAllPreodds error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch EnetPulse preodds",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "EnetPulse preodds fetched successfully",
Data: preodds,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetAllBettingOffers godoc
// @Summary Get all betting offers
// @Description Fetches all EnetPulse preodds betting offers stored in the database
// @Tags EnetPulse
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulsePreoddsBettingOffer}
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/enetpulse/betting-offers [get]
func (h *Handler) GetAllBettingOffers(c *fiber.Ctx) error {
// Call service
offers, err := h.enetPulseSvc.GetAllBettingOffers(c.Context())
if err != nil {
log.Println("GetAllBettingOffers error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch EnetPulse betting offers",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "EnetPulse betting offers fetched successfully",
Data: offers,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetAllPreoddsWithBettingOffers godoc
// @Summary Get all preodds with betting offers
// @Description Fetches all EnetPulse pre-match odds along with their associated betting offers stored in the database
// @Tags EnetPulse
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulsePreodds}
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/enetpulse/preodds-with-offers [get]
func (h *Handler) GetAllPreoddsWithBettingOffers(c *fiber.Ctx) error {
// Call service
preodds, err := h.enetPulseSvc.GetAllPreoddsWithBettingOffers(c.Context())
if err != nil {
log.Println("GetAllPreoddsWithBettingOffers error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch EnetPulse preodds with betting offers",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "EnetPulse preodds with betting offers fetched successfully",
Data: preodds,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// GetFixturesWithPreodds godoc
// @Summary Get fixtures with preodds
// @Description Fetches all EnetPulse fixtures along with their associated pre-match odds
// @Tags EnetPulse
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=[]domain.EnetpulseFixtureWithPreodds}
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/enetpulse/fixtures/preodds [get]
func (h *Handler) GetFixturesWithPreodds(c *fiber.Ctx) error {
// Call service
fixtures, err := h.enetPulseSvc.GetFixturesWithPreodds(c.Context())
if err != nil {
log.Println("GetFixturesWithPreodds error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch EnetPulse fixtures with preodds",
Error: err.Error(),
})
}
// Return success response
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "EnetPulse fixtures with preodds fetched successfully",
Data: fixtures,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// Helper: parse comma-separated string into []int // Helper: parse comma-separated string into []int
func parseIntSlice(input string) []int { func ParseIntSlice(input string) []int {
if input == "" { if input == "" {
return nil return nil
} }
@ -194,7 +306,7 @@ func parseIntSlice(input string) []int {
} }
// Helper: convert []int to []int64 // Helper: convert []int to []int64
func intSliceToInt64Slice(input []int) []int64 { func IntSliceToInt64Slice(input []int) []int64 {
if input == nil { if input == nil {
return nil return nil
} }

View File

@ -34,6 +34,7 @@ import (
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/orchestration"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
@ -42,6 +43,7 @@ import (
) )
type Handler struct { type Handler struct {
orchestrationSvc *orchestration.Service
enetPulseSvc *enetpulse.Service enetPulseSvc *enetpulse.Service
telebirrSvc *telebirr.TelebirrService telebirrSvc *telebirr.TelebirrService
arifpaySvc *arifpay.ArifpayService arifpaySvc *arifpay.ArifpayService
@ -69,7 +71,7 @@ type Handler struct {
leagueSvc *league.Service leagueSvc *league.Service
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService veliVirtualGameSvc *veli.Service
atlasVirtualGameSvc atlas.AtlasVirtualGameService atlasVirtualGameSvc atlas.AtlasVirtualGameService
recommendationSvc recommendation.RecommendationService recommendationSvc recommendation.RecommendationService
authSvc *authentication.Service authSvc *authentication.Service
@ -82,6 +84,7 @@ type Handler struct {
} }
func New( func New(
orchestrationSvc *orchestration.Service,
enetPulseSvc *enetpulse.Service, enetPulseSvc *enetpulse.Service,
telebirrSvc *telebirr.TelebirrService, telebirrSvc *telebirr.TelebirrService,
arifpaySvc *arifpay.ArifpayService, arifpaySvc *arifpay.ArifpayService,
@ -101,7 +104,7 @@ func New(
bonusSvc *bonus.Service, bonusSvc *bonus.Service,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameSvc alea.AleaVirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService,
veliVirtualGameSvc veli.VeliVirtualGameService, veliVirtualGameSvc *veli.Service,
atlasVirtualGameSvc atlas.AtlasVirtualGameService, atlasVirtualGameSvc atlas.AtlasVirtualGameService,
recommendationSvc recommendation.RecommendationService, recommendationSvc recommendation.RecommendationService,
userSvc *user.Service, userSvc *user.Service,
@ -121,6 +124,7 @@ func New(
mongoLoggerSvc *zap.Logger, mongoLoggerSvc *zap.Logger,
) *Handler { ) *Handler {
return &Handler{ return &Handler{
orchestrationSvc: orchestrationSvc,
enetPulseSvc: enetPulseSvc, enetPulseSvc: enetPulseSvc,
telebirrSvc: telebirrSvc, telebirrSvc: telebirrSvc,
arifpaySvc: arifpaySvc, arifpaySvc: arifpaySvc,

View File

@ -3,7 +3,9 @@ package handlers
import ( import (
"context" "context"
"errors" "errors"
"fmt" "time"
// "fmt"
"strings" "strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -120,14 +122,6 @@ func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error {
// @Failure 502 {object} domain.ErrorResponse // @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/veli/start-game [post] // @Router /api/v1/veli/start-game [post]
func (h *Handler) StartGame(c *fiber.Ctx) error { func (h *Handler) StartGame(c *fiber.Ctx) error {
userId, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Error: "missing user id",
Message: "Unauthorized",
})
}
var req domain.GameStartRequest var req domain.GameStartRequest
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
@ -136,25 +130,24 @@ func (h *Handler) StartGame(c *fiber.Ctx) error {
}) })
} }
// There needs to be a way to generate a sessionID
// Attach user ID to request
req.PlayerID = fmt.Sprintf("%d", userId)
// Default brand if not provided // Default brand if not provided
if req.BrandID == "" { if req.BrandID == "" {
req.BrandID = h.Cfg.VeliGames.BrandID req.BrandID = h.Cfg.VeliGames.BrandID
} }
req.IP = c.IP() useId := c.Locals("user_id")
req.IP = c.IP()
req.PlayerID = useId.(string)
// 1⃣ Call StartGame service
res, err := h.veliVirtualGameSvc.StartGame(context.Background(), req) res, err := h.veliVirtualGameSvc.StartGame(context.Background(), req)
if err != nil { if err != nil {
h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]StartGame", h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]StartGame",
zap.Any("request", req), zap.Any("request", req),
zap.Error(err), zap.Error(err),
) )
// Handle provider disabled case specifically
if strings.Contains(err.Error(), "is disabled") { if strings.Contains(err.Error(), "is disabled") {
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Provider is disabled", Message: "Provider is disabled",
@ -162,13 +155,39 @@ func (h *Handler) StartGame(c *fiber.Ctx) error {
}) })
} }
// Fallback for other errors
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to start game", Message: "Failed to start game",
Error: err.Error(), Error: err.Error(),
}) })
} }
// 2⃣ Game started successfully → Update total_games_played
go func() {
ctx := context.Background()
reportDate := time.Now().Truncate(24 * time.Hour)
reportType := "daily"
// Increment total_games_played by 1
err := h.orchestrationSvc.UpdateVirtualGameProviderReportByDate(
ctx,
req.ProviderID,
reportDate,
reportType,
1, // increment total_games_played by 1
0,
0,
1,
)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to update total_games_played",
zap.String("provider_id", req.ProviderID),
zap.Error(err),
)
}
}()
// 3⃣ Return response to user
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Game started successfully", Message: "Game started successfully",
Data: res, Data: res,
@ -260,15 +279,13 @@ func (h *Handler) GetBalance(c *fiber.Ctx) error {
func (h *Handler) PlaceBet(c *fiber.Ctx) error { func (h *Handler) PlaceBet(c *fiber.Ctx) error {
var req domain.BetRequest var req domain.BetRequest
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body", Message: "Invalid request body",
Error: err.Error(), Error: err.Error(),
}) })
} }
// Signature check optional here // 1⃣ Process the bet with the external provider
res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req) res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req)
if err != nil { if err != nil {
if errors.Is(err, veli.ErrDuplicateTransaction) { if errors.Is(err, veli.ErrDuplicateTransaction) {
@ -278,10 +295,42 @@ func (h *Handler) PlaceBet(c *fiber.Ctx) error {
Message: "Failed to process bet", Message: "Failed to process bet",
Error: err.Error(), Error: err.Error(),
}) })
// return fiber.NewError(fiber.StatusBadRequest, err.Error())
} }
return c.JSON(res) // 2⃣ If bet successful → update total_bets in the report
go func() {
ctx := context.Background()
reportDate := time.Now().Truncate(24 * time.Hour)
reportType := "daily"
// Increment total_bets by the bet amount
err := h.orchestrationSvc.UpdateVirtualGameProviderReportByDate(
ctx,
req.ProviderID,
reportDate,
reportType,
0, // total_games_played (no change)
req.Amount.Amount, // add this bet to total_bets
0, // total_payouts (no change)
0, // total_players (no change)
)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to update total_bets after bet",
zap.String("provider_id", req.ProviderID),
zap.Float64("bet_amount", req.Amount.Amount),
zap.Error(err),
)
}
}()
// 3⃣ Return success response
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Bet processed successfully",
Data: res,
StatusCode: fiber.StatusOK,
Success: true,
})
} }
func (h *Handler) RegisterWin(c *fiber.Ctx) error { func (h *Handler) RegisterWin(c *fiber.Ctx) error {

View File

@ -25,6 +25,54 @@ type launchVirtualGameRes struct {
LaunchURL string `json:"launch_url"` LaunchURL string `json:"launch_url"`
} }
// ListVirtualGameProviderReportsAscHandler
// @Summary List virtual game provider reports (ascending)
// @Description Retrieves all virtual game provider reports sorted by total_games_played in ascending order
// @Tags VirtualGames - Orchestration
// @Success 200 {array} domain.VirtualGameProviderReport
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/orchestrator/virtual-game/provider-reports/asc [get]
func (h *Handler) ListVirtualGameProviderReportsAscHandler(c *fiber.Ctx) error {
reports, err := h.orchestrationSvc.ListVirtualGameProviderReportsByGamesPlayedAsc(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch virtual game provider reports ascending",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Virtual game provider reports retrieved successfully",
Data: reports,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// ListVirtualGameProviderReportsDescHandler
// @Summary List virtual game provider reports (descending)
// @Description Retrieves all virtual game provider reports sorted by total_games_played in descending order
// @Tags VirtualGames - Orchestration
// @Success 200 {array} domain.VirtualGameProviderReport
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/orchestrator/virtual-game/provider-reports/desc [get]
func (h *Handler) ListVirtualGameProviderReportsDescHandler(c *fiber.Ctx) error {
reports, err := h.orchestrationSvc.ListVirtualGameProviderReportsByGamesPlayedDesc(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch virtual game provider reports descending",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Virtual game provider reports retrieved successfully",
Data: reports,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// ListVirtualGames godoc // ListVirtualGames godoc
// @Summary List all virtual games // @Summary List all virtual games
// @Description Returns all virtual games with optional filters (category, search, pagination) // @Description Returns all virtual games with optional filters (category, search, pagination)
@ -77,7 +125,7 @@ func (h *Handler) ListVirtualGames(c *fiber.Ctx) error {
} }
// --- Call service method --- // --- Call service method ---
games, err := h.veliVirtualGameSvc.GetAllVirtualGames(c.Context(), params) games, err := h.orchestrationSvc.GetAllVirtualGames(c.Context(), params)
if err != nil { if err != nil {
log.Println("ListVirtualGames error:", err) log.Println("ListVirtualGames error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
@ -105,7 +153,7 @@ func (h *Handler) ListVirtualGames(c *fiber.Ctx) error {
// @Router /api/v1/virtual-game/providers/{provider_id} [delete] // @Router /api/v1/virtual-game/providers/{provider_id} [delete]
func (h *Handler) RemoveProvider(c *fiber.Ctx) error { func (h *Handler) RemoveProvider(c *fiber.Ctx) error {
providerID := c.Params("providerID") providerID := c.Params("providerID")
if err := h.virtualGameSvc.RemoveProvider(c.Context(), providerID); err != nil { if err := h.orchestrationSvc.RemoveProvider(c.Context(), providerID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not remove provider") return fiber.NewError(fiber.StatusInternalServerError, "Could not remove provider")
} }
return c.SendStatus(fiber.StatusOK) return c.SendStatus(fiber.StatusOK)
@ -122,7 +170,7 @@ func (h *Handler) RemoveProvider(c *fiber.Ctx) error {
// @Router /api/v1/virtual-game/providers/{provider_id} [get] // @Router /api/v1/virtual-game/providers/{provider_id} [get]
func (h *Handler) GetProviderByID(c *fiber.Ctx) error { func (h *Handler) GetProviderByID(c *fiber.Ctx) error {
providerID := c.Params("providerID") providerID := c.Params("providerID")
provider, err := h.virtualGameSvc.GetProviderByID(c.Context(), providerID) provider, err := h.orchestrationSvc.GetProviderByID(c.Context(), providerID)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not fetch provider") return fiber.NewError(fiber.StatusInternalServerError, "Could not fetch provider")
} }
@ -143,7 +191,7 @@ func (h *Handler) ListProviders(c *fiber.Ctx) error {
limit, _ := strconv.Atoi(c.Query("limit", "20")) limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0")) offset, _ := strconv.Atoi(c.Query("offset", "0"))
providers, total, err := h.virtualGameSvc.ListProviders(c.Context(), int32(limit), int32(offset)) providers, total, err := h.orchestrationSvc.ListProviders(c.Context(), int32(limit), int32(offset))
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not list providers") return fiber.NewError(fiber.StatusInternalServerError, "Could not list providers")
} }
@ -168,7 +216,7 @@ func (h *Handler) SetProviderEnabled(c *fiber.Ctx) error {
providerID := c.Params("providerID") providerID := c.Params("providerID")
enabled, _ := strconv.ParseBool(c.Query("enabled", "true")) enabled, _ := strconv.ParseBool(c.Query("enabled", "true"))
provider, err := h.virtualGameSvc.SetProviderEnabled(c.Context(), providerID, enabled) provider, err := h.orchestrationSvc.SetProviderEnabled(c.Context(), providerID, enabled)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not update provider status") return fiber.NewError(fiber.StatusInternalServerError, "Could not update provider status")
} }
@ -264,6 +312,14 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error {
} }
func (h *Handler) HandleBet(c *fiber.Ctx) error { func (h *Handler) HandleBet(c *fiber.Ctx) error {
// userID := c.Locals("user_id")
// fmt.Printf("\n\nBet User ID is%v\n\n",userID)
// if userID == "" {
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
// Message: "Failed to process Bet request",
// Error: "Invalid user identification",
// })
// }
// Read the raw body // Read the raw body
body := c.Body() body := c.Body()
if len(body) == 0 { if len(body) == 0 {
@ -292,6 +348,8 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
}) })
} }
// req.PlayerID = fmt.Sprintf("%v", userID)
res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req) res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req)
if err != nil { if err != nil {
if errors.Is(err, veli.ErrDuplicateTransaction) { if errors.Is(err, veli.ErrDuplicateTransaction) {
@ -316,6 +374,8 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
}) })
} }
// req.PlayerID = fmt.Sprintf("%v", userID)
resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req) resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req)
if err != nil { if err != nil {
code := fiber.StatusInternalServerError code := fiber.StatusInternalServerError
@ -341,6 +401,8 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
}) })
} }
// req.PlayerID = fmt.Sprintf("%v", userID)
resp, err := h.atlasVirtualGameSvc.ProcessBet(c.Context(), req) resp, err := h.atlasVirtualGameSvc.ProcessBet(c.Context(), req)
if err != nil { if err != nil {
// code := fiber.StatusInternalServerError // code := fiber.StatusInternalServerError

View File

@ -20,6 +20,7 @@ import (
func (a *App) initAppRoutes() { func (a *App) initAppRoutes() {
h := handlers.New( h := handlers.New(
a.orchestrationSvc,
a.enetPulseSvc, a.enetPulseSvc,
a.telebirrSvc, a.telebirrSvc,
a.arifpaySvc, a.arifpaySvc,
@ -150,9 +151,9 @@ func (a *App) initAppRoutes() {
//Arifpay //Arifpay
groupV1.Post("/arifpay/checkout", a.authMiddleware, h.CreateCheckoutSessionHandler) groupV1.Post("/arifpay/checkout", a.authMiddleware, h.CreateCheckoutSessionHandler)
groupV1.Post("/arifpay/checkout/cancel/:session_id", a.authMiddleware, h.CancelCheckoutSessionHandler) groupV1.Post("/arifpay/checkout/cancel/:sessionId", a.authMiddleware, h.CancelCheckoutSessionHandler)
groupV1.Post("/api/v1/arifpay/c2b-webhook", a.authMiddleware, h.HandleArifpayC2BWebhook) groupV1.Post("/api/v1/arifpay/c2b-webhook", h.HandleArifpayC2BWebhook)
groupV1.Post("/api/v1/arifpay/b2c-webhook", a.authMiddleware, h.HandleArifpayB2CWebhook) groupV1.Post("/api/v1/arifpay/b2c-webhook", h.HandleArifpayB2CWebhook)
groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.ExecuteArifpayB2CTransfer) groupV1.Post("/arifpay/b2c/transfer", a.authMiddleware, h.ExecuteArifpayB2CTransfer)
groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler)
groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler)
@ -275,7 +276,7 @@ func (a *App) initAppRoutes() {
tenant.Delete("/odds/market-settings", a.authMiddleware, a.CompanyOnly, h.DeleteAllCompanyMarketSettings) tenant.Delete("/odds/market-settings", a.authMiddleware, a.CompanyOnly, h.DeleteAllCompanyMarketSettings)
tenant.Delete("/odds/market-settings/:id", a.authMiddleware, a.CompanyOnly, h.DeleteCompanyMarketSettings) tenant.Delete("/odds/market-settings/:id", a.authMiddleware, a.CompanyOnly, h.DeleteCompanyMarketSettings)
groupV1.Get("/events", a.authMiddleware, h.GetAllEvents) groupV1.Get("/events", h.GetAllEvents)
groupV1.Get("/events/:id", a.authMiddleware, h.GetEventByID) groupV1.Get("/events/:id", a.authMiddleware, h.GetEventByID)
groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved)
groupV1.Patch("/events/:id/is_monitored", a.authMiddleware, a.SuperAdminOnly, h.SetEventIsMonitored) groupV1.Patch("/events/:id/is_monitored", a.authMiddleware, a.SuperAdminOnly, h.SetEventIsMonitored)
@ -290,11 +291,16 @@ func (a *App) initAppRoutes() {
tenant.Get("/events/:id/bets", a.authMiddleware, a.CompanyOnly, h.GetTenantBetsByEventID) tenant.Get("/events/:id/bets", a.authMiddleware, a.CompanyOnly, h.GetTenantBetsByEventID)
//EnetPulse //EnetPulse
groupV1.Get("/odds/pre-match", h.GetPreMatchOdds) // groupV1.Get("/odds/pre-match", h.GetPreMatchOdds)
groupV1.Get("/sports", h.GetAllSports) groupV1.Get("/sports", h.GetAllSports)
groupV1.Get("/tournament_templates", h.GetAllTournamentTemplates) groupV1.Get("/tournament_templates", h.GetAllTournamentTemplates)
groupV1.Get("/tournaments", h.GetAllTournamentTemplates) groupV1.Get("/tournaments", h.GetAllTournaments)
groupV1.Get("/tournament_stages", h.GetAllTournamentStages) groupV1.Get("/tournament_stages", h.GetAllTournamentStages)
groupV1.Get("/fixtures", h.GetFixturesByDate)
groupV1.Get("/results", h.GetAllResults)
groupV1.Get("/preodds", h.GetAllPreoddsWithBettingOffers)
groupV1.Get("/bettingoffers", h.GetAllBettingOffers)
groupV1.Get("/fixtures/preodds", h.GetFixturesWithPreodds)
// Leagues // Leagues
groupV1.Get("/leagues", a.authMiddleware, a.SuperAdminOnly, h.GetAllLeagues) groupV1.Get("/leagues", a.authMiddleware, a.SuperAdminOnly, h.GetAllLeagues)
@ -381,10 +387,17 @@ func (a *App) initAppRoutes() {
//Chapa Routes //Chapa Routes
groupV1.Post("/chapa/payments/webhook/verify", h.WebhookCallback) groupV1.Post("/chapa/payments/webhook/verify", h.WebhookCallback)
groupV1.Get("/chapa/payments/manual/verify/:tx_ref", h.ManualVerifyTransaction) groupV1.Get("/chapa/transaction/manual/verify/:tx_ref", a.authMiddleware, h.ManualVerifyTransaction)
groupV1.Put("/chapa/transaction/cancel/:tx_ref", a.authMiddleware, h.CancelDeposit)
groupV1.Get("/chapa/transactions", a.authMiddleware, h.FetchAllTransactions)
groupV1.Get("/chapa/transaction/events/:ref_id", a.authMiddleware, h.GetTransactionEvents)
groupV1.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit) groupV1.Post("/chapa/payments/deposit", a.authMiddleware, h.InitiateDeposit)
groupV1.Post("/chapa/payments/withdraw", a.authMiddleware, h.InitiateWithdrawal) groupV1.Post("/chapa/payments/withdraw", a.authMiddleware, h.InitiateWithdrawal)
groupV1.Get("/chapa/banks", h.GetSupportedBanks) groupV1.Get("/chapa/banks", h.GetSupportedBanks)
groupV1.Get("/chapa/payments/receipt/:chapa_ref", a.authMiddleware, h.GetPaymentReceipt)
groupV1.Get("/chapa/transfers", a.authMiddleware, h.GetAllTransfers)
groupV1.Get("/chapa/balance", a.authMiddleware, h.GetAccountBalance)
groupV1.Post("/chapa/swap", a.authMiddleware, h.SwapCurrency)
// Currencies // Currencies
groupV1.Get("/currencies", h.GetSupportedCurrencies) groupV1.Get("/currencies", h.GetSupportedCurrencies)
@ -414,7 +427,7 @@ func (a *App) initAppRoutes() {
groupV1.Post("/veli/credit-balances", a.authMiddleware, h.GetCreditBalances) groupV1.Post("/veli/credit-balances", a.authMiddleware, h.GetCreditBalances)
//Atlas Virtual Game Routes //Atlas Virtual Game Routes
groupV1.Get("/atlas/games", a.authMiddleware, h.InitAtlasGame) groupV1.Get("/atlas/games", h.GetAtlasVGames)
groupV1.Post("/atlas/init-game", a.authMiddleware, h.InitAtlasGame) groupV1.Post("/atlas/init-game", a.authMiddleware, h.InitAtlasGame)
a.fiber.Post("/account", h.AtlasGetUserDataCallback) a.fiber.Post("/account", h.AtlasGetUserDataCallback)
a.fiber.Post("/betwin", h.HandleAtlasBetWin) a.fiber.Post("/betwin", h.HandleAtlasBetWin)
@ -469,6 +482,8 @@ func (a *App) initAppRoutes() {
groupV1.Delete("/virtual-game/favorites/:gameID", a.authMiddleware, h.RemoveFavorite) groupV1.Delete("/virtual-game/favorites/:gameID", a.authMiddleware, h.RemoveFavorite)
groupV1.Get("/virtual-game/favorites", a.authMiddleware, h.ListFavorites) groupV1.Get("/virtual-game/favorites", a.authMiddleware, h.ListFavorites)
groupV1.Get("/orchestrator/virtual-game/provider-reports/asc", a.OnlyAdminAndAbove, h.ListVirtualGameProviderReportsAscHandler)
groupV1.Get("/orchestrator/virtual-game/provider-reports/desc", a.OnlyAdminAndAbove, h.ListVirtualGameProviderReportsDescHandler)
groupV1.Delete("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.RemoveProvider) groupV1.Delete("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.RemoveProvider)
groupV1.Get("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.GetProviderByID) groupV1.Get("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.GetProviderByID)
groupV1.Get("/virtual-game/orchestrator/games", h.ListVirtualGames) groupV1.Get("/virtual-game/orchestrator/games", h.ListVirtualGames)

View File

@ -46,45 +46,45 @@ postgres:
.PHONY: backup .PHONY: backup
backup: backup:
@mkdir -p backup @mkdir -p backup
@docker exec -t fortunebet-backend-postgres-1 pg_dump -U root --data-only --exclude-table=schema_migrations gh | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz @docker exec -t fortunebet-postgres-1 pg_dump -U root --data-only --exclude-table=schema_migrations gh | gzip > backup/dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.gz
restore: restore:
@echo "Restoring latest backup..." @echo "Restoring latest backup..."
@latest_file=$$(ls -t backup/dump_*.sql.gz | head -n 1); \ @latest_file=$$(ls -t backup/dump_*.sql.gz | head -n 1); \
echo "Restoring from $$latest_file"; \ echo "Restoring from $$latest_file"; \
gunzip -c $$latest_file | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh gunzip -c $$latest_file | docker exec -i fortunebet-postgres-1 psql -U root -d gh
restore_file: restore_file:
@echo "Restoring latest backup..." @echo "Restoring latest backup..."
gunzip -c $(file) | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh gunzip -c $(file) | docker exec -i fortunebet-postgres-1 psql -U root -d gh
.PHONY: seed_data .PHONY: seed_data
seed_data: seed_data:
@echo "Waiting for PostgreSQL to be ready..." @echo "Waiting for PostgreSQL to be ready..."
@until docker exec fortunebet-backend-postgres-1 pg_isready -U root -d gh; do \ @until docker exec fortunebet-postgres-1 pg_isready -U root -d gh; do \
echo "PostgreSQL is not ready yet..."; \ echo "PostgreSQL is not ready yet..."; \
sleep 1; \ sleep 1; \
done done
@for file in db/data/*.sql; do \ @for file in db/data/*.sql; do \
echo "Seeding $$file..."; \ echo "Seeding $$file..."; \
cat $$file | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh; \ cat $$file | docker exec -i fortunebet-postgres-1 psql -U root -d gh; \
done done
.PHONY: seed_dev_data .PHONY: seed_dev_data
seed_dev_data: seed_dev_data:
@echo "Waiting for PostgreSQL to be ready..." @echo "Waiting for PostgreSQL to be ready..."
@until docker exec fortunebet-backend-postgres-1 pg_isready -U root -d gh; do \ @until docker exec fortunebet-postgres-1 pg_isready -U root -d gh; do \
echo "PostgreSQL is not ready yet..."; \ echo "PostgreSQL is not ready yet..."; \
sleep 1; \ sleep 1; \
done done
cat db/scripts/fix_autoincrement_desync.sql | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh; cat db/scripts/fix_autoincrement_desync.sql | docker exec -i fortunebet-postgres-1 psql -U root -d gh;
@for file in db/dev_data/*.sql; do \ @for file in db/dev_data/*.sql; do \
if [ -f "$$file" ]; then \ if [ -f "$$file" ]; then \
echo "Seeding $$file..."; \ echo "Seeding $$file..."; \
cat $$file | docker exec -i fortunebet-backend-postgres-1 psql -U root -d gh; \ cat $$file | docker exec -i fortunebet-postgres-1 psql -U root -d gh; \
fi \ fi \
done done
postgres_log: postgres_log:
docker logs fortunebet-backend-postgres-1 docker logs fortunebet-postgres-1
.PHONY: swagger .PHONY: swagger
swagger: swagger:
@swag init -g cmd/main.go @swag init -g cmd/main.go
@ -94,7 +94,7 @@ logs:
db-up: | logs db-up: | logs
@mkdir -p logs @mkdir -p logs
@docker compose up -d postgres migrate mongo @docker compose up -d postgres migrate mongo
@docker logs fortunebet-backend-postgres-1 > logs/postgres.log 2>&1 & @docker logs fortunebet-postgres-1 > logs/postgres.log 2>&1 &
.PHONY: db-down .PHONY: db-down
db-down: db-down:
@docker compose down -v @docker compose down -v