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

View File

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

View File

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

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

View File

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

View File

@ -63,38 +63,38 @@ RETURNING id,
INSERT INTO virtual_game_sessions (
user_id,
game_id,
session_token,
currency,
status,
expires_at
)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id,
session_token
)
VALUES ($1, $2, $3)
RETURNING
id,
user_id,
game_id,
session_token,
currency,
status,
created_at,
updated_at,
expires_at;
updated_at;
-- name: GetVirtualGameSessionByUserID :one
SELECT
id,
user_id,
game_id,
session_token,
created_at,
updated_at
FROM virtual_game_sessions
WHERE user_id = $1;
-- name: GetVirtualGameSessionByToken :one
SELECT id,
user_id,
game_id,
session_token,
currency,
status,
created_at,
updated_at,
expires_at
updated_at
FROM virtual_game_sessions
WHERE session_token = $1;
-- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions
SET status = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions (
session_id,
@ -288,3 +288,86 @@ ORDER BY vg.created_at DESC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: DeleteAllVirtualGames :exec
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
image: postgres:16-alpine
ports:
- 5422:5432
- "5422:5432"
environment:
- POSTGRES_PASSWORD=secret
- POSTGRES_USER=root
@ -20,12 +20,13 @@ services:
volumes:
- postgres_data:/var/lib/postgresql/data
- ./exports:/exports
mongo:
container_name: fortunebet-mongo
image: mongo:7.0.11
restart: always
ports:
- "27017:27017"
- "27025:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: secret
@ -38,6 +39,7 @@ services:
interval: 10s
timeout: 5s
retries: 5
migrate:
image: migrate/migrate
volumes:
@ -72,7 +74,7 @@ services:
dockerfile: Dockerfile
target: runner
ports:
- ${PORT}:8080
- "${PORT}:8080"
environment:
- DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable
- MONGO_URI=mongodb://root:secret@mongo:27017

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

View File

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

View File

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

8
go.mod
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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) {
return s.betStore.GetBetOutcomeViewByEventID(ctx, eventID, filter)
}
func (s *Service) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) {
return s.betStore.GetBetOutcomeByEventID(ctx, eventID, is_filtered)
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -237,10 +237,10 @@ func (s *Service) FetchB365ResultAndUpdateBets(ctx context.Context) error {
Value: time.Now(),
Valid: true,
},
Source: domain.ValidEventSource{
Value: domain.EVENT_SOURCE_BET365,
Valid: true,
},
// Source: domain.ValidEventSource{
// Value: domain.EVENT_SOURCE_BET365,
// Valid: true,
// },
})
if err != nil {
@ -463,10 +463,10 @@ func (s *Service) CheckAndUpdateExpiredB365Events(ctx context.Context) (int64, e
Value: time.Now(),
Valid: true,
},
Source: domain.ValidEventSource{
Value: domain.EVENT_SOURCE_BET365,
Valid: true,
},
// Source: domain.ValidEventSource{
// Value: domain.EVENT_SOURCE_BET365,
// Valid: true,
// },
})
if err != nil {
s.mongoLogger.Error(
@ -685,7 +685,7 @@ func (s *Service) GetBet365ResultForEvent(ctx context.Context, b365EventID strin
zap.String("b365EventID", b365EventID),
zap.Error(err),
)
return json.RawMessage{}, nil, fmt.Errorf("invalid API response for event %d", b365EventID)
return json.RawMessage{}, nil, fmt.Errorf("invalid API response for event %s", b365EventID)
}
var commonResp domain.CommonResultResponse

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)
}
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
wallet, err := s.walletSvc.GetCustomerWallet(ctx, userID)
if err != nil {
return fmt.Errorf("failed to get wallets for user %d: %w", userID, err)
return fmt.Errorf("failed to get wallets for customer %d: %w", userID, err)
}
// Optionally, credit user wallet
if transfer.Type == domain.DEPOSIT {
if _, err := s.walletSvc.AddToWallet(
ctx,
wallets[0].ID,
wallet.RegularID,
domain.Currency(amount),
domain.ValidInt64{},
domain.TRANSFER_SANTIMPAY,

View File

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

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

View File

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

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 (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type VirtualGameService interface {
// AddProvider(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error)
RemoveProvider(ctx context.Context, providerID string) error
GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error)
ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error)
SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error)
// RemoveProvider(ctx context.Context, providerID string) error
// GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error)
// ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error)
// SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error)
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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