Merge branch 'production' of https://github.com/SamuelTariku/FortuneBet-Backend into production

This commit is contained in:
Kerod Fresenbet 2025-06-25 20:55:35 +00:00
commit 8b8ebee765
129 changed files with 11195 additions and 3201 deletions

View File

@ -18,7 +18,6 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/infrastructure"
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
"github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
@ -36,6 +35,8 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
@ -43,12 +44,12 @@ import (
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/report"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
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/veli"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor"
@ -56,7 +57,6 @@ import (
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/worker"
)
// @title FortuneBet API
@ -87,7 +87,7 @@ func main() {
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
domain.MongoDBLogger, err = mongoLogger.InitLogger()
domain.MongoDBLogger, err = mongoLogger.InitLogger(cfg)
if err != nil {
log.Fatalf("Logger initialization failed: %v", err)
}
@ -99,11 +99,11 @@ func main() {
v := customvalidator.NewCustomValidator(validator.New())
// Initialize services
settingSvc := settings.NewService(store)
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
userSvc := user.NewService(store, store, cfg)
eventSvc := event.New(cfg.Bet365Token, store)
oddsSvc := odds.New(store, cfg, logger)
ticketSvc := ticket.NewService(store)
notificationRepo := repository.NewNotificationRepository(store)
virtuaGamesRepo := repository.NewVirtualGameRepository(store)
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
@ -121,8 +121,9 @@ func main() {
branchSvc := branch.NewService(store)
companySvc := company.NewService(store)
leagueSvc := league.New(store)
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc)
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger)
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc)
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store)
recommendationRepo := repository.NewRecommendationRepository(store)
@ -130,7 +131,7 @@ func main() {
referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger)
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger)
veliService := veli.NewVeliPlayService(vitualGameRepo, *walletSvc, cfg, logger)
// veliService := veli.NewVeliPlayService(vitualGameRepo, *walletSvc, cfg, logger)
recommendationSvc := recommendation.NewService(recommendationRepo)
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
@ -138,6 +139,7 @@ func main() {
wallet.TransferStore(store),
*walletSvc,
user.UserStore(store),
cfg,
chapaClient,
)
@ -162,15 +164,19 @@ func main() {
logger,
)
// Initialize report worker with CSV exporter
csvExporter := infrastructure.CSVExporter{
ExportPath: cfg.ReportExportPath, // Make sure to add this to your config
}
go httpserver.SetupReportCronJobs(context.Background(), reportSvc)
reportWorker := worker.NewReportWorker(
reportSvc,
csvExporter,
)
bankRepository := repository.NewBankRepository(store)
instSvc := institutions.New(bankRepository)
// Initialize report worker with CSV exporter
// csvExporter := infrastructure.CSVExporter{
// ExportPath: cfg.ReportExportPath, // Make sure to add this to your config
// }
// reportWorker := worker.NewReportWorker(
// reportSvc,
// csvExporter,
// )
// Start cron jobs for automated reporting
@ -196,13 +202,39 @@ func main() {
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc)
httpserver.StartTicketCrons(*ticketSvc)
go httpserver.SetupReportCronJob(reportWorker)
// Fetch companies and branches for live wallet metrics update
ctx := context.Background()
companies := []domain.GetCompany{
{ID: 1, Name: "Company A", WalletBalance: 1000.0},
}
branches := []domain.BranchWallet{
{ID: 10, Name: "Branch Z", CompanyID: 1, Balance: 500.0},
}
notificationSvc.UpdateLiveWalletMetrics(ctx, companies, branches)
if err != nil {
log.Println("Failed to update live metrics:", err)
} else {
log.Println("Live metrics broadcasted successfully")
}
issueReportingRepo := repository.NewReportedIssueRepository(store)
issueReportingSvc := issuereporting.New(issueReportingRepo)
// go httpserver.SetupReportCronJob(reportWorker)
// Initialize and start HTTP server
app := httpserver.NewApp(
issueReportingSvc,
instSvc,
currSvc,
cfg.Port,
v,
settingSvc,
authSvc,
logger,
jwtutil.JwtConfig{
@ -225,10 +257,11 @@ func main() {
referalSvc,
virtualGameSvc,
aleaService,
veliService,
// veliService,
recommendationSvc,
resultSvc,
cfg,
domain.MongoDBLogger,
)
logger.Info("Starting server", "port", cfg.Port)
@ -236,4 +269,5 @@ func main() {
logger.Error("Failed to start server", "error", err)
os.Exit(1)
}
select {}
}

17
db.sql Normal file
View File

@ -0,0 +1,17 @@
psql (16.8)
Type "help" for help.
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=# 
gh=#

View File

@ -77,3 +77,6 @@ DROP TABLE IF EXISTS otps;
DROP TABLE IF EXISTS odds;
DROP TABLE IF EXISTS events;
DROP TABLE IF EXISTS leagues;
DROP TABLE IF EXISTS teams;
DROP TABLE IF EXISTS settings;
-- DELETE FROM wallet_transfer;

View File

@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS bets (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_shop_bet BOOLEAN NOT NULL,
outcomes_hash TEXT NOT NULL,
UNIQUE(cashout_id),
CHECK (
user_id IS NOT NULL
@ -111,6 +112,23 @@ CREATE TABLE IF NOT EXISTS ticket_outcomes (
status INT NOT NULL DEFAULT 0,
expires TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS banks (
id BIGSERIAL PRIMARY KEY,
slug VARCHAR(255) NOT NULL UNIQUE,
swift VARCHAR(20) NOT NULL,
name VARCHAR(255) NOT NULL,
acct_length INT NOT NULL,
country_id INT NOT NULL,
is_mobilemoney INT, -- nullable integer (0 or 1)
is_active INT NOT NULL, -- 0 or 1
is_rtgs INT NOT NULL, -- 0 or 1
active INT NOT NULL, -- 0 or 1
is_24hrs INT, -- nullable integer (0 or 1)
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
currency VARCHAR(10) NOT NULL,
bank_logo TEXT -- URL or base64 string
);
CREATE TABLE IF NOT EXISTS wallets (
id BIGSERIAL PRIMARY KEY,
balance BIGINT NOT NULL DEFAULT 0,
@ -138,7 +156,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer (
sender_wallet_id BIGINT,
cashier_id BIGINT,
verified BOOLEAN DEFAULT false,
reference_number VARCHAR(255),
reference_number VARCHAR(255) NOT NULL,
status VARCHAR(255),
payment_method VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -249,10 +267,12 @@ CREATE TABLE companies (
CREATE TABLE leagues (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL,
img TEXT,
country_code TEXT,
bet365_id INT,
sport_id INT NOT NULL,
is_active BOOLEAN DEFAULT true
is_active BOOLEAN DEFAULT true,
is_featured BOOLEAN DEFAULT false
);
CREATE TABLE teams (
id TEXT PRIMARY KEY,
@ -261,6 +281,12 @@ CREATE TABLE teams (
bet365_id INT,
logo_url TEXT
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Views
CREATE VIEW companies_details AS
SELECT companies.*,
@ -273,12 +299,12 @@ FROM companies
JOIN wallets ON wallets.id = companies.wallet_id
JOIN users ON users.id = companies.admin_id;
;
CREATE VIEW branch_details AS
SELECT branches.*,
CONCAT(users.first_name, ' ', users.last_name) AS manager_name,
users.phone_number AS manager_phone_number,
wallets.balance
wallets.balance,
wallets.is_active AS wallet_is_active
FROM branches
LEFT JOIN users ON branches.branch_manager_id = users.id
LEFT JOin wallets ON wallets.id = branches.wallet_id;
@ -299,42 +325,60 @@ SELECT tickets.*,
FROM tickets
LEFT JOIN ticket_outcomes ON tickets.id = ticket_outcomes.ticket_id
GROUP BY tickets.id;
CREATE VIEW customer_wallet_details AS
SELECT cw.id,
cw.customer_id,
rw.id AS regular_id,
rw.balance AS regular_balance,
sw.id AS static_id,
sw.balance AS static_balance,
rw.is_active as regular_is_active,
sw.is_active as static_is_active,
rw.updated_at as regular_updated_at,
sw.updated_at as static_updated_at,
cw.created_at,
users.first_name,
users.last_name,
users.phone_number
FROM customer_wallets cw
JOIN wallets rw ON cw.regular_wallet_id = rw.id
JOIN wallets sw ON cw.static_wallet_id = sw.id
JOIN users ON users.id = cw.customer_id;
-- Foreign Keys
ALTER TABLE users
ADD CONSTRAINT unique_email UNIQUE (email),
ADD CONSTRAINT unique_email UNIQUE (email),
ADD CONSTRAINT unique_phone_number UNIQUE (phone_number);
ALTER TABLE refresh_tokens
ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id);
ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE bets
ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id);
ALTER TABLE wallets
ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB';
ALTER TABLE customer_wallets
ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id),
ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id),
ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_customer_wallets_static_wallet FOREIGN KEY (static_wallet_id) REFERENCES wallets(id);
ALTER TABLE wallet_transfer
ADD CONSTRAINT fk_wallet_transfer_receiver_wallet FOREIGN KEY (receiver_wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_wallet_transfer_receiver_wallet FOREIGN KEY (receiver_wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_wallet_transfer_sender_wallet FOREIGN KEY (sender_wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_wallet_transfer_cashier FOREIGN KEY (cashier_id) REFERENCES users(id);
ALTER TABLE transactions
ADD CONSTRAINT fk_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches(id),
ADD CONSTRAINT fk_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches(id),
ADD CONSTRAINT fk_transactions_cashiers FOREIGN KEY (cashier_id) REFERENCES users(id),
ADD CONSTRAINT fk_transactions_bets FOREIGN KEY (bet_id) REFERENCES bets(id);
ALTER TABLE branches
ADD CONSTRAINT fk_branches_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_branches_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_branches_manager FOREIGN KEY (branch_manager_id) REFERENCES users(id);
ALTER TABLE branch_operations
ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_operations_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE branch_cashiers
ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE companies
ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id),
ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id),
ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE;
----------------------------------------------seed data-------------------------------------------------------------
-------------------------------------- DO NOT USE IN PRODUCTION-------------------------------------------------

View File

@ -30,6 +30,9 @@ CREATE TABLE virtual_game_transactions (
id BIGSERIAL PRIMARY KEY,
session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id),
user_id BIGINT NOT NULL REFERENCES users(id),
company_id BIGINT,
provider VARCHAR(100),
game_id VARCHAR(100),
wallet_id BIGINT NOT NULL REFERENCES wallets(id),
transaction_type VARCHAR(20) NOT NULL,
amount BIGINT NOT NULL,
@ -40,6 +43,41 @@ CREATE TABLE virtual_game_transactions (
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE virtual_game_histories (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(100), -- nullable
user_id BIGINT NOT NULL,
company_id BIGINT,
provider VARCHAR(100),
wallet_id BIGINT, -- nullable
game_id BIGINT, -- nullable
transaction_type VARCHAR(20) NOT NULL, -- e.g., BET, WIN, CANCEL
amount BIGINT NOT NULL, -- in cents or smallest currency unit
currency VARCHAR(10) NOT NULL,
external_transaction_id VARCHAR(100) NOT NULL,
reference_transaction_id VARCHAR(100), -- nullable, for cancel/refund
status VARCHAR(20) NOT NULL DEFAULT 'COMPLETED', -- transaction status
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS favorite_games (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
game_id BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Optional: Indexes for performance
CREATE INDEX idx_virtual_game_user_id ON virtual_game_histories(user_id);
CREATE INDEX idx_virtual_game_transaction_type ON virtual_game_histories(transaction_type);
CREATE INDEX idx_virtual_game_game_id ON virtual_game_histories(game_id);
CREATE INDEX idx_virtual_game_external_transaction_id ON virtual_game_histories(external_transaction_id);
CREATE INDEX idx_virtual_game_sessions_user_id ON virtual_game_sessions(user_id);
CREATE INDEX idx_virtual_game_transactions_session_id ON virtual_game_transactions(session_id);
CREATE INDEX idx_virtual_game_transactions_user_id ON virtual_game_transactions(user_id);
ALTER TABLE favorite_games
ADD CONSTRAINT unique_user_game_favorite UNIQUE (user_id, game_id);

View File

@ -1,28 +1,28 @@
-- CREATE TABLE virtual_games (
-- id BIGSERIAL PRIMARY KEY,
-- name VARCHAR(255) NOT NULL,
-- provider VARCHAR(100) NOT NULL,
-- category VARCHAR(100) NOT NULL,
-- min_bet DECIMAL(15,2) NOT NULL,
-- max_bet DECIMAL(15,2) NOT NULL,
-- volatility VARCHAR(50) NOT NULL,
-- rtp DECIMAL(5,2) NOT NULL,
-- is_featured BOOLEAN DEFAULT false,
-- popularity_score INTEGER DEFAULT 0,
-- thumbnail_url TEXT,
-- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
-- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
-- );
CREATE TABLE IF NOT EXISTS virtual_games (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
provider VARCHAR(100) NOT NULL,
category VARCHAR(100) NOT NULL,
min_bet DECIMAL(15,2) NOT NULL,
max_bet DECIMAL(15,2) NOT NULL,
volatility VARCHAR(50) NOT NULL,
rtp DECIMAL(5,2) NOT NULL,
is_featured BOOLEAN DEFAULT false,
popularity_score INTEGER DEFAULT 0,
thumbnail_url TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_game_interactions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
game_id BIGINT NOT NULL REFERENCES virtual_games(id),
interaction_type VARCHAR(50) NOT NULL, -- 'view', 'play', 'bet', 'favorite'
amount DECIMAL(15,2),
interaction_type VARCHAR(50) NOT NULL,
-- 'view', 'play', 'bet', 'favorite'
amount DECIMAL(15, 2),
duration_seconds INTEGER,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_user_game_interactions_user ON user_game_interactions(user_id);
CREATE INDEX idx_user_game_interactions_game ON user_game_interactions(game_id);

View File

@ -0,0 +1,17 @@
-- Settings Initial Data
INSERT INTO settings (key, value)
VALUES ('max_number_of_outcomes', '30') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;
INSERT INTO settings (key, value)
VALUES ('bet_amount_limit', '100000') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;
INSERT INTO settings (key, value)
VALUES ('daily_ticket_limit', '50') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;
INSERT INTO settings (key, value)
VALUES ('total_winnings_limit', '1000000') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;

View File

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

View File

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS reported_issues (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL,
subject TEXT NOT NULL,
description TEXT NOT NULL,
issue_type TEXT NOT NULL, -- e.g., "deposit", "withdrawal", "bet", "technical"
status TEXT NOT NULL DEFAULT 'pending', -- pending, in_progress, resolved, rejected
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -9,9 +9,10 @@ INSERT INTO bets (
user_id,
is_shop_bet,
cashout_id,
company_id
company_id,
outcomes_hash
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *;
-- name: CreateBetOutcome :copyfrom
INSERT INTO bet_outcomes (
@ -48,16 +49,33 @@ VALUES (
SELECT *
FROM bet_with_outcomes
wHERE (
branch_id = $1
OR $1 IS NULL
branch_id = sqlc.narg('branch_id')
OR sqlc.narg('branch_id') IS NULL
)
AND (
company_id = $2
OR $2 IS NULL
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
AND (
user_id = $3
OR $3 IS NULL
user_id = sqlc.narg('user_id')
OR sqlc.narg('user_id') IS NULL
)
AND (
is_shop_bet = sqlc.narg('is_shop_bet')
OR sqlc.narg('is_shop_bet') IS NULL
)
AND (
full_name ILIKE '%' || sqlc.narg('query') || '%'
OR phone_number ILIKE '%' || sqlc.narg('query') || '%'
OR sqlc.narg('query') IS NULL
)
AND (
created_at > sqlc.narg('created_before')
OR sqlc.narg('created_before') IS NULL
)
AND (
created_at < sqlc.narg('created_after')
OR sqlc.narg('created_after') IS NULL
);
-- name: GetBetByID :one
SELECT *
@ -78,11 +96,22 @@ WHERE user_id = $1;
-- name: GetBetOutcomeByEventID :many
SELECT *
FROM bet_outcomes
WHERE event_id = $1;
WHERE (event_id = $1)
AND (
status = sqlc.narg('filter_status')
OR sqlc.narg('filter_status') IS NULL
OR status = sqlc.narg('filter_status_2')
OR sqlc.narg('filter_status_2') IS NULL
);
-- name: GetBetOutcomeByBetID :many
SELECT *
FROM bet_outcomes
WHERE bet_id = $1;
-- name: GetBetCount :one
SELECT COUNT(*)
FROM bets
where user_id = $1
AND outcomes_hash = $2;
-- name: UpdateCashOut :exec
UPDATE bets
SET cashed_out = $2,
@ -93,6 +122,16 @@ UPDATE bet_outcomes
SET status = $1
WHERE id = $2
RETURNING *;
-- name: UpdateBetOutcomeStatusByBetID :one
UPDATE bet_outcomes
SET status = $1
WHERE bet_id = $2
RETURNING *;
-- name: UpdateBetOutcomeStatusForEvent :many
UPDATE bet_outcomes
SEt status = $1
WHERE event_id = $2
RETURNING *;
-- name: UpdateStatus :exec
UPDATE bets
SET status = $1,

View File

@ -23,7 +23,32 @@ VALUES ($1, $2)
RETURNING *;
-- name: GetAllBranches :many
SELECT *
FROM branch_details;
FROM branch_details
WHERE (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
AND (
is_active = sqlc.narg('is_active')
OR sqlc.narg('is_active') IS NULL
)
AND (
branch_manager_id = sqlc.narg('branch_manager_id')
OR sqlc.narg('branch_manager_id') IS NULL
)
AND (
name ILIKE '%' || sqlc.narg('query') || '%'
OR location ILIKE '%' || sqlc.narg('query') || '%'
OR sqlc.narg('query') IS NULL
)
AND (
created_at > sqlc.narg('created_before')
OR sqlc.narg('created_before') IS NULL
)
AND (
created_at < sqlc.narg('created_after')
OR sqlc.narg('created_after') IS NULL
);
-- name: GetBranchByID :one
SELECT *
FROM branch_details
@ -61,7 +86,8 @@ SET name = COALESCE(sqlc.narg(name), name),
location = COALESCE(sqlc.narg(location), location),
branch_manager_id = COALESCE(sqlc.narg(branch_manager_id), branch_manager_id),
company_id = COALESCE(sqlc.narg(company_id), company_id),
is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned)
is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned),
is_active = COALESCE(sqlc.narg(is_active), is_active)
WHERE id = $1
RETURNING *;
-- name: DeleteBranch :exec

60
db/query/institutions.sql Normal file
View File

@ -0,0 +1,60 @@
-- name: CreateBank :one
INSERT INTO banks (
slug,
swift,
name,
acct_length,
country_id,
is_mobilemoney,
is_active,
is_rtgs,
active,
is_24hrs,
created_at,
updated_at,
currency,
bank_logo
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, $11, $12
)
RETURNING *;
-- name: GetBankByID :one
SELECT *
FROM banks
WHERE id = $1;
-- name: GetAllBanks :many
SELECT *
FROM banks
WHERE (
country_id = sqlc.narg('country_id')
OR sqlc.narg('country_id') IS NULL
)
AND (
is_active = sqlc.narg('is_active')
OR sqlc.narg('is_active') IS NULL
);
-- name: UpdateBank :one
UPDATE banks
SET slug = COALESCE(sqlc.narg(slug), slug),
swift = COALESCE(sqlc.narg(swift), swift),
name = COALESCE(sqlc.narg(name), name),
acct_length = COALESCE(sqlc.narg(acct_length), acct_length),
country_id = COALESCE(sqlc.narg(country_id), country_id),
is_mobilemoney = COALESCE(sqlc.narg(is_mobilemoney), is_mobilemoney),
is_active = COALESCE(sqlc.narg(is_active), is_active),
is_rtgs = COALESCE(sqlc.narg(is_rtgs), is_rtgs),
active = COALESCE(sqlc.narg(active), active),
is_24hrs = COALESCE(sqlc.narg(is_24hrs), is_24hrs),
updated_at = CURRENT_TIMESTAMP,
currency = COALESCE(sqlc.narg(currency), currency),
bank_logo = COALESCE(sqlc.narg(bank_logo), bank_logo)
WHERE id = $1
RETURNING *;
-- name: DeleteBank :exec
DELETE FROM banks
WHERE id = $1;

View File

@ -0,0 +1,32 @@
-- name: CreateReportedIssue :one
INSERT INTO reported_issues (
customer_id, subject, description, issue_type, metadata
) VALUES (
$1, $2, $3, $4, $5
)
RETURNING *;
-- name: ListReportedIssues :many
SELECT * FROM reported_issues
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;
-- name: ListReportedIssuesByCustomer :many
SELECT * FROM reported_issues
WHERE customer_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
-- name: CountReportedIssues :one
SELECT COUNT(*) FROM reported_issues;
-- name: CountReportedIssuesByCustomer :one
SELECT COUNT(*) FROM reported_issues WHERE customer_id = $1;
-- name: UpdateReportedIssueStatus :exec
UPDATE reported_issues
SET status = $2, updated_at = NOW()
WHERE id = $1;
-- name: DeleteReportedIssue :exec
DELETE FROM reported_issues WHERE id = $1;

View File

@ -5,14 +5,16 @@ INSERT INTO leagues (
country_code,
bet365_id,
sport_id,
is_active
is_active,
is_featured
)
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO
VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO
UPDATE
SET name = EXCLUDED.name,
country_code = EXCLUDED.country_code,
bet365_id = EXCLUDED.bet365_id,
is_active = EXCLUDED.is_active,
is_featured = EXCLUDED.is_featured,
sport_id = EXCLUDED.sport_id;
-- name: GetAllLeagues :many
SELECT id,
@ -20,6 +22,7 @@ SELECT id,
country_code,
bet365_id,
is_active,
is_featured,
sport_id
FROM leagues
WHERE (
@ -34,7 +37,21 @@ WHERE (
is_active = sqlc.narg('is_active')
OR sqlc.narg('is_active') IS NULL
)
AND (
is_featured = sqlc.narg('is_featured')
OR sqlc.narg('is_featured') IS NULL
)
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetFeaturedLeagues :many
SELECT id,
name,
country_code,
bet365_id,
is_active,
is_featured,
sport_id
FROM leagues
WHERE is_featured = true;
-- name: CheckLeagueSupport :one
SELECT EXISTS(
SELECT 1
@ -48,6 +65,7 @@ SET name = COALESCE(sqlc.narg('name'), name),
country_code = COALESCE(sqlc.narg('country_code'), country_code),
bet365_id = COALESCE(sqlc.narg('bet365_id'), bet365_id),
is_active = COALESCE(sqlc.narg('is_active'), is_active),
is_featured = COALESCE(sqlc.narg('is_featured'), is_featured),
sport_id = COALESCE(sqlc.narg('sport_id'), sport_id)
WHERE id = $1;
-- name: UpdateLeagueByBet365ID :exec
@ -56,6 +74,7 @@ SET name = COALESCE(sqlc.narg('name'), name),
id = COALESCE(sqlc.narg('id'), id),
country_code = COALESCE(sqlc.narg('country_code'), country_code),
is_active = COALESCE(sqlc.narg('is_active'), is_active),
is_featured = COALESCE(sqlc.narg('is_featured'), is_featured),
sport_id = COALESCE(sqlc.narg('sport_id'), sport_id)
WHERE bet365_id = $1;
-- name: SetLeagueActive :exec

44
db/query/report.sql Normal file
View File

@ -0,0 +1,44 @@
-- name: GetTotalBetsMadeInRange :one
SELECT COUNT(*) AS total_bets
FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to');
-- name: GetTotalCashMadeInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_made
FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to');
-- name: GetTotalCashOutInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_out
FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
AND cashed_out = true;
-- name: GetTotalCashBacksInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_backs
FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
AND status = 5;
-- name: GetCompanyWiseReport :many
SELECT
b.company_id,
c.name AS company_name,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out,
COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs
FROM bets b
JOIN companies c ON b.company_id = c.id
WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
GROUP BY b.company_id, c.name;
-- name: GetBranchWiseReport :many
SELECT
b.branch_id,
br.name AS branch_name,
br.company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out,
COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs
FROM bets b
JOIN branches br ON b.branch_id = br.id
WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
GROUP BY b.branch_id, br.name, br.company_id;

13
db/query/settings.sql Normal file
View File

@ -0,0 +1,13 @@
-- name: GetSettings :many
SELECT *
FROM settings;
-- name: GetSetting :one
SELECT *
FROM settings
WHERE key = $1;
-- name: SaveSetting :one
INSERT INTO settings (key, value, updated_at)
VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value
RETURNING *;

View File

@ -59,3 +59,7 @@ where created_at < now() - interval '1 day';
-- name: DeleteTicketOutcome :exec
Delete from ticket_outcomes
where ticket_id = $1;
-- name: GetAllTicketsInRange :one
SELECT COUNT(*) as total_tickets, COALESCE(SUM(amount), 0) as total_amount
FROM tickets
WHERE created_at BETWEEN $1 AND $2;

View File

@ -39,3 +39,12 @@ UPDATE wallet_transfer
SET status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: GetWalletTransactionsInRange :many
SELECT type, COUNT(*) as count, SUM(amount) as total_amount
FROM wallet_transfer
WHERE created_at BETWEEN $1 AND $2
GROUP BY type;

View File

@ -4,30 +4,85 @@ INSERT INTO virtual_game_sessions (
) VALUES (
$1, $2, $3, $4, $5, $6
) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at;
-- name: GetVirtualGameSessionByToken :one
SELECT id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_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, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status
session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at;
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
) RETURNING id, session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at;
-- name: CreateVirtualGameHistory :one
INSERT INTO virtual_game_histories (
session_id,
user_id,
company_id,
provider,
wallet_id,
game_id,
transaction_type,
amount,
currency,
external_transaction_id,
reference_transaction_id,
status
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
) RETURNING
id,
session_id,
user_id,
company_id,
provider,
wallet_id,
game_id,
transaction_type,
amount,
currency,
external_transaction_id,
reference_transaction_id,
status,
created_at,
updated_at;
-- name: GetVirtualGameTransactionByExternalID :one
SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
FROM virtual_game_transactions
WHERE external_transaction_id = $1;
-- name: UpdateVirtualGameTransactionStatus :exec
UPDATE virtual_game_transactions
SET status = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: GetVirtualGameSummaryInRange :many
SELECT
c.name AS company_name,
vg.name AS game_name,
COUNT(vgt.id) AS number_of_bets,
COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum
FROM virtual_game_transactions vgt
JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
JOIN virtual_games vg ON vgs.game_id = vg.id
JOIN companies c ON vgt.company_id = c.id
WHERE vgt.transaction_type = 'BET'
AND vgt.created_at BETWEEN $1 AND $2
GROUP BY c.name, vg.name;
-- name: AddFavoriteGame :exec
INSERT INTO favorite_games (
user_id,
game_id,
created_at
) VALUES ($1, $2, NOW())
ON CONFLICT (user_id, game_id) DO NOTHING;
-- name: RemoveFavoriteGame :exec
DELETE FROM favorite_games
WHERE user_id = $1 AND game_id = $2;
-- name: ListFavoriteGames :many
SELECT game_id
FROM favorite_games
WHERE user_id = $1;

View File

@ -26,20 +26,13 @@ WHERE id = $1;
SELECT *
FROM wallets
WHERE user_id = $1;
-- name: GetAllCustomerWallet :many
SELECT *
FROM customer_wallet_details;
-- name: GetCustomerWallet :one
SELECT cw.id,
cw.customer_id,
rw.id AS regular_id,
rw.balance AS regular_balance,
sw.id AS static_id,
sw.balance AS static_balance,
rw.updated_at as regular_updated_at,
sw.updated_at as static_updated_at,
cw.created_at
FROM customer_wallets cw
JOIN wallets rw ON cw.regular_wallet_id = rw.id
JOIN wallets sw ON cw.static_wallet_id = sw.id
WHERE cw.customer_id = $1;
SELECT *
FROM customer_wallet_details
WHERE customer_id = $1;
-- name: GetAllBranchWallets :many
SELECT wallets.id,
wallets.balance,
@ -63,3 +56,13 @@ UPDATE wallets
SET is_active = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: GetCompanyByWalletID :one
SELECT id, name, admin_id, wallet_id
FROM companies
WHERE wallet_id = $1
LIMIT 1;
-- name: GetBranchByWalletID :one
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
FROM branches
WHERE wallet_id = $1
LIMIT 1;

View File

@ -1,3 +1,5 @@
version: "3.8"
services:
postgres:
image: postgres:16-alpine
@ -54,6 +56,18 @@ services:
networks:
- app
redis:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- app
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
app:
build:
context: .
@ -64,14 +78,19 @@ services:
environment:
- DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable
- MONGO_URI=mongodb://root:secret@mongo:27017
- REDIS_ADDR=redis:6379
depends_on:
migrate:
condition: service_completed_successfully
mongo:
condition: service_healthy
redis:
condition: service_healthy
networks:
- app
command: ["/app/bin/web"]
volumes:
- "C:/Users/User/Desktop:/host-desktop"
test:
build:

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

@ -22,23 +22,25 @@ INSERT INTO bets (
user_id,
is_shop_bet,
cashout_id,
company_id
company_id,
outcomes_hash
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash
`
type CreateBetParams struct {
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
IsShopBet bool `json:"is_shop_bet"`
CashoutID string `json:"cashout_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
IsShopBet bool `json:"is_shop_bet"`
CashoutID string `json:"cashout_id"`
CompanyID pgtype.Int8 `json:"company_id"`
OutcomesHash string `json:"outcomes_hash"`
}
func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, error) {
@ -53,6 +55,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro
arg.IsShopBet,
arg.CashoutID,
arg.CompanyID,
arg.OutcomesHash,
)
var i Bet
err := row.Scan(
@ -70,6 +73,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
)
return i, err
}
@ -111,7 +115,7 @@ func (q *Queries) DeleteBetOutcome(ctx context.Context, betID int64) error {
}
const GetAllBets = `-- name: GetAllBets :many
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes
FROM bet_with_outcomes
wHERE (
branch_id = $1
@ -125,16 +129,45 @@ wHERE (
user_id = $3
OR $3 IS NULL
)
AND (
is_shop_bet = $4
OR $4 IS NULL
)
AND (
full_name ILIKE '%' || $5 || '%'
OR phone_number ILIKE '%' || $5 || '%'
OR $5 IS NULL
)
AND (
created_at > $6
OR $6 IS NULL
)
AND (
created_at < $7
OR $7 IS NULL
)
`
type GetAllBetsParams struct {
BranchID pgtype.Int8 `json:"branch_id"`
CompanyID pgtype.Int8 `json:"company_id"`
UserID pgtype.Int8 `json:"user_id"`
BranchID pgtype.Int8 `json:"branch_id"`
CompanyID pgtype.Int8 `json:"company_id"`
UserID pgtype.Int8 `json:"user_id"`
IsShopBet pgtype.Bool `json:"is_shop_bet"`
Query pgtype.Text `json:"query"`
CreatedBefore pgtype.Timestamp `json:"created_before"`
CreatedAfter pgtype.Timestamp `json:"created_after"`
}
func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWithOutcome, error) {
rows, err := q.db.Query(ctx, GetAllBets, arg.BranchID, arg.CompanyID, arg.UserID)
rows, err := q.db.Query(ctx, GetAllBets,
arg.BranchID,
arg.CompanyID,
arg.UserID,
arg.IsShopBet,
arg.Query,
arg.CreatedBefore,
arg.CreatedAfter,
)
if err != nil {
return nil, err
}
@ -157,6 +190,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.Outcomes,
); err != nil {
return nil, err
@ -170,7 +204,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi
}
const GetBetByBranchID = `-- name: GetBetByBranchID :many
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes
FROM bet_with_outcomes
WHERE branch_id = $1
`
@ -199,6 +233,7 @@ func (q *Queries) GetBetByBranchID(ctx context.Context, branchID pgtype.Int8) ([
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.Outcomes,
); err != nil {
return nil, err
@ -212,7 +247,7 @@ func (q *Queries) GetBetByBranchID(ctx context.Context, branchID pgtype.Int8) ([
}
const GetBetByCashoutID = `-- name: GetBetByCashoutID :one
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes
FROM bet_with_outcomes
WHERE cashout_id = $1
`
@ -235,13 +270,14 @@ func (q *Queries) GetBetByCashoutID(ctx context.Context, cashoutID string) (BetW
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.Outcomes,
)
return i, err
}
const GetBetByID = `-- name: GetBetByID :one
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes
FROM bet_with_outcomes
WHERE id = $1
`
@ -264,13 +300,14 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.Outcomes,
)
return i, err
}
const GetBetByUserID = `-- name: GetBetByUserID :many
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes
FROM bet_with_outcomes
WHERE user_id = $1
`
@ -299,6 +336,7 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID pgtype.Int8) ([]Bet
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.Outcomes,
); err != nil {
return nil, err
@ -311,6 +349,25 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID pgtype.Int8) ([]Bet
return items, nil
}
const GetBetCount = `-- name: GetBetCount :one
SELECT COUNT(*)
FROM bets
where user_id = $1
AND outcomes_hash = $2
`
type GetBetCountParams struct {
UserID pgtype.Int8 `json:"user_id"`
OutcomesHash string `json:"outcomes_hash"`
}
func (q *Queries) GetBetCount(ctx context.Context, arg GetBetCountParams) (int64, error) {
row := q.db.QueryRow(ctx, GetBetCount, arg.UserID, arg.OutcomesHash)
var count int64
err := row.Scan(&count)
return count, err
}
const GetBetOutcomeByBetID = `-- name: GetBetOutcomeByBetID :many
SELECT id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
FROM bet_outcomes
@ -356,11 +413,23 @@ func (q *Queries) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]BetO
const GetBetOutcomeByEventID = `-- name: GetBetOutcomeByEventID :many
SELECT id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
FROM bet_outcomes
WHERE event_id = $1
WHERE (event_id = $1)
AND (
status = $2
OR $2 IS NULL
OR status = $3
OR $3 IS NULL
)
`
func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]BetOutcome, error) {
rows, err := q.db.Query(ctx, GetBetOutcomeByEventID, eventID)
type GetBetOutcomeByEventIDParams struct {
EventID int64 `json:"event_id"`
FilterStatus pgtype.Int4 `json:"filter_status"`
FilterStatus2 pgtype.Int4 `json:"filter_status_2"`
}
func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, arg GetBetOutcomeByEventIDParams) ([]BetOutcome, error) {
rows, err := q.db.Query(ctx, GetBetOutcomeByEventID, arg.EventID, arg.FilterStatus, arg.FilterStatus2)
if err != nil {
return nil, err
}
@ -430,6 +499,89 @@ func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutco
return i, err
}
const UpdateBetOutcomeStatusByBetID = `-- name: UpdateBetOutcomeStatusByBetID :one
UPDATE bet_outcomes
SET status = $1
WHERE bet_id = $2
RETURNING id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
`
type UpdateBetOutcomeStatusByBetIDParams struct {
Status int32 `json:"status"`
BetID int64 `json:"bet_id"`
}
func (q *Queries) UpdateBetOutcomeStatusByBetID(ctx context.Context, arg UpdateBetOutcomeStatusByBetIDParams) (BetOutcome, error) {
row := q.db.QueryRow(ctx, UpdateBetOutcomeStatusByBetID, arg.Status, arg.BetID)
var i BetOutcome
err := row.Scan(
&i.ID,
&i.BetID,
&i.SportID,
&i.EventID,
&i.OddID,
&i.HomeTeamName,
&i.AwayTeamName,
&i.MarketID,
&i.MarketName,
&i.Odd,
&i.OddName,
&i.OddHeader,
&i.OddHandicap,
&i.Status,
&i.Expires,
)
return i, err
}
const UpdateBetOutcomeStatusForEvent = `-- name: UpdateBetOutcomeStatusForEvent :many
UPDATE bet_outcomes
SEt status = $1
WHERE event_id = $2
RETURNING id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
`
type UpdateBetOutcomeStatusForEventParams struct {
Status int32 `json:"status"`
EventID int64 `json:"event_id"`
}
func (q *Queries) UpdateBetOutcomeStatusForEvent(ctx context.Context, arg UpdateBetOutcomeStatusForEventParams) ([]BetOutcome, error) {
rows, err := q.db.Query(ctx, UpdateBetOutcomeStatusForEvent, arg.Status, arg.EventID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BetOutcome
for rows.Next() {
var i BetOutcome
if err := rows.Scan(
&i.ID,
&i.BetID,
&i.SportID,
&i.EventID,
&i.OddID,
&i.HomeTeamName,
&i.AwayTeamName,
&i.MarketID,
&i.MarketName,
&i.Odd,
&i.OddName,
&i.OddHeader,
&i.OddHandicap,
&i.Status,
&i.Expires,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateCashOut = `-- name: UpdateCashOut :exec
UPDATE bets
SET cashed_out = $2,

View File

@ -155,12 +155,53 @@ func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOpe
}
const GetAllBranches = `-- name: GetAllBranches :many
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active
FROM branch_details
WHERE (
company_id = $1
OR $1 IS NULL
)
AND (
is_active = $2
OR $2 IS NULL
)
AND (
branch_manager_id = $3
OR $3 IS NULL
)
AND (
name ILIKE '%' || $4 || '%'
OR location ILIKE '%' || $4 || '%'
OR $4 IS NULL
)
AND (
created_at > $5
OR $5 IS NULL
)
AND (
created_at < $6
OR $6 IS NULL
)
`
func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) {
rows, err := q.db.Query(ctx, GetAllBranches)
type GetAllBranchesParams struct {
CompanyID pgtype.Int8 `json:"company_id"`
IsActive pgtype.Bool `json:"is_active"`
BranchManagerID pgtype.Int8 `json:"branch_manager_id"`
Query pgtype.Text `json:"query"`
CreatedBefore pgtype.Timestamp `json:"created_before"`
CreatedAfter pgtype.Timestamp `json:"created_after"`
}
func (q *Queries) GetAllBranches(ctx context.Context, arg GetAllBranchesParams) ([]BranchDetail, error) {
rows, err := q.db.Query(ctx, GetAllBranches,
arg.CompanyID,
arg.IsActive,
arg.BranchManagerID,
arg.Query,
arg.CreatedBefore,
arg.CreatedAfter,
)
if err != nil {
return nil, err
}
@ -182,6 +223,7 @@ func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) {
&i.ManagerName,
&i.ManagerPhoneNumber,
&i.Balance,
&i.WalletIsActive,
); err != nil {
return nil, err
}
@ -244,7 +286,7 @@ func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch,
}
const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active
FROM branch_details
WHERE company_id = $1
`
@ -272,6 +314,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
&i.ManagerName,
&i.ManagerPhoneNumber,
&i.Balance,
&i.WalletIsActive,
); err != nil {
return nil, err
}
@ -284,7 +327,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
}
const GetBranchByID = `-- name: GetBranchByID :one
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active
FROM branch_details
WHERE id = $1
`
@ -306,12 +349,13 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er
&i.ManagerName,
&i.ManagerPhoneNumber,
&i.Balance,
&i.WalletIsActive,
)
return i, err
}
const GetBranchByManagerID = `-- name: GetBranchByManagerID :many
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active
FROM branch_details
WHERE branch_manager_id = $1
`
@ -339,6 +383,7 @@ func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int6
&i.ManagerName,
&i.ManagerPhoneNumber,
&i.Balance,
&i.WalletIsActive,
); err != nil {
return nil, err
}
@ -398,7 +443,7 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge
}
const SearchBranchByName = `-- name: SearchBranchByName :many
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active
FROM branch_details
WHERE name ILIKE '%' || $1 || '%'
`
@ -426,6 +471,7 @@ func (q *Queries) SearchBranchByName(ctx context.Context, dollar_1 pgtype.Text)
&i.ManagerName,
&i.ManagerPhoneNumber,
&i.Balance,
&i.WalletIsActive,
); err != nil {
return nil, err
}
@ -443,7 +489,8 @@ SET name = COALESCE($2, name),
location = COALESCE($3, location),
branch_manager_id = COALESCE($4, branch_manager_id),
company_id = COALESCE($5, company_id),
is_self_owned = COALESCE($6, is_self_owned)
is_self_owned = COALESCE($6, is_self_owned),
is_active = COALESCE($7, is_active)
WHERE id = $1
RETURNING id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
`
@ -455,6 +502,7 @@ type UpdateBranchParams struct {
BranchManagerID pgtype.Int8 `json:"branch_manager_id"`
CompanyID pgtype.Int8 `json:"company_id"`
IsSelfOwned pgtype.Bool `json:"is_self_owned"`
IsActive pgtype.Bool `json:"is_active"`
}
func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Branch, error) {
@ -465,6 +513,7 @@ func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Bra
arg.BranchManagerID,
arg.CompanyID,
arg.IsSelfOwned,
arg.IsActive,
)
var i Branch
err := row.Scan(

251
gen/db/institutions.sql.go Normal file
View File

@ -0,0 +1,251 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: institutions.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateBank = `-- name: CreateBank :one
INSERT INTO banks (
slug,
swift,
name,
acct_length,
country_id,
is_mobilemoney,
is_active,
is_rtgs,
active,
is_24hrs,
created_at,
updated_at,
currency,
bank_logo
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, $11, $12
)
RETURNING id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo
`
type CreateBankParams struct {
Slug string `json:"slug"`
Swift string `json:"swift"`
Name string `json:"name"`
AcctLength int32 `json:"acct_length"`
CountryID int32 `json:"country_id"`
IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"`
IsActive int32 `json:"is_active"`
IsRtgs int32 `json:"is_rtgs"`
Active int32 `json:"active"`
Is24hrs pgtype.Int4 `json:"is_24hrs"`
Currency string `json:"currency"`
BankLogo pgtype.Text `json:"bank_logo"`
}
func (q *Queries) CreateBank(ctx context.Context, arg CreateBankParams) (Bank, error) {
row := q.db.QueryRow(ctx, CreateBank,
arg.Slug,
arg.Swift,
arg.Name,
arg.AcctLength,
arg.CountryID,
arg.IsMobilemoney,
arg.IsActive,
arg.IsRtgs,
arg.Active,
arg.Is24hrs,
arg.Currency,
arg.BankLogo,
)
var i Bank
err := row.Scan(
&i.ID,
&i.Slug,
&i.Swift,
&i.Name,
&i.AcctLength,
&i.CountryID,
&i.IsMobilemoney,
&i.IsActive,
&i.IsRtgs,
&i.Active,
&i.Is24hrs,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BankLogo,
)
return i, err
}
const DeleteBank = `-- name: DeleteBank :exec
DELETE FROM banks
WHERE id = $1
`
func (q *Queries) DeleteBank(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteBank, id)
return err
}
const GetAllBanks = `-- name: GetAllBanks :many
SELECT id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo
FROM banks
WHERE (
country_id = $1
OR $1 IS NULL
)
AND (
is_active = $2
OR $2 IS NULL
)
`
type GetAllBanksParams struct {
CountryID pgtype.Int4 `json:"country_id"`
IsActive pgtype.Int4 `json:"is_active"`
}
func (q *Queries) GetAllBanks(ctx context.Context, arg GetAllBanksParams) ([]Bank, error) {
rows, err := q.db.Query(ctx, GetAllBanks, arg.CountryID, arg.IsActive)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Bank
for rows.Next() {
var i Bank
if err := rows.Scan(
&i.ID,
&i.Slug,
&i.Swift,
&i.Name,
&i.AcctLength,
&i.CountryID,
&i.IsMobilemoney,
&i.IsActive,
&i.IsRtgs,
&i.Active,
&i.Is24hrs,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BankLogo,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetBankByID = `-- name: GetBankByID :one
SELECT id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo
FROM banks
WHERE id = $1
`
func (q *Queries) GetBankByID(ctx context.Context, id int64) (Bank, error) {
row := q.db.QueryRow(ctx, GetBankByID, id)
var i Bank
err := row.Scan(
&i.ID,
&i.Slug,
&i.Swift,
&i.Name,
&i.AcctLength,
&i.CountryID,
&i.IsMobilemoney,
&i.IsActive,
&i.IsRtgs,
&i.Active,
&i.Is24hrs,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BankLogo,
)
return i, err
}
const UpdateBank = `-- name: UpdateBank :one
UPDATE banks
SET slug = COALESCE($2, slug),
swift = COALESCE($3, swift),
name = COALESCE($4, name),
acct_length = COALESCE($5, acct_length),
country_id = COALESCE($6, country_id),
is_mobilemoney = COALESCE($7, is_mobilemoney),
is_active = COALESCE($8, is_active),
is_rtgs = COALESCE($9, is_rtgs),
active = COALESCE($10, active),
is_24hrs = COALESCE($11, is_24hrs),
updated_at = CURRENT_TIMESTAMP,
currency = COALESCE($12, currency),
bank_logo = COALESCE($13, bank_logo)
WHERE id = $1
RETURNING id, slug, swift, name, acct_length, country_id, is_mobilemoney, is_active, is_rtgs, active, is_24hrs, created_at, updated_at, currency, bank_logo
`
type UpdateBankParams struct {
ID int64 `json:"id"`
Slug pgtype.Text `json:"slug"`
Swift pgtype.Text `json:"swift"`
Name pgtype.Text `json:"name"`
AcctLength pgtype.Int4 `json:"acct_length"`
CountryID pgtype.Int4 `json:"country_id"`
IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"`
IsActive pgtype.Int4 `json:"is_active"`
IsRtgs pgtype.Int4 `json:"is_rtgs"`
Active pgtype.Int4 `json:"active"`
Is24hrs pgtype.Int4 `json:"is_24hrs"`
Currency pgtype.Text `json:"currency"`
BankLogo pgtype.Text `json:"bank_logo"`
}
func (q *Queries) UpdateBank(ctx context.Context, arg UpdateBankParams) (Bank, error) {
row := q.db.QueryRow(ctx, UpdateBank,
arg.ID,
arg.Slug,
arg.Swift,
arg.Name,
arg.AcctLength,
arg.CountryID,
arg.IsMobilemoney,
arg.IsActive,
arg.IsRtgs,
arg.Active,
arg.Is24hrs,
arg.Currency,
arg.BankLogo,
)
var i Bank
err := row.Scan(
&i.ID,
&i.Slug,
&i.Swift,
&i.Name,
&i.AcctLength,
&i.CountryID,
&i.IsMobilemoney,
&i.IsActive,
&i.IsRtgs,
&i.Active,
&i.Is24hrs,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BankLogo,
)
return i, err
}

View File

@ -0,0 +1,181 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: issue_reporting.sql
package dbgen
import (
"context"
)
const CountReportedIssues = `-- name: CountReportedIssues :one
SELECT COUNT(*) FROM reported_issues
`
func (q *Queries) CountReportedIssues(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, CountReportedIssues)
var count int64
err := row.Scan(&count)
return count, err
}
const CountReportedIssuesByCustomer = `-- name: CountReportedIssuesByCustomer :one
SELECT COUNT(*) FROM reported_issues WHERE customer_id = $1
`
func (q *Queries) CountReportedIssuesByCustomer(ctx context.Context, customerID int64) (int64, error) {
row := q.db.QueryRow(ctx, CountReportedIssuesByCustomer, customerID)
var count int64
err := row.Scan(&count)
return count, err
}
const CreateReportedIssue = `-- name: CreateReportedIssue :one
INSERT INTO reported_issues (
customer_id, subject, description, issue_type, metadata
) VALUES (
$1, $2, $3, $4, $5
)
RETURNING id, customer_id, subject, description, issue_type, status, metadata, created_at, updated_at
`
type CreateReportedIssueParams struct {
CustomerID int64 `json:"customer_id"`
Subject string `json:"subject"`
Description string `json:"description"`
IssueType string `json:"issue_type"`
Metadata []byte `json:"metadata"`
}
func (q *Queries) CreateReportedIssue(ctx context.Context, arg CreateReportedIssueParams) (ReportedIssue, error) {
row := q.db.QueryRow(ctx, CreateReportedIssue,
arg.CustomerID,
arg.Subject,
arg.Description,
arg.IssueType,
arg.Metadata,
)
var i ReportedIssue
err := row.Scan(
&i.ID,
&i.CustomerID,
&i.Subject,
&i.Description,
&i.IssueType,
&i.Status,
&i.Metadata,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const DeleteReportedIssue = `-- name: DeleteReportedIssue :exec
DELETE FROM reported_issues WHERE id = $1
`
func (q *Queries) DeleteReportedIssue(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteReportedIssue, id)
return err
}
const ListReportedIssues = `-- name: ListReportedIssues :many
SELECT id, customer_id, subject, description, issue_type, status, metadata, created_at, updated_at FROM reported_issues
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`
type ListReportedIssuesParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListReportedIssues(ctx context.Context, arg ListReportedIssuesParams) ([]ReportedIssue, error) {
rows, err := q.db.Query(ctx, ListReportedIssues, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ReportedIssue
for rows.Next() {
var i ReportedIssue
if err := rows.Scan(
&i.ID,
&i.CustomerID,
&i.Subject,
&i.Description,
&i.IssueType,
&i.Status,
&i.Metadata,
&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 ListReportedIssuesByCustomer = `-- name: ListReportedIssuesByCustomer :many
SELECT id, customer_id, subject, description, issue_type, status, metadata, created_at, updated_at FROM reported_issues
WHERE customer_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
`
type ListReportedIssuesByCustomerParams struct {
CustomerID int64 `json:"customer_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListReportedIssuesByCustomer(ctx context.Context, arg ListReportedIssuesByCustomerParams) ([]ReportedIssue, error) {
rows, err := q.db.Query(ctx, ListReportedIssuesByCustomer, arg.CustomerID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ReportedIssue
for rows.Next() {
var i ReportedIssue
if err := rows.Scan(
&i.ID,
&i.CustomerID,
&i.Subject,
&i.Description,
&i.IssueType,
&i.Status,
&i.Metadata,
&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 UpdateReportedIssueStatus = `-- name: UpdateReportedIssueStatus :exec
UPDATE reported_issues
SET status = $2, updated_at = NOW()
WHERE id = $1
`
type UpdateReportedIssueStatusParams struct {
ID int64 `json:"id"`
Status string `json:"status"`
}
func (q *Queries) UpdateReportedIssueStatus(ctx context.Context, arg UpdateReportedIssueStatusParams) error {
_, err := q.db.Exec(ctx, UpdateReportedIssueStatus, arg.ID, arg.Status)
return err
}

View File

@ -33,6 +33,7 @@ SELECT id,
country_code,
bet365_id,
is_active,
is_featured,
sport_id
FROM leagues
WHERE (
@ -47,13 +48,18 @@ WHERE (
is_active = $3
OR $3 IS NULL
)
LIMIT $5 OFFSET $4
AND (
is_featured = $4
OR $4 IS NULL
)
LIMIT $6 OFFSET $5
`
type GetAllLeaguesParams struct {
CountryCode pgtype.Text `json:"country_code"`
SportID pgtype.Int4 `json:"sport_id"`
IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
@ -64,6 +70,7 @@ type GetAllLeaguesRow struct {
CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID int32 `json:"sport_id"`
}
@ -72,6 +79,7 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([
arg.CountryCode,
arg.SportID,
arg.IsActive,
arg.IsFeatured,
arg.Offset,
arg.Limit,
)
@ -88,6 +96,57 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([
&i.CountryCode,
&i.Bet365ID,
&i.IsActive,
&i.IsFeatured,
&i.SportID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetFeaturedLeagues = `-- name: GetFeaturedLeagues :many
SELECT id,
name,
country_code,
bet365_id,
is_active,
is_featured,
sport_id
FROM leagues
WHERE is_featured = true
`
type GetFeaturedLeaguesRow struct {
ID int64 `json:"id"`
Name string `json:"name"`
CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID int32 `json:"sport_id"`
}
func (q *Queries) GetFeaturedLeagues(ctx context.Context) ([]GetFeaturedLeaguesRow, error) {
rows, err := q.db.Query(ctx, GetFeaturedLeagues)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFeaturedLeaguesRow
for rows.Next() {
var i GetFeaturedLeaguesRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.CountryCode,
&i.Bet365ID,
&i.IsActive,
&i.IsFeatured,
&i.SportID,
); err != nil {
return nil, err
@ -107,14 +166,16 @@ INSERT INTO leagues (
country_code,
bet365_id,
sport_id,
is_active
is_active,
is_featured
)
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO
VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id) DO
UPDATE
SET name = EXCLUDED.name,
country_code = EXCLUDED.country_code,
bet365_id = EXCLUDED.bet365_id,
is_active = EXCLUDED.is_active,
is_featured = EXCLUDED.is_featured,
sport_id = EXCLUDED.sport_id
`
@ -125,6 +186,7 @@ type InsertLeagueParams struct {
Bet365ID pgtype.Int4 `json:"bet365_id"`
SportID int32 `json:"sport_id"`
IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
}
func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error {
@ -135,6 +197,7 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro
arg.Bet365ID,
arg.SportID,
arg.IsActive,
arg.IsFeatured,
)
return err
}
@ -161,7 +224,8 @@ SET name = COALESCE($2, name),
country_code = COALESCE($3, country_code),
bet365_id = COALESCE($4, bet365_id),
is_active = COALESCE($5, is_active),
sport_id = COALESCE($6, sport_id)
is_featured = COALESCE($6, is_featured),
sport_id = COALESCE($7, sport_id)
WHERE id = $1
`
@ -171,6 +235,7 @@ type UpdateLeagueParams struct {
CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID pgtype.Int4 `json:"sport_id"`
}
@ -181,6 +246,7 @@ func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) erro
arg.CountryCode,
arg.Bet365ID,
arg.IsActive,
arg.IsFeatured,
arg.SportID,
)
return err
@ -192,7 +258,8 @@ SET name = COALESCE($2, name),
id = COALESCE($3, id),
country_code = COALESCE($4, country_code),
is_active = COALESCE($5, is_active),
sport_id = COALESCE($6, sport_id)
is_featured = COALESCE($6, is_featured),
sport_id = COALESCE($7, sport_id)
WHERE bet365_id = $1
`
@ -202,6 +269,7 @@ type UpdateLeagueByBet365IDParams struct {
ID pgtype.Int8 `json:"id"`
CountryCode pgtype.Text `json:"country_code"`
IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
SportID pgtype.Int4 `json:"sport_id"`
}
@ -212,6 +280,7 @@ func (q *Queries) UpdateLeagueByBet365ID(ctx context.Context, arg UpdateLeagueBy
arg.ID,
arg.CountryCode,
arg.IsActive,
arg.IsFeatured,
arg.SportID,
)
return err

View File

@ -55,21 +55,40 @@ func (ns NullReferralstatus) Value() (driver.Value, error) {
return string(ns.Referralstatus), nil
}
type Bank struct {
ID int64 `json:"id"`
Slug string `json:"slug"`
Swift string `json:"swift"`
Name string `json:"name"`
AcctLength int32 `json:"acct_length"`
CountryID int32 `json:"country_id"`
IsMobilemoney pgtype.Int4 `json:"is_mobilemoney"`
IsActive int32 `json:"is_active"`
IsRtgs int32 `json:"is_rtgs"`
Active int32 `json:"active"`
Is24hrs pgtype.Int4 `json:"is_24hrs"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Currency string `json:"currency"`
BankLogo pgtype.Text `json:"bank_logo"`
}
type Bet struct {
ID int64 `json:"id"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
CompanyID pgtype.Int8 `json:"company_id"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
CashedOut bool `json:"cashed_out"`
CashoutID string `json:"cashout_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
IsShopBet bool `json:"is_shop_bet"`
ID int64 `json:"id"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
CompanyID pgtype.Int8 `json:"company_id"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
CashedOut bool `json:"cashed_out"`
CashoutID string `json:"cashout_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
IsShopBet bool `json:"is_shop_bet"`
OutcomesHash string `json:"outcomes_hash"`
}
type BetOutcome struct {
@ -91,21 +110,22 @@ type BetOutcome struct {
}
type BetWithOutcome struct {
ID int64 `json:"id"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
CompanyID pgtype.Int8 `json:"company_id"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
CashedOut bool `json:"cashed_out"`
CashoutID string `json:"cashout_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
IsShopBet bool `json:"is_shop_bet"`
Outcomes []BetOutcome `json:"outcomes"`
ID int64 `json:"id"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
CompanyID pgtype.Int8 `json:"company_id"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
CashedOut bool `json:"cashed_out"`
CashoutID string `json:"cashout_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
IsShopBet bool `json:"is_shop_bet"`
OutcomesHash string `json:"outcomes_hash"`
Outcomes []BetOutcome `json:"outcomes"`
}
type Branch struct {
@ -141,6 +161,7 @@ type BranchDetail struct {
ManagerName interface{} `json:"manager_name"`
ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"`
Balance pgtype.Int8 `json:"balance"`
WalletIsActive pgtype.Bool `json:"wallet_is_active"`
}
type BranchOperation struct {
@ -179,6 +200,23 @@ type CustomerWallet struct {
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type CustomerWalletDetail struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
RegularID int64 `json:"regular_id"`
RegularBalance int64 `json:"regular_balance"`
StaticID int64 `json:"static_id"`
StaticBalance int64 `json:"static_balance"`
RegularIsActive bool `json:"regular_is_active"`
StaticIsActive bool `json:"static_is_active"`
RegularUpdatedAt pgtype.Timestamp `json:"regular_updated_at"`
StaticUpdatedAt pgtype.Timestamp `json:"static_updated_at"`
CreatedAt pgtype.Timestamp `json:"created_at"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber pgtype.Text `json:"phone_number"`
}
type Event struct {
ID string `json:"id"`
SportID pgtype.Int4 `json:"sport_id"`
@ -204,13 +242,31 @@ type Event struct {
Source pgtype.Text `json:"source"`
}
type ExchangeRate struct {
ID int32 `json:"id"`
FromCurrency string `json:"from_currency"`
ToCurrency string `json:"to_currency"`
Rate pgtype.Numeric `json:"rate"`
ValidUntil pgtype.Timestamp `json:"valid_until"`
CreatedAt pgtype.Timestamp `json:"created_at"`
}
type FavoriteGame struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
GameID int64 `json:"game_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
}
type League struct {
ID int64 `json:"id"`
Name string `json:"name"`
Img pgtype.Text `json:"img"`
CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"`
SportID int32 `json:"sport_id"`
IsActive pgtype.Bool `json:"is_active"`
IsFeatured pgtype.Bool `json:"is_featured"`
}
type Notification struct {
@ -296,6 +352,18 @@ type RefreshToken struct {
Revoked bool `json:"revoked"`
}
type ReportedIssue struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
Subject string `json:"subject"`
Description string `json:"description"`
IssueType string `json:"issue_type"`
Status string `json:"status"`
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type Result struct {
ID int64 `json:"id"`
BetOutcomeID int64 `json:"bet_outcome_id"`
@ -311,6 +379,13 @@ type Result struct {
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type Setting struct {
Key string `json:"key"`
Value string `json:"value"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type SupportedOperation struct {
ID int64 `json:"id"`
Name string `json:"name"`
@ -434,6 +509,24 @@ type VirtualGame struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type VirtualGameHistory struct {
ID int64 `json:"id"`
SessionID pgtype.Text `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
WalletID pgtype.Int8 `json:"wallet_id"`
GameID pgtype.Int8 `json:"game_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
ExternalTransactionID string `json:"external_transaction_id"`
ReferenceTransactionID pgtype.Text `json:"reference_transaction_id"`
Status string `json:"status"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type VirtualGameSession struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
@ -450,6 +543,9 @@ type VirtualGameTransaction struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
GameID pgtype.Text `json:"game_id"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`
@ -470,6 +566,7 @@ type Wallet struct {
IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
Currency string `json:"currency"`
BonusBalance pgtype.Numeric `json:"bonus_balance"`
CashBalance pgtype.Numeric `json:"cash_balance"`
}
@ -488,7 +585,7 @@ type WalletTransfer struct {
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
CashierID pgtype.Int8 `json:"cashier_id"`
Verified pgtype.Bool `json:"verified"`
ReferenceNumber pgtype.Text `json:"reference_number"`
ReferenceNumber string `json:"reference_number"`
Status pgtype.Text `json:"status"`
PaymentMethod pgtype.Text `json:"payment_method"`
CreatedAt pgtype.Timestamp `json:"created_at"`

199
gen/db/report.sql.go Normal file
View File

@ -0,0 +1,199 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: report.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const GetBranchWiseReport = `-- name: GetBranchWiseReport :many
SELECT
b.branch_id,
br.name AS branch_name,
br.company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out,
COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs
FROM bets b
JOIN branches br ON b.branch_id = br.id
WHERE b.created_at BETWEEN $1 AND $2
GROUP BY b.branch_id, br.name, br.company_id
`
type GetBranchWiseReportParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
}
type GetBranchWiseReportRow struct {
BranchID pgtype.Int8 `json:"branch_id"`
BranchName string `json:"branch_name"`
CompanyID int64 `json:"company_id"`
TotalBets int64 `json:"total_bets"`
TotalCashMade interface{} `json:"total_cash_made"`
TotalCashOut interface{} `json:"total_cash_out"`
TotalCashBacks interface{} `json:"total_cash_backs"`
}
func (q *Queries) GetBranchWiseReport(ctx context.Context, arg GetBranchWiseReportParams) ([]GetBranchWiseReportRow, error) {
rows, err := q.db.Query(ctx, GetBranchWiseReport, arg.From, arg.To)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetBranchWiseReportRow
for rows.Next() {
var i GetBranchWiseReportRow
if err := rows.Scan(
&i.BranchID,
&i.BranchName,
&i.CompanyID,
&i.TotalBets,
&i.TotalCashMade,
&i.TotalCashOut,
&i.TotalCashBacks,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetCompanyWiseReport = `-- name: GetCompanyWiseReport :many
SELECT
b.company_id,
c.name AS company_name,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out,
COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs
FROM bets b
JOIN companies c ON b.company_id = c.id
WHERE b.created_at BETWEEN $1 AND $2
GROUP BY b.company_id, c.name
`
type GetCompanyWiseReportParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
}
type GetCompanyWiseReportRow struct {
CompanyID pgtype.Int8 `json:"company_id"`
CompanyName string `json:"company_name"`
TotalBets int64 `json:"total_bets"`
TotalCashMade interface{} `json:"total_cash_made"`
TotalCashOut interface{} `json:"total_cash_out"`
TotalCashBacks interface{} `json:"total_cash_backs"`
}
func (q *Queries) GetCompanyWiseReport(ctx context.Context, arg GetCompanyWiseReportParams) ([]GetCompanyWiseReportRow, error) {
rows, err := q.db.Query(ctx, GetCompanyWiseReport, arg.From, arg.To)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCompanyWiseReportRow
for rows.Next() {
var i GetCompanyWiseReportRow
if err := rows.Scan(
&i.CompanyID,
&i.CompanyName,
&i.TotalBets,
&i.TotalCashMade,
&i.TotalCashOut,
&i.TotalCashBacks,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetTotalBetsMadeInRange = `-- name: GetTotalBetsMadeInRange :one
SELECT COUNT(*) AS total_bets
FROM bets
WHERE created_at BETWEEN $1 AND $2
`
type GetTotalBetsMadeInRangeParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
}
func (q *Queries) GetTotalBetsMadeInRange(ctx context.Context, arg GetTotalBetsMadeInRangeParams) (int64, error) {
row := q.db.QueryRow(ctx, GetTotalBetsMadeInRange, arg.From, arg.To)
var total_bets int64
err := row.Scan(&total_bets)
return total_bets, err
}
const GetTotalCashBacksInRange = `-- name: GetTotalCashBacksInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_backs
FROM bets
WHERE created_at BETWEEN $1 AND $2
AND status = 5
`
type GetTotalCashBacksInRangeParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
}
func (q *Queries) GetTotalCashBacksInRange(ctx context.Context, arg GetTotalCashBacksInRangeParams) (interface{}, error) {
row := q.db.QueryRow(ctx, GetTotalCashBacksInRange, arg.From, arg.To)
var total_cash_backs interface{}
err := row.Scan(&total_cash_backs)
return total_cash_backs, err
}
const GetTotalCashMadeInRange = `-- name: GetTotalCashMadeInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_made
FROM bets
WHERE created_at BETWEEN $1 AND $2
`
type GetTotalCashMadeInRangeParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
}
func (q *Queries) GetTotalCashMadeInRange(ctx context.Context, arg GetTotalCashMadeInRangeParams) (interface{}, error) {
row := q.db.QueryRow(ctx, GetTotalCashMadeInRange, arg.From, arg.To)
var total_cash_made interface{}
err := row.Scan(&total_cash_made)
return total_cash_made, err
}
const GetTotalCashOutInRange = `-- name: GetTotalCashOutInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_out
FROM bets
WHERE created_at BETWEEN $1 AND $2
AND cashed_out = true
`
type GetTotalCashOutInRangeParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
}
func (q *Queries) GetTotalCashOutInRange(ctx context.Context, arg GetTotalCashOutInRangeParams) (interface{}, error) {
row := q.db.QueryRow(ctx, GetTotalCashOutInRange, arg.From, arg.To)
var total_cash_out interface{}
err := row.Scan(&total_cash_out)
return total_cash_out, err
}

83
gen/db/settings.sql.go Normal file
View File

@ -0,0 +1,83 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: settings.sql
package dbgen
import (
"context"
)
const GetSetting = `-- name: GetSetting :one
SELECT key, value, created_at, updated_at
FROM settings
WHERE key = $1
`
func (q *Queries) GetSetting(ctx context.Context, key string) (Setting, error) {
row := q.db.QueryRow(ctx, GetSetting, key)
var i Setting
err := row.Scan(
&i.Key,
&i.Value,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetSettings = `-- name: GetSettings :many
SELECT key, value, created_at, updated_at
FROM settings
`
func (q *Queries) GetSettings(ctx context.Context) ([]Setting, error) {
rows, err := q.db.Query(ctx, GetSettings)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Setting
for rows.Next() {
var i Setting
if err := rows.Scan(
&i.Key,
&i.Value,
&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 SaveSetting = `-- name: SaveSetting :one
INSERT INTO settings (key, value, updated_at)
VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value
RETURNING key, value, created_at, updated_at
`
type SaveSettingParams struct {
Key string `json:"key"`
Value string `json:"value"`
}
func (q *Queries) SaveSetting(ctx context.Context, arg SaveSettingParams) (Setting, error) {
row := q.db.QueryRow(ctx, SaveSetting, arg.Key, arg.Value)
var i Setting
err := row.Scan(
&i.Key,
&i.Value,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@ -128,6 +128,29 @@ func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error
return items, nil
}
const GetAllTicketsInRange = `-- name: GetAllTicketsInRange :one
SELECT COUNT(*) as total_tickets, COALESCE(SUM(amount), 0) as total_amount
FROM tickets
WHERE created_at BETWEEN $1 AND $2
`
type GetAllTicketsInRangeParams struct {
CreatedAt pgtype.Timestamp `json:"created_at"`
CreatedAt_2 pgtype.Timestamp `json:"created_at_2"`
}
type GetAllTicketsInRangeRow struct {
TotalTickets int64 `json:"total_tickets"`
TotalAmount interface{} `json:"total_amount"`
}
func (q *Queries) GetAllTicketsInRange(ctx context.Context, arg GetAllTicketsInRangeParams) (GetAllTicketsInRangeRow, error) {
row := q.db.QueryRow(ctx, GetAllTicketsInRange, arg.CreatedAt, arg.CreatedAt_2)
var i GetAllTicketsInRangeRow
err := row.Scan(&i.TotalTickets, &i.TotalAmount)
return i, err
}
const GetTicketByID = `-- name: GetTicketByID :one
SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes
FROM ticket_with_outcomes

View File

@ -34,7 +34,7 @@ type CreateTransferParams struct {
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
CashierID pgtype.Int8 `json:"cashier_id"`
Verified pgtype.Bool `json:"verified"`
ReferenceNumber pgtype.Text `json:"reference_number"`
ReferenceNumber string `json:"reference_number"`
Status pgtype.Text `json:"status"`
PaymentMethod pgtype.Text `json:"payment_method"`
}
@ -139,7 +139,7 @@ FROM wallet_transfer
WHERE reference_number = $1
`
func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber pgtype.Text) (WalletTransfer, error) {
func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber string) (WalletTransfer, error) {
row := q.db.QueryRow(ctx, GetTransferByReference, referenceNumber)
var i WalletTransfer
err := row.Scan(
@ -199,6 +199,44 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgt
return items, nil
}
const GetWalletTransactionsInRange = `-- name: GetWalletTransactionsInRange :many
SELECT type, COUNT(*) as count, SUM(amount) as total_amount
FROM wallet_transfer
WHERE created_at BETWEEN $1 AND $2
GROUP BY type
`
type GetWalletTransactionsInRangeParams struct {
CreatedAt pgtype.Timestamp `json:"created_at"`
CreatedAt_2 pgtype.Timestamp `json:"created_at_2"`
}
type GetWalletTransactionsInRangeRow struct {
Type pgtype.Text `json:"type"`
Count int64 `json:"count"`
TotalAmount int64 `json:"total_amount"`
}
func (q *Queries) GetWalletTransactionsInRange(ctx context.Context, arg GetWalletTransactionsInRangeParams) ([]GetWalletTransactionsInRangeRow, error) {
rows, err := q.db.Query(ctx, GetWalletTransactionsInRange, arg.CreatedAt, arg.CreatedAt_2)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWalletTransactionsInRangeRow
for rows.Next() {
var i GetWalletTransactionsInRangeRow
if err := rows.Scan(&i.Type, &i.Count, &i.TotalAmount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateTransferStatus = `-- name: UpdateTransferStatus :exec
UPDATE wallet_transfer
SET status = $1,

View File

@ -11,6 +11,110 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const AddFavoriteGame = `-- name: AddFavoriteGame :exec
INSERT INTO favorite_games (
user_id,
game_id,
created_at
) VALUES ($1, $2, NOW())
ON CONFLICT (user_id, game_id) DO NOTHING
`
type AddFavoriteGameParams struct {
UserID int64 `json:"user_id"`
GameID int64 `json:"game_id"`
}
func (q *Queries) AddFavoriteGame(ctx context.Context, arg AddFavoriteGameParams) error {
_, err := q.db.Exec(ctx, AddFavoriteGame, arg.UserID, arg.GameID)
return err
}
const CreateVirtualGameHistory = `-- name: CreateVirtualGameHistory :one
INSERT INTO virtual_game_histories (
session_id,
user_id,
company_id,
provider,
wallet_id,
game_id,
transaction_type,
amount,
currency,
external_transaction_id,
reference_transaction_id,
status
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
) RETURNING
id,
session_id,
user_id,
company_id,
provider,
wallet_id,
game_id,
transaction_type,
amount,
currency,
external_transaction_id,
reference_transaction_id,
status,
created_at,
updated_at
`
type CreateVirtualGameHistoryParams struct {
SessionID pgtype.Text `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
WalletID pgtype.Int8 `json:"wallet_id"`
GameID pgtype.Int8 `json:"game_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
ExternalTransactionID string `json:"external_transaction_id"`
ReferenceTransactionID pgtype.Text `json:"reference_transaction_id"`
Status string `json:"status"`
}
func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtualGameHistoryParams) (VirtualGameHistory, error) {
row := q.db.QueryRow(ctx, CreateVirtualGameHistory,
arg.SessionID,
arg.UserID,
arg.CompanyID,
arg.Provider,
arg.WalletID,
arg.GameID,
arg.TransactionType,
arg.Amount,
arg.Currency,
arg.ExternalTransactionID,
arg.ReferenceTransactionID,
arg.Status,
)
var i VirtualGameHistory
err := row.Scan(
&i.ID,
&i.SessionID,
&i.UserID,
&i.CompanyID,
&i.Provider,
&i.WalletID,
&i.GameID,
&i.TransactionType,
&i.Amount,
&i.Currency,
&i.ExternalTransactionID,
&i.ReferenceTransactionID,
&i.Status,
&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
@ -54,27 +158,47 @@ func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtua
const CreateVirtualGameTransaction = `-- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions (
session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status
session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
) RETURNING id, session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
`
type CreateVirtualGameTransactionParams struct {
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
ExternalTransactionID string `json:"external_transaction_id"`
Status string `json:"status"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
ExternalTransactionID string `json:"external_transaction_id"`
Status string `json:"status"`
}
func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVirtualGameTransactionParams) (VirtualGameTransaction, error) {
type CreateVirtualGameTransactionRow struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
ExternalTransactionID string `json:"external_transaction_id"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVirtualGameTransactionParams) (CreateVirtualGameTransactionRow, error) {
row := q.db.QueryRow(ctx, CreateVirtualGameTransaction,
arg.SessionID,
arg.UserID,
arg.CompanyID,
arg.Provider,
arg.WalletID,
arg.TransactionType,
arg.Amount,
@ -82,11 +206,13 @@ func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVi
arg.ExternalTransactionID,
arg.Status,
)
var i VirtualGameTransaction
var i CreateVirtualGameTransactionRow
err := row.Scan(
&i.ID,
&i.SessionID,
&i.UserID,
&i.CompanyID,
&i.Provider,
&i.WalletID,
&i.TransactionType,
&i.Amount,
@ -122,15 +248,81 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken
return i, err
}
const GetVirtualGameSummaryInRange = `-- name: GetVirtualGameSummaryInRange :many
SELECT
c.name AS company_name,
vg.name AS game_name,
COUNT(vgt.id) AS number_of_bets,
COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum
FROM virtual_game_transactions vgt
JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
JOIN virtual_games vg ON vgs.game_id = vg.id
JOIN companies c ON vgt.company_id = c.id
WHERE vgt.transaction_type = 'BET'
AND vgt.created_at BETWEEN $1 AND $2
GROUP BY c.name, vg.name
`
type GetVirtualGameSummaryInRangeParams struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
CreatedAt_2 pgtype.Timestamptz `json:"created_at_2"`
}
type GetVirtualGameSummaryInRangeRow struct {
CompanyName string `json:"company_name"`
GameName string `json:"game_name"`
NumberOfBets int64 `json:"number_of_bets"`
TotalTransactionSum interface{} `json:"total_transaction_sum"`
}
func (q *Queries) GetVirtualGameSummaryInRange(ctx context.Context, arg GetVirtualGameSummaryInRangeParams) ([]GetVirtualGameSummaryInRangeRow, error) {
rows, err := q.db.Query(ctx, GetVirtualGameSummaryInRange, arg.CreatedAt, arg.CreatedAt_2)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetVirtualGameSummaryInRangeRow
for rows.Next() {
var i GetVirtualGameSummaryInRangeRow
if err := rows.Scan(
&i.CompanyName,
&i.GameName,
&i.NumberOfBets,
&i.TotalTransactionSum,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetVirtualGameTransactionByExternalID = `-- name: GetVirtualGameTransactionByExternalID :one
SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
FROM virtual_game_transactions
WHERE external_transaction_id = $1
`
func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, externalTransactionID string) (VirtualGameTransaction, error) {
type GetVirtualGameTransactionByExternalIDRow struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
ExternalTransactionID string `json:"external_transaction_id"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, externalTransactionID string) (GetVirtualGameTransactionByExternalIDRow, error) {
row := q.db.QueryRow(ctx, GetVirtualGameTransactionByExternalID, externalTransactionID)
var i VirtualGameTransaction
var i GetVirtualGameTransactionByExternalIDRow
err := row.Scan(
&i.ID,
&i.SessionID,
@ -147,6 +339,47 @@ func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, ext
return i, err
}
const ListFavoriteGames = `-- name: ListFavoriteGames :many
SELECT game_id
FROM favorite_games
WHERE user_id = $1
`
func (q *Queries) ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) {
rows, err := q.db.Query(ctx, ListFavoriteGames, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var game_id int64
if err := rows.Scan(&game_id); err != nil {
return nil, err
}
items = append(items, game_id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const RemoveFavoriteGame = `-- name: RemoveFavoriteGame :exec
DELETE FROM favorite_games
WHERE user_id = $1 AND game_id = $2
`
type RemoveFavoriteGameParams struct {
UserID int64 `json:"user_id"`
GameID int64 `json:"game_id"`
}
func (q *Queries) RemoveFavoriteGame(ctx context.Context, arg RemoveFavoriteGameParams) error {
_, err := q.db.Exec(ctx, RemoveFavoriteGame, arg.UserID, arg.GameID)
return err
}
const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions
SET status = $2, updated_at = CURRENT_TIMESTAMP

View File

@ -49,7 +49,7 @@ INSERT INTO wallets (
user_id
)
VALUES ($1, $2, $3, $4)
RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance
RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
`
type CreateWalletParams struct {
@ -77,6 +77,7 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BonusBalance,
&i.CashBalance,
)
@ -142,8 +143,48 @@ func (q *Queries) GetAllBranchWallets(ctx context.Context) ([]GetAllBranchWallet
return items, nil
}
const GetAllCustomerWallet = `-- name: GetAllCustomerWallet :many
SELECT id, customer_id, regular_id, regular_balance, static_id, static_balance, regular_is_active, static_is_active, regular_updated_at, static_updated_at, created_at, first_name, last_name, phone_number
FROM customer_wallet_details
`
func (q *Queries) GetAllCustomerWallet(ctx context.Context) ([]CustomerWalletDetail, error) {
rows, err := q.db.Query(ctx, GetAllCustomerWallet)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CustomerWalletDetail
for rows.Next() {
var i CustomerWalletDetail
if err := rows.Scan(
&i.ID,
&i.CustomerID,
&i.RegularID,
&i.RegularBalance,
&i.StaticID,
&i.StaticBalance,
&i.RegularIsActive,
&i.StaticIsActive,
&i.RegularUpdatedAt,
&i.StaticUpdatedAt,
&i.CreatedAt,
&i.FirstName,
&i.LastName,
&i.PhoneNumber,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetAllWallets = `-- name: GetAllWallets :many
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
FROM wallets
`
@ -166,6 +207,7 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) {
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BonusBalance,
&i.CashBalance,
); err != nil {
@ -179,37 +221,59 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) {
return items, nil
}
const GetCustomerWallet = `-- name: GetCustomerWallet :one
SELECT cw.id,
cw.customer_id,
rw.id AS regular_id,
rw.balance AS regular_balance,
sw.id AS static_id,
sw.balance AS static_balance,
rw.updated_at as regular_updated_at,
sw.updated_at as static_updated_at,
cw.created_at
FROM customer_wallets cw
JOIN wallets rw ON cw.regular_wallet_id = rw.id
JOIN wallets sw ON cw.static_wallet_id = sw.id
WHERE cw.customer_id = $1
const GetBranchByWalletID = `-- name: GetBranchByWalletID :one
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
FROM branches
WHERE wallet_id = $1
LIMIT 1
`
type GetCustomerWalletRow struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
RegularID int64 `json:"regular_id"`
RegularBalance int64 `json:"regular_balance"`
StaticID int64 `json:"static_id"`
StaticBalance int64 `json:"static_balance"`
RegularUpdatedAt pgtype.Timestamp `json:"regular_updated_at"`
StaticUpdatedAt pgtype.Timestamp `json:"static_updated_at"`
CreatedAt pgtype.Timestamp `json:"created_at"`
func (q *Queries) GetBranchByWalletID(ctx context.Context, walletID int64) (Branch, error) {
row := q.db.QueryRow(ctx, GetBranchByWalletID, walletID)
var i Branch
err := row.Scan(
&i.ID,
&i.Name,
&i.Location,
&i.IsActive,
&i.WalletID,
&i.BranchManagerID,
&i.CompanyID,
&i.IsSelfOwned,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (GetCustomerWalletRow, error) {
const GetCompanyByWalletID = `-- name: GetCompanyByWalletID :one
SELECT id, name, admin_id, wallet_id
FROM companies
WHERE wallet_id = $1
LIMIT 1
`
func (q *Queries) GetCompanyByWalletID(ctx context.Context, walletID int64) (Company, error) {
row := q.db.QueryRow(ctx, GetCompanyByWalletID, walletID)
var i Company
err := row.Scan(
&i.ID,
&i.Name,
&i.AdminID,
&i.WalletID,
)
return i, err
}
const GetCustomerWallet = `-- name: GetCustomerWallet :one
SELECT id, customer_id, regular_id, regular_balance, static_id, static_balance, regular_is_active, static_is_active, regular_updated_at, static_updated_at, created_at, first_name, last_name, phone_number
FROM customer_wallet_details
WHERE customer_id = $1
`
func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (CustomerWalletDetail, error) {
row := q.db.QueryRow(ctx, GetCustomerWallet, customerID)
var i GetCustomerWalletRow
var i CustomerWalletDetail
err := row.Scan(
&i.ID,
&i.CustomerID,
@ -217,15 +281,20 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (GetC
&i.RegularBalance,
&i.StaticID,
&i.StaticBalance,
&i.RegularIsActive,
&i.StaticIsActive,
&i.RegularUpdatedAt,
&i.StaticUpdatedAt,
&i.CreatedAt,
&i.FirstName,
&i.LastName,
&i.PhoneNumber,
)
return i, err
}
const GetWalletByID = `-- name: GetWalletByID :one
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
FROM wallets
WHERE id = $1
`
@ -243,6 +312,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) {
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BonusBalance,
&i.CashBalance,
)
@ -250,7 +320,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) {
}
const GetWalletByUserID = `-- name: GetWalletByUserID :many
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
FROM wallets
WHERE user_id = $1
`
@ -274,6 +344,7 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
&i.Currency,
&i.BonusBalance,
&i.CashBalance,
); err != nil {

14
go.mod
View File

@ -77,4 +77,16 @@ require (
go.uber.org/multierr v1.10.0 // indirect
)
require go.uber.org/atomic v1.9.0 // indirect
require (
github.com/go-resty/resty/v2 v2.16.5
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 // indirect
go.uber.org/atomic v1.9.0 // indirect
)

24
go.sum
View File

@ -9,11 +9,14 @@ github.com/amanuelabay/afrosms-go v1.0.6/go.mod h1:5mzzZtWSCDdvQsA0OyYf5CtbdGpl9
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
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=
@ -22,6 +25,8 @@ 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=
@ -49,11 +54,15 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@ -94,6 +103,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@ -114,8 +125,12 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6
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/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,6 +165,8 @@ github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9J
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/twilio/twilio-go v1.26.3 h1:K2mYBzbhPVyWF+Jq5Sw53edBFvkgWo4sKTvgaO7461I=
github.com/twilio/twilio-go v1.26.3/go.mod h1:FpgNWMoD8CFnmukpKq9RNpUSGXC0BwnbeKZj2YHlIkw=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
@ -170,6 +187,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
@ -198,6 +216,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -213,8 +232,10 @@ golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -233,9 +254,12 @@ 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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=

View File

@ -13,25 +13,28 @@ import (
)
var (
ErrInvalidDbUrl = errors.New("db url is invalid")
ErrInvalidPort = errors.New("port number is invalid")
ErrRefreshExpiry = errors.New("refresh token expiry is invalid")
ErrAccessExpiry = errors.New("access token expiry is invalid")
ErrInvalidJwtKey = errors.New("jwt key is invalid")
ErrLogLevel = errors.New("log level not set")
ErrInvalidLevel = errors.New("invalid log level")
ErrInvalidEnv = errors.New("env not set or invalid")
ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid")
ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env")
ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid")
ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid")
ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid")
ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid")
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
ErrMissingResendApiKey = errors.New("missing Resend Api key")
ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
ErrInvalidDbUrl = errors.New("db url is invalid")
ErrInvalidPort = errors.New("port number is invalid")
ErrRefreshExpiry = errors.New("refresh token expiry is invalid")
ErrAccessExpiry = errors.New("access token expiry is invalid")
ErrInvalidJwtKey = errors.New("jwt key is invalid")
ErrLogLevel = errors.New("log level not set")
ErrInvalidLevel = errors.New("invalid log level")
ErrInvalidEnv = errors.New("env not set or invalid")
ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid")
ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env")
ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid")
ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid")
ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid")
ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid")
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
ErrMissingResendApiKey = errors.New("missing Resend Api key")
ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
ErrMissingTwilioAccountSid = errors.New("missing twilio account sid")
ErrMissingTwilioAuthToken = errors.New("missing twilio auth token")
ErrMissingTwilioSenderPhoneNumber = errors.New("missing twilio sender phone number")
)
type AleaPlayConfig struct {
@ -44,15 +47,14 @@ type AleaPlayConfig struct {
SessionTimeout int `mapstructure:"session_timeout"` // In hours
}
type VeliGamesConfig struct {
Enabled bool `mapstructure:"enabled"`
APIURL string `mapstructure:"api_url"`
OperatorKey string `mapstructure:"operator_key"`
SecretKey string `mapstructure:"secret_key"`
DefaultCurrency string `mapstructure:"default_currency"`
GameIDs struct {
Aviator string `mapstructure:"aviator"`
} `mapstructure:"game_ids"`
type VeliConfig struct {
APIKey string `mapstructure:"VELI_API_KEY"`
BaseURL string `mapstructure:"VELI_BASE_URL"`
SecretKey string `mapstructure:"VELI_SECRET_KEY"`
OperatorID string `mapstructure:"VELI_OPERATOR_ID"`
Currency string `mapstructure:"VELI_DEFAULT_CURRENCY"`
WebhookURL string `mapstructure:"VELI_WEBHOOK_URL"`
Enabled bool `mapstructure:"Enabled"`
}
type Config struct {
@ -60,6 +62,7 @@ type Config struct {
FIXER_BASE_URL string
BASE_CURRENCY domain.IntCurrency
Port int
Service string
DbUrl string
RefreshExpiry int
AccessExpiry int
@ -81,10 +84,14 @@ type Config struct {
CHAPA_RETURN_URL string
Bet365Token string
PopOK domain.PopOKConfig
AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
VeliGames VeliGamesConfig `mapstructure:"veli_games"`
AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
VeliGames VeliConfig `mapstructure:"veli_games"`
ResendApiKey string
ResendSenderEmail string
TwilioAccountSid string
TwilioAuthToken string
TwilioSenderPhoneNumber string
RedisAddr string
}
func NewConfig() (*Config, error) {
@ -109,6 +116,8 @@ func (c *Config) loadEnv() error {
c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH")
c.RedisAddr = os.Getenv("REDIS_ADDR")
c.CHAPA_TRANSFER_TYPE = os.Getenv("CHAPA_TRANSFER_TYPE")
c.CHAPA_PAYMENT_TYPE = os.Getenv("CHAPA_PAYMENT_TYPE")
@ -236,26 +245,26 @@ func (c *Config) loadEnv() error {
if apiURL == "" {
apiURL = "https://api.velitech.games" // Default production URL
}
c.VeliGames.APIURL = apiURL
c.VeliGames.BaseURL = apiURL
operatorKey := os.Getenv("VELI_OPERATOR_KEY")
if operatorKey == "" && c.VeliGames.Enabled {
return ErrInvalidVeliOperatorKey
}
c.VeliGames.OperatorKey = operatorKey
// c.VeliGames.OperatorKey = operatorKey
secretKey := os.Getenv("VELI_SECRET_KEY")
if secretKey == "" && c.VeliGames.Enabled {
return ErrInvalidVeliSecretKey
}
c.VeliGames.SecretKey = secretKey
c.VeliGames.GameIDs.Aviator = os.Getenv("VELI_GAME_ID_AVIATOR")
// c.VeliGames.GameIDs.Aviator = os.Getenv("VELI_GAME_ID_AVIATOR")
defaultCurrency := os.Getenv("VELI_DEFAULT_CURRENCY")
if defaultCurrency == "" {
defaultCurrency = "USD" // Default currency
}
c.VeliGames.DefaultCurrency = defaultCurrency
// c.VeliGames.DefaultCurrency = defaultCurrency
c.LogLevel = lvl
@ -324,6 +333,24 @@ func (c *Config) loadEnv() error {
}
c.ResendSenderEmail = resendSenderEmail
twilioAccountSid := os.Getenv("TWILIO_ACCOUNT_SID")
if twilioAccountSid == "" {
return ErrMissingTwilioAccountSid
}
c.TwilioAccountSid = twilioAccountSid
twilioAuthToken := os.Getenv("TWILIO_AUTH_TOKEN")
if twilioAuthToken == "" {
return ErrMissingTwilioAuthToken
}
c.TwilioAuthToken = twilioAuthToken
twilioSenderPhoneNumber := os.Getenv("TWILIO_SENDER_PHONE_NUMBER")
if twilioSenderPhoneNumber == "" {
return ErrMissingTwilioSenderPhoneNumber
}
c.TwilioSenderPhoneNumber = twilioSenderPhoneNumber
return nil
}

View File

@ -57,9 +57,13 @@ type Bet struct {
}
type BetFilter struct {
BranchID ValidInt64 // Can Be Nullable
CompanyID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable
BranchID ValidInt64 // Can Be Nullable
CompanyID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable
IsShopBet ValidBool
Query ValidString
CreatedBefore ValidTime
CreatedAfter ValidTime
}
type GetBet struct {
@ -80,16 +84,17 @@ type GetBet struct {
}
type CreateBet struct {
Amount Currency
TotalOdds float32
Status OutcomeStatus
FullName string
PhoneNumber string
CompanyID ValidInt64 // Can Be Nullable
BranchID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable
IsShopBet bool
CashoutID string
Amount Currency
TotalOdds float32
Status OutcomeStatus
FullName string
PhoneNumber string
CompanyID ValidInt64 // Can Be Nullable
BranchID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable
IsShopBet bool
CashoutID string
OutcomesHash string
}
type CreateBetOutcomeReq struct {
@ -173,4 +178,3 @@ func ConvertBet(bet GetBet) BetRes {
CreatedAt: bet.CreatedAt,
}
}

View File

@ -7,10 +7,19 @@ type Branch struct {
WalletID int64
BranchManagerID int64
CompanyID int64
IsSuspended bool
IsActive bool
IsSelfOwned bool
}
type BranchFilter struct {
CompanyID ValidInt64
IsActive ValidBool
BranchManagerID ValidInt64
Query ValidString
CreatedBefore ValidTime
CreatedAfter ValidTime
}
type BranchDetail struct {
ID int64
Name string
@ -19,10 +28,11 @@ type BranchDetail struct {
Balance Currency
BranchManagerID int64
CompanyID int64
IsSuspended bool
IsActive bool
IsSelfOwned bool
ManagerName string
ManagerPhoneNumber string
WalletIsActive bool
}
type SupportedOperation struct {
@ -53,6 +63,7 @@ type UpdateBranch struct {
BranchManagerID *int64
CompanyID *int64
IsSelfOwned *bool
IsActive *bool
}
type CreateSupportedOperation struct {

View File

@ -16,6 +16,7 @@ type PaymentStatus string
type WithdrawalStatus string
const (
WithdrawalStatusSuccessful WithdrawalStatus = "success"
WithdrawalStatusPending WithdrawalStatus = "pending"
WithdrawalStatusProcessing WithdrawalStatus = "processing"
WithdrawalStatusCompleted WithdrawalStatus = "completed"
@ -23,9 +24,10 @@ const (
)
const (
PaymentStatusPending PaymentStatus = "pending"
PaymentStatusCompleted PaymentStatus = "completed"
PaymentStatusFailed PaymentStatus = "failed"
PaymentStatusSuccessful PaymentStatus = "success"
PaymentStatusPending PaymentStatus = "pending"
PaymentStatusCompleted PaymentStatus = "completed"
PaymentStatusFailed PaymentStatus = "failed"
)
type ChapaDepositRequest struct {
@ -70,22 +72,23 @@ type ChapaVerificationResponse struct {
TxRef string `json:"tx_ref"`
}
type Bank struct {
ID int `json:"id"`
Slug string `json:"slug"`
Swift string `json:"swift"`
Name string `json:"name"`
AcctLength int `json:"acct_length"`
CountryID int `json:"country_id"`
IsMobileMoney int `json:"is_mobilemoney"` // nullable
IsActive int `json:"is_active"`
IsRTGS int `json:"is_rtgs"`
Active int `json:"active"`
Is24Hrs int `json:"is_24hrs"` // nullable
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Currency string `json:"currency"`
}
// type Bank struct {
// ID int `json:"id"`
// Slug string `json:"slug"`
// Swift string `json:"swift"`
// Name string `json:"name"`
// AcctLength int `json:"acct_length"`
// CountryID int `json:"country_id"`
// IsMobileMoney int `json:"is_mobilemoney"` // nullable
// IsActive int `json:"is_active"`
// IsRTGS int `json:"is_rtgs"`
// Active int `json:"active"`
// Is24Hrs int `json:"is_24hrs"` // nullable
// CreatedAt time.Time `json:"created_at"`
// UpdatedAt time.Time `json:"updated_at"`
// Currency string `json:"currency"`
// BankLogo string `json:"bank_logo"` // URL or base64
// }
type BankResponse struct {
Message string `json:"message"`
@ -142,11 +145,9 @@ type ChapaWithdrawalRequest struct {
// }
type ChapaWithdrawalResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Data struct {
Reference string `json:"reference"`
} `json:"data"`
Status string `json:"status"`
Data string `json:"data"` // Accepts string instead of struct
}
type ChapaTransactionType struct {

View File

@ -78,3 +78,6 @@ func CalculateWinnings(amount Currency, totalOdds float32) Currency {
return ToCurrency(possibleWin - incomeTax)
}
func PtrFloat64(v float64) *float64 { return &v }
func PtrInt64(v int64) *int64 { return &v }

View File

@ -0,0 +1,21 @@
package domain
import "time"
type Bank struct {
ID int `json:"id"`
Slug string `json:"slug"`
Swift string `json:"swift"`
Name string `json:"name"`
AcctLength int `json:"acct_length"`
CountryID int `json:"country_id"`
IsMobileMoney int `json:"is_mobilemoney"` // nullable
IsActive int `json:"is_active"`
IsRTGS int `json:"is_rtgs"`
Active int `json:"active"`
Is24Hrs int `json:"is_24hrs"` // nullable
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Currency string `json:"currency"`
BankLogo string `json:"bank_logo"` // URL or base64
}

View File

@ -0,0 +1,15 @@
package domain
import "time"
type ReportedIssue struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
Subject string `json:"subject"`
Description string `json:"description"`
IssueType string `json:"issue_type"`
Status string `json:"status"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

@ -7,6 +7,7 @@ type League struct {
Bet365ID int32 `json:"bet365_id" example:"1121"`
IsActive bool `json:"is_active" example:"false"`
SportID int32 `json:"sport_id" example:"1"`
IsFeatured bool `json:"is_featured" example:"false"`
}
type UpdateLeague struct {
@ -15,6 +16,7 @@ type UpdateLeague struct {
CountryCode ValidString `json:"cc" example:"uk"`
Bet365ID ValidInt32 `json:"bet365_id" example:"1121"`
IsActive ValidBool `json:"is_active" example:"false"`
IsFeatured ValidBool `json:"is_featured" example:"false"`
SportID ValidInt32 `json:"sport_id" example:"1"`
}
@ -22,6 +24,69 @@ type LeagueFilter struct {
CountryCode ValidString
SportID ValidInt32
IsActive ValidBool
IsFeatured ValidBool
Limit ValidInt64
Offset ValidInt64
}
// These leagues are automatically featured when the league is created
var FeaturedLeagues = []int64{
// Football
10044469, // Ethiopian Premier League
10041282, //Premier League
10083364, //La Liga
10041095, //German Bundesliga
10041100, //Ligue 1
10041809, //UEFA Champions League
10041957, //UEFA Europa League
10079560, //UEFA Conference League
10050282, //UEFA Nations League
10044685, //FIFA Club World Cup
10050346, //UEFA Super Cup
10081269, //CONCACAF Champions Cup
10070189, //CONCACAF Gold Cup
10076185, //UEFA Regions Cup
10067913, //Europe - World Cup Qualifying
10040162, //Asia - World Cup Qualifying
10067624, //South America - World Cup Qualifying
10073057, //North & Central America - World Cup Qualifying
10037075, //International Match
10077480, //Womens International
10037109, //Europe Friendlies
10068837, //Euro U21
10041315, //Italian Serie A
10036538, //Spain Segunda
10047168, // US MLS
10043156, //England FA Cup
10042103, //France Cup
10041088, //Premier League 2
10084250, //Turkiye Super League
10041187, //Kenya Super League
10041391, //Netherlands Eredivisie
// Basketball
10041830, //NBA
10049984, //WNBA
10037165, //German Bundesliga
10036608, //Italian Lega 1
10040795, //EuroLeague
10041534, //Basketball Africa League
// Ice Hockey
10037477, //NHL
10037447, //AHL
10069385, //IIHF World Championship
// AMERICAN FOOTBALL
10037219, //NFL
// BASEBALL
10037485, // MLB
// VOLLEYBALL
10069666, //FIVB Nations League
}

View File

@ -14,19 +14,22 @@ type NotificationDeliveryStatus string
type DeliveryChannel string
const (
NotificationTypeCashOutSuccess NotificationType = "cash_out_success"
NotificationTypeDepositSuccess NotificationType = "deposit_success"
NotificationTypeBetPlaced NotificationType = "bet_placed"
NotificationTypeDailyReport NotificationType = "daily_report"
NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet"
NotificationTypeBetOverload NotificationType = "bet_overload"
NotificationTypeSignUpWelcome NotificationType = "signup_welcome"
NotificationTypeOTPSent NotificationType = "otp_sent"
NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold"
NOTIFICATION_TYPE_TRANSFER NotificationType = "transfer_failed"
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin"
NotificationTypeCashOutSuccess NotificationType = "cash_out_success"
NotificationTypeDepositSuccess NotificationType = "deposit_success"
NotificationTypeWithdrawSuccess NotificationType = "withdraw_success"
NotificationTypeBetPlaced NotificationType = "bet_placed"
NotificationTypeDailyReport NotificationType = "daily_report"
NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet"
NotificationTypeBetOverload NotificationType = "bet_overload"
NotificationTypeSignUpWelcome NotificationType = "signup_welcome"
NotificationTypeOTPSent NotificationType = "otp_sent"
NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold"
NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed"
NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success"
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin"
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
NotificationRecieverSideCashier NotificationRecieverSide = "cashier"
@ -57,9 +60,9 @@ const (
)
type NotificationPayload struct {
Headline string `json:"headline"`
Message string `json:"message"`
Tags []string `json:"tags"`
Headline string `json:"headline"`
Message string `json:"message"`
Tags []string `json:"tags"`
}
type Notification struct {
@ -91,3 +94,19 @@ func FromJSON(data []byte) (*Notification, error) {
}
return &n, nil
}
func ReceiverFromRole(role Role) NotificationRecieverSide {
switch role {
case RoleAdmin:
return NotificationRecieverSideAdmin
case RoleCashier:
return NotificationRecieverSideCashier
case RoleBranchManager:
return NotificationRecieverSideBranchManager
case RoleCustomer:
return NotificationRecieverSideCustomer
default:
return ""
}
}

View File

@ -26,6 +26,13 @@ const (
OtpMediumSms OtpMedium = "sms"
)
type OtpProvider string
const (
TwilioSms OtpProvider = "twilio"
AfroMessage OtpProvider = "aformessage"
)
type Otp struct {
ID int64
SentTo string

View File

@ -10,6 +10,39 @@ const (
Monthly TimeFrame = "monthly"
)
type ReportFrequency string
const (
ReportDaily ReportFrequency = "daily"
ReportWeekly ReportFrequency = "weekly"
ReportMonthly ReportFrequency = "monthly"
)
type ReportRequest struct {
Frequency ReportFrequency
StartDate time.Time
EndDate time.Time
}
type ReportData struct {
TotalBets int64
TotalCashIn float64
TotalCashOut float64
CashBacks float64
Withdrawals float64
Deposits float64
TotalTickets int64
VirtualGameStats []VirtualGameStat
CompanyReports []CompanyReport
BranchReports []BranchReport
}
type VirtualGameStat struct {
GameName string
NumBets int64
TotalTransaction float64
}
type Report struct {
ID string
TimeFrame TimeFrame
@ -22,6 +55,22 @@ type Report struct {
GeneratedAt time.Time
}
type LiveMetric struct {
TotalCashSportsbook float64
TotalCashSportGames float64
TotalLiveTickets int64
TotalUnsettledCash float64
TotalGames int64
}
type MetricUpdates struct {
TotalCashSportsbookDelta *float64
TotalCashSportGamesDelta *float64
TotalLiveTicketsDelta *int64
TotalUnsettledCashDelta *float64
TotalGamesDelta *int64
}
type DashboardSummary struct {
TotalStakes Currency `json:"total_stakes"`
TotalBets int64 `json:"total_bets"`
@ -319,3 +368,41 @@ type CashierPerformance struct {
LastActivity time.Time `json:"last_activity"`
ActiveDays int `json:"active_days"`
}
type CompanyWalletBalance struct {
CompanyID int64 `json:"company_id"`
CompanyName string `json:"company_name"`
Balance float64 `json:"balance"`
}
type BranchWalletBalance struct {
BranchID int64 `json:"branch_id"`
BranchName string `json:"branch_name"`
CompanyID int64 `json:"company_id"`
Balance float64 `json:"balance"`
}
type LiveWalletMetrics struct {
Timestamp time.Time `json:"timestamp"`
CompanyBalances []CompanyWalletBalance `json:"company_balances"`
BranchBalances []BranchWalletBalance `json:"branch_balances"`
}
type CompanyReport struct {
CompanyID int64
CompanyName string
TotalBets int64
TotalCashIn float64
TotalCashOut float64
TotalCashBacks float64
}
type BranchReport struct {
BranchID int64
BranchName string
CompanyID int64
TotalBets int64
TotalCashIn float64
TotalCashOut float64
TotalCashBacks float64
}

View File

@ -0,0 +1,22 @@
package domain
import "time"
type Setting struct {
Key string
Value string
UpdatedAt time.Time
}
type SettingRes struct {
Key string `json:"key"`
Value string `json:"value"`
UpdatedAt string `json:"updated_at"`
}
type SettingList struct {
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
BetAmountLimit Currency `json:"bet_amount_limit"`
DailyTicketPerIP int64 `json:"daily_ticket_limit"`
TotalWinningLimit Currency `json:"total_winning_limit"`
}

View File

@ -53,3 +53,31 @@ type CreateTicket struct {
TotalOdds float32
IP string
}
type CreateTicketOutcomeReq struct {
// TicketID int64 `json:"ticket_id" example:"1"`
EventID int64 `json:"event_id" example:"1"`
OddID int64 `json:"odd_id" example:"1"`
MarketID int64 `json:"market_id" example:"1"`
// HomeTeamName string `json:"home_team_name" example:"Manchester"`
// AwayTeamName string `json:"away_team_name" example:"Liverpool"`
// MarketName string `json:"market_name" example:"Fulltime Result"`
// Odd float32 `json:"odd" example:"1.5"`
// OddName string `json:"odd_name" example:"1"`
// Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
}
type CreateTicketReq struct {
Outcomes []CreateTicketOutcomeReq `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
}
type CreateTicketRes struct {
FastCode int64 `json:"fast_code" example:"1234"`
CreatedNumber int64 `json:"created_number" example:"3"`
}
type TicketRes struct {
ID int64 `json:"id" example:"1"`
Outcomes []TicketOutcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
}

View File

@ -12,7 +12,10 @@ const (
type PaymentMethod string
// Info on why the wallet was modified
// If its internal system modification then, its always direct
const (
TRANSFER_DIRECT PaymentMethod = "direct"
TRANSFER_CASH PaymentMethod = "cash"
TRANSFER_BANK PaymentMethod = "bank"
TRANSFER_CHAPA PaymentMethod = "chapa"
@ -22,31 +25,36 @@ const (
TRANSFER_OTHER PaymentMethod = "other"
)
// There is always a receiving wallet id
// There is a sender wallet id only if wallet transfer type
// Info for the payment providers
type PaymentDetails struct {
ReferenceNumber ValidString
BankNumber ValidString
}
// A Transfer is logged for every modification of ALL wallets and wallet types
type Transfer struct {
ID int64
Amount Currency
Verified bool
Type TransferType
PaymentMethod PaymentMethod
ReceiverWalletID int64
SenderWalletID int64
ReferenceNumber string
Status string
CashierID ValidInt64
CreatedAt time.Time
UpdatedAt time.Time
ID int64 `json:"id"`
Amount Currency `json:"amount"`
Verified bool `json:"verified"`
Type TransferType `json:"type"`
PaymentMethod PaymentMethod `json:"payment_method"`
ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"`
SenderWalletID ValidInt64 `json:"sender_wallet_id"`
ReferenceNumber string `json:"reference_number"` // <-- needed
Status string `json:"status"`
CashierID ValidInt64 `json:"cashier_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateTransfer struct {
Amount Currency
Verified bool
ReferenceNumber string
Status string
ReceiverWalletID int64
SenderWalletID int64
CashierID ValidInt64
Type TransferType
PaymentMethod PaymentMethod
Amount Currency `json:"amount"`
Verified bool `json:"verified"`
Type TransferType `json:"type"`
PaymentMethod PaymentMethod `json:"payment_method"`
ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"`
SenderWalletID ValidInt64 `json:"sender_wallet_id"`
ReferenceNumber string `json:"reference_number"` // <-- needed
Status string `json:"status"`
CashierID ValidInt64 `json:"cashier_id"`
}

View File

@ -0,0 +1,36 @@
package domain
import "time"
type Game struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ReleaseDate string `json:"release_date"`
Developer string `json:"developer"`
Publisher string `json:"publisher"`
Genres []string `json:"genres"`
Platforms []string `json:"platforms"`
Price float64 `json:"price"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type GameListResponse struct {
Data []Game `json:"data"`
Total int `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
TotalPages int `json:"total_pages"`
}
type GameCreateRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
ReleaseDate string `json:"release_date" validate:"required"`
Developer string `json:"developer" validate:"required"`
Publisher string `json:"publisher" validate:"required"`
Genres []string `json:"genres" validate:"required"`
Platforms []string `json:"platforms" validate:"required"`
Price float64 `json:"price" validate:"required"`
}

View File

@ -4,6 +4,30 @@ import (
"time"
)
type Provider string
const (
PROVIDER_POPOK Provider = "PopOk"
PROVIDER_ALEA_PLAY Provider = "AleaPlay"
PROVIDER_VELI_GAMES Provider = "VeliGames"
)
type FavoriteGame struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
GameID int64 `json:"game_id"`
CreatedAt time.Time `json:"created_at"`
}
type FavoriteGameRequest struct {
GameID int64 `json:"game_id"`
}
type FavoriteGameResponse struct {
GameID int64 `json:"game_id"`
GameName string `json:"game_name"`
}
type VirtualGame struct {
ID int64 `json:"id"`
Name string `json:"name"`
@ -38,10 +62,31 @@ type VirtualGameSession struct {
GameMode string `json:"game_mode"` // real, demo, tournament
}
type VirtualGameHistory struct {
ID int64 `json:"id"`
SessionID string `json:"session_id,omitempty"` // Optional, if session tracking is used
UserID int64 `json:"user_id"`
CompanyID int64 `json:"company_id"`
Provider string `json:"provider"`
WalletID *int64 `json:"wallet_id,omitempty"` // Optional if wallet detail is needed
GameID *int64 `json:"game_id,omitempty"` // Optional for game-level analysis
TransactionType string `json:"transaction_type"` // BET, WIN, CANCEL, etc.
Amount int64 `json:"amount"` // Stored in minor units (e.g. cents)
Currency string `json:"currency"` // e.g., ETB, USD
ExternalTransactionID string `json:"external_transaction_id"` // Provider transaction ID
ReferenceTransactionID string `json:"reference_transaction_id,omitempty"` // For CANCELs pointing to BETs
Status string `json:"status"` // COMPLETED, CANCELLED, FAILED, etc.
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type VirtualGameTransaction struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID int64 `json:"company_id"`
Provider string `json:"provider"`
GameID string `json:"game_id"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc.
Amount int64 `json:"amount"` // Always in cents
@ -143,6 +188,11 @@ type PopOKWinResponse struct {
Balance float64 `json:"balance"`
}
type PopOKGenerateTokenRequest struct {
GameID string `json:"newGameId"`
Token string `json:"token"`
}
type PopOKCancelRequest struct {
ExternalToken string `json:"externalToken"`
PlayerID string `json:"playerId"`
@ -156,6 +206,10 @@ type PopOKCancelResponse struct {
Balance float64 `json:"balance"`
}
type PopOKGenerateTokenResponse struct {
NewToken string `json:"newToken"`
}
type AleaPlayCallback struct {
EventID string `json:"event_id"`
TransactionID string `json:"transaction_id"`
@ -191,3 +245,27 @@ type GameSpecificData struct {
RiskLevel string `json:"risk_level,omitempty"` // For Mines
BucketIndex int `json:"bucket_index,omitempty"` // For Plinko
}
type PopOKGame struct {
ID int `json:"id"`
GameName string `json:"gameName"`
Bets []float64 `json:"bets"`
Thumbnail string `json:"thumbnail"`
Status int `json:"status"`
}
type PopOKGameListResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Slots []PopOKGame `json:"slots"`
} `json:"data"`
}
type GameRecommendation struct {
GameID int `json:"game_id"`
GameName string `json:"game_name"`
Thumbnail string `json:"thumbnail"`
Bets []float64 `json:"bets"`
Reason string `json:"reason"` // e.g., "Based on your activity", "Popular", "Random pick"
}

View File

@ -15,6 +15,13 @@ type Wallet struct {
CreatedAt time.Time
}
type WalletFilter struct {
IsActive ValidBool
Query ValidString
CreatedBefore ValidTime
CreatedAfter ValidTime
}
type CustomerWallet struct {
ID int64
RegularID int64
@ -28,9 +35,14 @@ type GetCustomerWallet struct {
StaticID int64
StaticBalance Currency
CustomerID int64
RegularIsActive bool
StaticIsActive bool
RegularUpdatedAt time.Time
StaticUpdatedAt time.Time
CreatedAt time.Time
FirstName string
LastName string
PhoneNumber string
}
type BranchWallet struct {
@ -58,3 +70,11 @@ type CreateCustomerWallet struct {
RegularWalletID int64
StaticWalletID int64
}
type WalletType string
const (
CustomerWalletType WalletType = "customer_wallet"
BranchWalletType WalletType = "branch_wallet"
CompanyWalletType WalletType = "company_wallet"
)

View File

@ -4,16 +4,18 @@ import (
"fmt"
"os"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func InitLogger() (*zap.Logger, error) {
func InitLogger(cfg *config.Config) (*zap.Logger, error) {
mongoCore, err := NewMongoCore(
"mongodb://root:secret@localhost:27017/?authSource=admin",
os.Getenv("MONGODB_URL"),
"logdb",
"applogs",
zapcore.InfoLevel,
cfg,
)
if err != nil {
return nil, fmt.Errorf("failed to create MongoDB core: %w", err)

View File

@ -7,6 +7,7 @@ import (
"maps"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
@ -17,9 +18,10 @@ type MongoCore struct {
collection *mongo.Collection
level zapcore.Level
fields []zapcore.Field
cfg *config.Config
}
func NewMongoCore(uri, dbName, collectionName string, level zapcore.Level) (zapcore.Core, error) {
func NewMongoCore(uri, dbName, collectionName string, level zapcore.Level, cfg *config.Config) (zapcore.Core, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
@ -36,6 +38,7 @@ func NewMongoCore(uri, dbName, collectionName string, level zapcore.Level) (zapc
return &MongoCore{
collection: coll,
level: level,
cfg: cfg,
}, nil
}
@ -73,8 +76,8 @@ func (mc *MongoCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
"fields": logMap,
"caller": entry.Caller.String(),
"stacktrace": entry.Stack,
"service": "fortunebet-backend",
"env": "dev",
"service": mc.cfg.Service,
"env": mc.cfg.Env,
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

View File

@ -209,6 +209,22 @@ func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]doma
Int64: filter.UserID.Value,
Valid: filter.UserID.Valid,
},
IsShopBet: pgtype.Bool{
Bool: filter.IsShopBet.Value,
Valid: filter.IsShopBet.Valid,
},
Query: pgtype.Text{
String: filter.Query.Value,
Valid: filter.Query.Valid,
},
CreatedBefore: pgtype.Timestamp{
Time: filter.CreatedBefore.Value,
Valid: filter.CreatedBefore.Valid,
},
CreatedAfter: pgtype.Timestamp{
Time: filter.CreatedAfter.Value,
Valid: filter.CreatedAfter.Valid,
},
})
if err != nil {
domain.MongoDBLogger.Error("failed to get all bets",
@ -265,6 +281,19 @@ func (s *Store) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetB
return result, nil
}
func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
count, err := s.queries.GetBetCount(ctx, dbgen.GetBetCountParams{
UserID: pgtype.Int8{Int64: UserID},
OutcomesHash: outcomesHash,
})
if err != nil {
return 0, err
}
return count, nil
}
func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {
err := s.queries.UpdateCashOut(ctx, dbgen.UpdateCashOutParams{
ID: id,
@ -295,8 +324,19 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.Outcom
return err
}
func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error) {
outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, eventID)
func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) {
outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, dbgen.GetBetOutcomeByEventIDParams{
EventID: eventID,
FilterStatus: pgtype.Int4{
Int32: int32(domain.OUTCOME_STATUS_PENDING),
Valid: is_filtered,
},
FilterStatus2: pgtype.Int4{
Int32: int32(domain.OUTCOME_STATUS_ERROR),
Valid: is_filtered,
},
})
if err != nil {
domain.MongoDBLogger.Error("failed to get bet outcomes by event ID",
zap.Int64("event_id", eventID),
@ -347,8 +387,44 @@ func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status dom
return res, nil
}
func (s *Store) DeleteBet(ctx context.Context, id int64) error {
return s.queries.DeleteBet(ctx, id)
func (s *Store) UpdateBetOutcomeStatusByBetID(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) {
update, err := s.queries.UpdateBetOutcomeStatusByBetID(ctx, dbgen.UpdateBetOutcomeStatusByBetIDParams{
Status: int32(status),
BetID: id,
})
if err != nil {
domain.MongoDBLogger.Error("failed to update bet outcome status",
zap.Int64("id", id),
zap.Int32("status", int32(status)),
zap.Error(err),
)
return domain.BetOutcome{}, err
}
res := convertDBBetOutcomes(update)
return res, nil
}
func (s *Store) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) {
outcomes, err := s.queries.UpdateBetOutcomeStatusForEvent(ctx, dbgen.UpdateBetOutcomeStatusForEventParams{
EventID: eventID,
Status: int32(status),
})
if err != nil {
domain.MongoDBLogger.Error("failed to update bet outcome status for event",
zap.Int64("eventID", eventID),
zap.Int32("status", int32(status)),
zap.Error(err),
)
return nil, err
}
var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes))
for _, outcome := range outcomes {
result = append(result, convertDBBetOutcomes(outcome))
}
return result, nil
}
// GetBetSummary returns aggregated bet statistics

View File

@ -32,6 +32,8 @@ func convertDBBranchDetail(dbBranch dbgen.BranchDetail) domain.BranchDetail {
ManagerName: dbBranch.ManagerName.(string),
ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String,
Balance: domain.Currency(dbBranch.Balance.Int64),
IsActive: dbBranch.IsActive,
WalletIsActive: dbBranch.WalletIsActive.Bool,
}
}
@ -83,6 +85,12 @@ func convertUpdateBranch(updateBranch domain.UpdateBranch) dbgen.UpdateBranchPar
Valid: true,
}
}
if updateBranch.IsActive != nil {
newUpdateBranch.IsActive = pgtype.Bool{
Bool: *updateBranch.IsActive,
Valid: true,
}
}
return newUpdateBranch
}
@ -128,8 +136,29 @@ func (s *Store) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]do
return branches, nil
}
func (s *Store) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) {
dbBranches, err := s.queries.GetAllBranches(ctx)
func (s *Store) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) {
dbBranches, err := s.queries.GetAllBranches(ctx, dbgen.GetAllBranchesParams{
CompanyID: pgtype.Int8{
Int64: filter.CompanyID.Value,
Valid: filter.CompanyID.Valid,
},
BranchManagerID: pgtype.Int8{
Int64: filter.BranchManagerID.Value,
Valid: filter.BranchManagerID.Valid,
},
Query: pgtype.Text{
String: filter.Query.Value,
Valid: filter.Query.Valid,
},
CreatedBefore: pgtype.Timestamp{
Time: filter.CreatedBefore.Value,
Valid: filter.CreatedBefore.Valid,
},
CreatedAfter: pgtype.Timestamp{
Time: filter.CreatedAfter.Value,
Valid: filter.CreatedAfter.Valid,
},
})
if err != nil {
return nil, err
}

View File

@ -0,0 +1,139 @@
package repository
import (
"context"
"database/sql"
"errors"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
type BankRepository interface {
CreateBank(ctx context.Context, bank *domain.Bank) error
GetBankByID(ctx context.Context, id int) (*domain.Bank, error)
GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error)
UpdateBank(ctx context.Context, bank *domain.Bank) error
DeleteBank(ctx context.Context, id int) error
}
type BankRepo struct {
store *Store
}
func NewBankRepository(store *Store) BankRepository {
return &BankRepo{store: store}
}
func (r *BankRepo) CreateBank(ctx context.Context, bank *domain.Bank) error {
params := dbgen.CreateBankParams{
Slug: bank.Slug,
Swift: bank.Swift,
Name: bank.Name,
AcctLength: int32(bank.AcctLength),
CountryID: int32(bank.CountryID),
IsMobilemoney: pgtype.Int4{Int32: int32(bank.IsMobileMoney), Valid: true},
IsActive: int32(bank.IsActive),
IsRtgs: int32(bank.IsRTGS),
Active: int32(bank.Active),
Is24hrs: pgtype.Int4{Int32: int32(bank.Is24Hrs), Valid: true},
Currency: bank.Currency,
BankLogo: pgtype.Text{String: bank.BankLogo, Valid: true},
}
createdBank, err := r.store.queries.CreateBank(ctx, params)
if err != nil {
return err
}
// Update the ID and timestamps on the passed struct
bank.ID = int(createdBank.ID)
bank.CreatedAt = createdBank.CreatedAt.Time
bank.UpdatedAt = createdBank.UpdatedAt.Time
return nil
}
func (r *BankRepo) GetBankByID(ctx context.Context, id int) (*domain.Bank, error) {
dbBank, err := r.store.queries.GetBankByID(ctx, int64(id))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return mapDBBankToDomain(&dbBank), nil
}
func (r *BankRepo) GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error) {
params := dbgen.GetAllBanksParams{
CountryID: pgtype.Int4{},
IsActive: pgtype.Int4{},
}
if countryID != nil {
params.CountryID = pgtype.Int4{Int32: int32(*countryID), Valid: true}
}
if isActive != nil {
params.IsActive = pgtype.Int4{Int32: int32(*isActive), Valid: true}
}
dbBanks, err := r.store.queries.GetAllBanks(ctx, params)
if err != nil {
return nil, err
}
banks := make([]domain.Bank, len(dbBanks))
for i, b := range dbBanks {
banks[i] = *mapDBBankToDomain(&b)
}
return banks, nil
}
func (r *BankRepo) UpdateBank(ctx context.Context, bank *domain.Bank) error {
params := dbgen.UpdateBankParams{
ID: int64(bank.ID),
Slug: pgtype.Text{String: bank.Slug, Valid: true},
Swift: pgtype.Text{String: bank.Swift, Valid: true},
Name: pgtype.Text{String: bank.Name, Valid: true},
AcctLength: pgtype.Int4{Int32: int32(bank.AcctLength), Valid: true},
CountryID: pgtype.Int4{Int32: int32(bank.CountryID), Valid: true},
IsMobilemoney: pgtype.Int4{Int32: int32(bank.IsMobileMoney), Valid: true},
IsActive: pgtype.Int4{Int32: int32(bank.IsActive), Valid: true},
IsRtgs: pgtype.Int4{Int32: int32(bank.IsRTGS), Valid: true},
Active: pgtype.Int4{Int32: int32(bank.Active), Valid: true},
Is24hrs: pgtype.Int4{Int32: int32(bank.Is24Hrs), Valid: true},
Currency: pgtype.Text{String: bank.Currency, Valid: true},
BankLogo: pgtype.Text{String: bank.BankLogo, Valid: true},
}
updatedBank, err := r.store.queries.UpdateBank(ctx, params)
if err != nil {
return err
}
// update timestamps in domain struct
bank.UpdatedAt = updatedBank.UpdatedAt.Time
return nil
}
func (r *BankRepo) DeleteBank(ctx context.Context, id int) error {
return r.store.queries.DeleteBank(ctx, int64(id))
}
// Helper to map DB struct to domain
func mapDBBankToDomain(dbBank *dbgen.Bank) *domain.Bank {
return &domain.Bank{
ID: int(dbBank.ID),
Slug: dbBank.Slug,
Swift: dbBank.Swift,
Name: dbBank.Name,
AcctLength: int(dbBank.AcctLength),
CountryID: int(dbBank.CountryID),
IsMobileMoney: int(dbBank.IsMobilemoney.Int32),
IsActive: int(dbBank.IsActive),
IsRTGS: int(dbBank.IsRtgs),
Active: int(dbBank.Active),
Is24Hrs: int(dbBank.Is24hrs.Int32),
CreatedAt: dbBank.CreatedAt.Time,
UpdatedAt: dbBank.UpdatedAt.Time,
Currency: dbBank.Currency,
BankLogo: dbBank.BankLogo.String,
}
}

View File

@ -0,0 +1,65 @@
package repository
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
)
type ReportedIssueRepository interface {
CreateReportedIssue(ctx context.Context, arg dbgen.CreateReportedIssueParams) (dbgen.ReportedIssue, error)
ListReportedIssues(ctx context.Context, limit, offset int32) ([]dbgen.ReportedIssue, error)
ListReportedIssuesByCustomer(ctx context.Context, customerID int64, limit, offset int32) ([]dbgen.ReportedIssue, error)
CountReportedIssues(ctx context.Context) (int64, error)
CountReportedIssuesByCustomer(ctx context.Context, customerID int64) (int64, error)
UpdateReportedIssueStatus(ctx context.Context, id int64, status string) error
DeleteReportedIssue(ctx context.Context, id int64) error
}
type ReportedIssueRepo struct {
store *Store
}
func NewReportedIssueRepository(store *Store) ReportedIssueRepository {
return &ReportedIssueRepo{store: store}
}
func (s *ReportedIssueRepo) CreateReportedIssue(ctx context.Context, arg dbgen.CreateReportedIssueParams) (dbgen.ReportedIssue, error) {
return s.store.queries.CreateReportedIssue(ctx, arg)
}
func (s *ReportedIssueRepo) ListReportedIssues(ctx context.Context, limit, offset int32) ([]dbgen.ReportedIssue, error) {
params := dbgen.ListReportedIssuesParams{
Limit: limit,
Offset: offset,
}
return s.store.queries.ListReportedIssues(ctx, params)
}
func (s *ReportedIssueRepo) ListReportedIssuesByCustomer(ctx context.Context, customerID int64, limit, offset int32) ([]dbgen.ReportedIssue, error) {
params := dbgen.ListReportedIssuesByCustomerParams{
CustomerID: customerID,
Limit: limit,
Offset: offset,
}
return s.store.queries.ListReportedIssuesByCustomer(ctx, params)
}
func (s *ReportedIssueRepo) CountReportedIssues(ctx context.Context) (int64, error) {
return s.store.queries.CountReportedIssues(ctx)
}
func (s *ReportedIssueRepo) CountReportedIssuesByCustomer(ctx context.Context, customerID int64) (int64, error) {
return s.store.queries.CountReportedIssuesByCustomer(ctx, customerID)
}
func (s *ReportedIssueRepo) UpdateReportedIssueStatus(ctx context.Context, id int64, status string) error {
return s.store.queries.UpdateReportedIssueStatus(ctx, dbgen.UpdateReportedIssueStatusParams{
ID: id,
Status: status,
})
}
func (s *ReportedIssueRepo) DeleteReportedIssue(ctx context.Context, id int64) error {
return s.store.queries.DeleteReportedIssue(ctx, id)
}

View File

@ -15,6 +15,7 @@ func (s *Store) SaveLeague(ctx context.Context, l domain.League) error {
CountryCode: pgtype.Text{String: l.CountryCode, Valid: true},
Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true},
IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true},
IsFeatured: pgtype.Bool{Bool: l.IsFeatured, Valid: true},
SportID: l.SportID,
})
}
@ -33,6 +34,10 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) (
Bool: filter.IsActive.Value,
Valid: filter.IsActive.Valid,
},
IsFeatured: pgtype.Bool{
Bool: filter.IsFeatured.Value,
Valid: filter.IsFeatured.Valid,
},
Limit: pgtype.Int4{
Int32: int32(filter.Limit.Value),
Valid: filter.Limit.Valid,
@ -54,12 +59,35 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) (
CountryCode: league.CountryCode.String,
Bet365ID: league.Bet365ID.Int32,
IsActive: league.IsActive.Bool,
IsFeatured: league.IsFeatured.Bool,
SportID: league.SportID,
}
}
return leagues, nil
}
func (s *Store) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) {
l, err := s.queries.GetFeaturedLeagues(ctx)
if err != nil {
return nil, err
}
leagues := make([]domain.League, len(l))
for i, league := range l {
leagues[i] = domain.League{
ID: league.ID,
Name: league.Name,
CountryCode: league.CountryCode.String,
Bet365ID: league.Bet365ID.Int32,
IsActive: league.IsActive.Bool,
SportID: league.SportID,
}
}
return leagues, nil
}
func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, error) {
return s.queries.CheckLeagueSupport(ctx, leagueID)
}
@ -93,6 +121,10 @@ func (s *Store) UpdateLeague(ctx context.Context, league domain.UpdateLeague) er
Bool: league.IsActive.Value,
Valid: league.IsActive.Valid,
},
IsFeatured: pgtype.Bool{
Bool: league.IsFeatured.Value,
Valid: league.IsActive.Valid,
},
SportID: pgtype.Int4{
Int32: league.SportID.Value,
Valid: league.SportID.Valid,

View File

@ -317,6 +317,40 @@ func (s *Store) CountUnreadNotifications(ctx context.Context, userID int64) (int
return count, nil
}
func (s *Store) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
dbCompany, err := s.queries.GetCompanyByWalletID(ctx, walletID)
if err != nil {
return domain.Company{}, err
}
return domain.Company{
ID: dbCompany.ID,
Name: dbCompany.Name,
AdminID: dbCompany.AdminID,
WalletID: dbCompany.WalletID,
}, nil
}
func (s *Store) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
dbBranch, err := s.queries.GetBranchByWalletID(ctx, walletID)
if err != nil {
return domain.Branch{}, err
}
return domain.Branch{
ID: dbBranch.ID,
Name: dbBranch.Name,
Location: dbBranch.Location,
IsActive: dbBranch.IsActive,
WalletID: dbBranch.WalletID,
BranchManagerID: dbBranch.BranchManagerID,
CompanyID: dbBranch.CompanyID,
IsSelfOwned: dbBranch.IsSelfOwned,
// Creat: dbBranch.CreatedAt.Time,
// UpdatedAt: dbBranch.UpdatedAt.Time,
}, nil
}
// func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
// dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{
// Limit: int32(limit),

View File

@ -2,15 +2,28 @@ package repository
import (
"context"
"fmt"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
type ReportRepository interface {
GenerateReport(timeFrame domain.TimeFrame, start, end time.Time) (*domain.Report, error)
SaveReport(report *domain.Report) error
FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit int) ([]*domain.Report, error)
GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error)
GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error)
GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error)
GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error)
GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error)
GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error)
}
type ReportRepo struct {
@ -105,3 +118,117 @@ func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit in
return reports, nil
}
func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) {
params := dbgen.GetTotalBetsMadeInRangeParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
}
return r.store.queries.GetTotalBetsMadeInRange(ctx, params)
}
func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) {
params := dbgen.GetTotalCashBacksInRangeParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
}
value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params)
if err != nil {
return 0, err
}
return parseFloat(value)
}
func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) {
params := dbgen.GetTotalCashMadeInRangeParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
}
value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params)
if err != nil {
return 0, err
}
return parseFloat(value)
}
func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) {
params := dbgen.GetTotalCashOutInRangeParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
}
value, err := r.store.queries.GetTotalCashOutInRange(ctx, params)
if err != nil {
return 0, err
}
return parseFloat(value)
}
func (r *ReportRepo) GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error) {
params := dbgen.GetWalletTransactionsInRangeParams{
CreatedAt: ToPgTimestamp(from),
CreatedAt_2: ToPgTimestamp(to),
}
return r.store.queries.GetWalletTransactionsInRange(ctx, params)
}
func (r *ReportRepo) GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) {
params := dbgen.GetAllTicketsInRangeParams{
CreatedAt: ToPgTimestamp(from),
CreatedAt_2: ToPgTimestamp(to),
}
return r.store.queries.GetAllTicketsInRange(ctx, params)
}
func (r *ReportRepo) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) {
params := dbgen.GetVirtualGameSummaryInRangeParams{
CreatedAt: ToPgTimestamptz(from),
CreatedAt_2: ToPgTimestamptz(to),
}
return r.store.queries.GetVirtualGameSummaryInRange(ctx, params)
}
func ToPgTimestamp(t time.Time) pgtype.Timestamp {
return pgtype.Timestamp{Time: t, Valid: true}
}
func ToPgTimestamptz(t time.Time) pgtype.Timestamptz {
return pgtype.Timestamptz{Time: t, Valid: true}
}
func parseFloat(value interface{}) (float64, error) {
switch v := value.(type) {
case float64:
return v, nil
case int64:
return float64(v), nil
case pgtype.Numeric:
if !v.Valid {
return 0, nil
}
f, err := v.Float64Value()
if err != nil {
return 0, fmt.Errorf("failed to convert pgtype.Numeric to float64: %w", err)
}
return f.Float64, nil
case nil:
return 0, nil
default:
return 0, fmt.Errorf("unexpected type %T for value: %+v", v, v)
}
}
func (r *ReportRepo) GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) {
params := dbgen.GetCompanyWiseReportParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
}
return r.store.queries.GetCompanyWiseReport(ctx, params)
}
func (r *ReportRepo) GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) {
params := dbgen.GetBranchWiseReportParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
}
return r.store.queries.GetBranchWiseReport(ctx, params)
}

View File

@ -0,0 +1,127 @@
package repository
import (
"context"
"fmt"
"strconv"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
type DBSettingList struct {
MaxNumberOfOutcomes domain.ValidInt64
BetAmountLimit domain.ValidInt64
DailyTicketPerIP domain.ValidInt64
TotalWinningLimit domain.ValidInt64
}
func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) {
var dbSettingList DBSettingList
var int64SettingsMap = map[string]*domain.ValidInt64{
"max_number_of_outcomes": &dbSettingList.MaxNumberOfOutcomes,
"bet_amount_limit": &dbSettingList.BetAmountLimit,
"daily_ticket_limit": &dbSettingList.DailyTicketPerIP,
"total_winnings_limit": &dbSettingList.TotalWinningLimit,
}
for _, setting := range settings {
is_setting_unknown := true
for key, dbSetting := range int64SettingsMap {
if setting.Key == key {
value, err := strconv.ParseInt(setting.Value, 10, 64)
if err != nil {
return domain.SettingList{}, err
}
*dbSetting = domain.ValidInt64{
Value: value,
Valid: true,
}
is_setting_unknown = false
}
}
if is_setting_unknown {
domain.MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key))
}
}
for key, dbSetting := range int64SettingsMap {
if !dbSetting.Valid {
fmt.Printf("setting value not found on database: %v \n", key)
domain.MongoDBLogger.Warn("setting value not found on database", zap.String("setting", key))
}
}
return domain.SettingList{
MaxNumberOfOutcomes: dbSettingList.MaxNumberOfOutcomes.Value,
BetAmountLimit: domain.Currency(dbSettingList.BetAmountLimit.Value),
DailyTicketPerIP: dbSettingList.DailyTicketPerIP.Value,
TotalWinningLimit: domain.Currency(dbSettingList.TotalWinningLimit.Value),
}, nil
}
func (s *Store) GetSettingList(ctx context.Context) (domain.SettingList, error) {
settings, err := s.queries.GetSettings(ctx)
if err != nil {
domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err))
}
return GetDBSettingList(settings)
}
func (s *Store) GetSettings(ctx context.Context) ([]domain.Setting, error) {
settings, err := s.queries.GetSettings(ctx)
if err != nil {
domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err))
}
var result []domain.Setting = make([]domain.Setting, 0, len(settings))
for _, setting := range settings {
result = append(result, domain.Setting{
Key: setting.Key,
Value: setting.Value,
UpdatedAt: setting.UpdatedAt.Time,
})
}
return result, nil
}
func (s *Store) GetSetting(ctx context.Context, key string) (domain.Setting, error) {
dbSetting, err := s.queries.GetSetting(ctx, key)
if err != nil {
domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err))
}
result := domain.Setting{
Key: dbSetting.Key,
Value: dbSetting.Value,
UpdatedAt: dbSetting.UpdatedAt.Time,
}
return result, nil
}
func (s *Store) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) {
dbSetting, err := s.queries.SaveSetting(ctx, dbgen.SaveSettingParams{
Key: key,
Value: value,
})
if err != nil {
domain.MongoDBLogger.Error("failed to update setting", zap.String("key", key), zap.String("value", value), zap.Error(err))
return domain.Setting{}, err
}
setting := domain.Setting{
Key: dbSetting.Key,
Value: dbSetting.Value,
}
return setting, err
}

View File

@ -10,17 +10,27 @@ import (
func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer {
return domain.Transfer{
ID: transfer.ID,
Amount: domain.Currency(transfer.Amount.Int64),
Type: domain.TransferType(transfer.Type.String),
Verified: transfer.Verified.Bool,
ReceiverWalletID: transfer.ReceiverWalletID.Int64,
SenderWalletID: transfer.SenderWalletID.Int64,
ID: transfer.ID,
Amount: domain.Currency(transfer.Amount.Int64),
Type: domain.TransferType(transfer.Type.String),
Verified: transfer.Verified.Bool,
ReceiverWalletID: domain.ValidInt64{
Value: transfer.ReceiverWalletID.Int64,
Valid: transfer.ReceiverWalletID.Valid,
},
SenderWalletID: domain.ValidInt64{
Value: transfer.SenderWalletID.Int64,
Valid: transfer.SenderWalletID.Valid,
},
CashierID: domain.ValidInt64{
Value: transfer.CashierID.Int64,
Valid: transfer.CashierID.Valid,
},
PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod.String),
PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod.String),
ReferenceNumber: transfer.ReferenceNumber,
Status: transfer.Status.String,
CreatedAt: transfer.CreatedAt.Time,
UpdatedAt: transfer.UpdatedAt.Time,
}
}
@ -29,17 +39,19 @@ func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferP
Amount: pgtype.Int8{Int64: int64(transfer.Amount), Valid: true},
Type: pgtype.Text{String: string(transfer.Type), Valid: true},
ReceiverWalletID: pgtype.Int8{
Int64: transfer.ReceiverWalletID,
Valid: true,
Int64: transfer.ReceiverWalletID.Value,
Valid: transfer.ReceiverWalletID.Valid,
},
SenderWalletID: pgtype.Int8{
Int64: transfer.SenderWalletID,
Valid: true,
Int64: transfer.SenderWalletID.Value,
Valid: transfer.SenderWalletID.Valid,
},
CashierID: pgtype.Int8{
Int64: transfer.CashierID.Value,
Valid: transfer.CashierID.Valid,
},
ReferenceNumber: string(transfer.ReferenceNumber),
PaymentMethod: pgtype.Text{String: string(transfer.PaymentMethod), Valid: true},
}
}
@ -64,6 +76,7 @@ func (s *Store) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error)
}
return result, nil
}
func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) {
transfers, err := s.queries.GetTransfersByWallet(ctx, pgtype.Int8{Int64: walletID, Valid: true})
if err != nil {
@ -79,7 +92,7 @@ func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]dom
}
func (s *Store) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) {
transfer, err := s.queries.GetTransferByReference(ctx, pgtype.Text{String: reference, Valid: true})
transfer, err := s.queries.GetTransferByReference(ctx, reference)
if err != nil {
return domain.Transfer{}, nil
}

View File

@ -19,8 +19,13 @@ type VirtualGameRepository interface {
GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error)
UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error
// WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error
AddFavoriteGame(ctx context.Context, userID, gameID int64) error
RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error
ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error)
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error)
CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error
}
type VirtualGameRepo struct {
@ -36,6 +41,26 @@ func NewVirtualGameRepository(store *Store) VirtualGameRepository {
return &VirtualGameRepo{store: store}
}
func (r *VirtualGameRepo) AddFavoriteGame(ctx context.Context, userID, gameID int64) error {
params := dbgen.AddFavoriteGameParams{
UserID: userID,
GameID: gameID,
}
return r.store.queries.AddFavoriteGame(ctx, params)
}
func (r *VirtualGameRepo) RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error {
params := dbgen.RemoveFavoriteGameParams{
UserID: userID,
GameID: gameID,
}
return r.store.queries.RemoveFavoriteGame(ctx, params)
}
func (r *VirtualGameRepo) ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) {
return r.store.queries.ListFavoriteGames(ctx, userID)
}
func (r *VirtualGameRepo) CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error {
params := dbgen.CreateVirtualGameSessionParams{
UserID: session.UserID,
@ -92,6 +117,21 @@ func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *
return err
}
func (r *VirtualGameRepo) CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error {
params := dbgen.CreateVirtualGameHistoryParams{
SessionID: pgtype.Text{String: his.SessionID, Valid: true},
UserID: his.UserID,
// WalletID: pgtype.Int8{Int64: *his.WalletID, Valid: true},
TransactionType: his.TransactionType,
Amount: his.Amount,
Currency: his.Currency,
ExternalTransactionID: his.ExternalTransactionID,
Status: his.Status,
}
_, err := r.store.queries.CreateVirtualGameHistory(ctx, params)
return err
}
func (r *VirtualGameRepo) GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) {
dbTx, err := r.store.queries.GetVirtualGameTransactionByExternalID(ctx, externalID)
if err != nil {
@ -153,6 +193,24 @@ func (r *VirtualGameRepo) GetGameCounts(ctx context.Context, filter domain.Repor
return total, active, inactive, nil
}
func (r *VirtualGameRepo) GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error) {
query := `SELECT game_id FROM virtual_game_histories WHERE user_id = $1 AND transaction_type = 'BET' ORDER BY created_at DESC LIMIT 100`
rows, err := r.store.conn.Query(ctx, query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var history []domain.VirtualGameHistory
for rows.Next() {
var tx domain.VirtualGameHistory
if err := rows.Scan(&tx.GameID); err == nil {
history = append(history, tx)
}
}
return history, nil
}
// func (r *VirtualGameRepo) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
// _, tx, err := r.store.BeginTx(ctx)
// if err != nil {

View File

@ -47,7 +47,7 @@ func convertCreateCustomerWallet(customerWallet domain.CreateCustomerWallet) dbg
}
}
func convertDBGetCustomerWallet(customerWallet dbgen.GetCustomerWalletRow) domain.GetCustomerWallet {
func convertDBGetCustomerWallet(customerWallet dbgen.CustomerWalletDetail) domain.GetCustomerWallet {
return domain.GetCustomerWallet{
ID: customerWallet.ID,
RegularID: customerWallet.RegularID,
@ -55,9 +55,14 @@ func convertDBGetCustomerWallet(customerWallet dbgen.GetCustomerWalletRow) domai
StaticID: customerWallet.StaticID,
StaticBalance: domain.Currency(customerWallet.StaticBalance),
CustomerID: customerWallet.CustomerID,
RegularIsActive: customerWallet.RegularIsActive,
StaticIsActive: customerWallet.StaticIsActive,
RegularUpdatedAt: customerWallet.RegularUpdatedAt.Time,
StaticUpdatedAt: customerWallet.StaticUpdatedAt.Time,
CreatedAt: customerWallet.CreatedAt.Time,
FirstName: customerWallet.FirstName,
LastName: customerWallet.LastName,
PhoneNumber: customerWallet.PhoneNumber.String,
}
}
@ -115,6 +120,19 @@ func (s *Store) GetWalletsByUser(ctx context.Context, userID int64) ([]domain.Wa
return result, nil
}
func (s *Store) GetAllCustomerWallets(ctx context.Context) ([]domain.GetCustomerWallet, error) {
customerWallets, err := s.queries.GetAllCustomerWallet(ctx)
if err != nil {
return nil, err
}
var result []domain.GetCustomerWallet = make([]domain.GetCustomerWallet, 0, len(customerWallets))
for _, wallet := range customerWallets {
result = append(result, convertDBGetCustomerWallet(wallet))
}
return result, nil
}
func (s *Store) GetCustomerWallet(ctx context.Context, customerID int64) (domain.GetCustomerWallet, error) {
customerWallet, err := s.queries.GetCustomerWallet(ctx, customerID)
@ -257,3 +275,4 @@ func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter)
return total, nil
}

View File

@ -15,12 +15,14 @@ type BetStore interface {
GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error)
GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error)
GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error)
GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error)
GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error)
GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error)
GetBetCount(ctx context.Context, userID int64, outcomesHash string) (int64, error)
UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)
DeleteBet(ctx context.Context, id int64) error
UpdateBetOutcomeStatusByBetID(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)
UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error)
GetBetSummary(ctx context.Context, filter domain.ReportFilter) (
totalStakes domain.Currency,

View File

@ -3,13 +3,17 @@ package bet
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math/big"
random "math/rand"
"sort"
"strconv"
"strings"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -25,6 +29,12 @@ var (
ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events")
ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending")
ErrEventHasBeenRemoved = errors.New("Event has been removed")
ErrEventHasNotEnded = errors.New("Event has not ended yet")
ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid")
ErrBranchIDRequired = errors.New("Branch ID required for this role")
ErrOutcomeLimit = errors.New("Too many outcomes on a single bet")
ErrTotalBalanceNotEnough = errors.New("Total Wallet balance is insufficient to create bet")
)
type Service struct {
@ -37,7 +47,15 @@ type Service struct {
mongoLogger *zap.Logger
}
func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.ServiceImpl, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger, mongoLogger *zap.Logger) *Service {
func NewService(
betStore BetStore,
eventSvc event.Service,
prematchSvc odds.ServiceImpl,
walletSvc wallet.Service,
branchSvc branch.Service,
logger *slog.Logger,
mongoLogger *zap.Logger,
) *Service {
return &Service{
betStore: betStore,
eventSvc: eventSvc,
@ -49,13 +67,6 @@ func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.Serv
}
}
var (
ErrEventHasNotEnded = errors.New("Event has not ended yet")
ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid")
ErrBranchIDRequired = errors.New("Branch ID required for this role")
ErrOutcomeLimit = errors.New("Too many outcomes on a single bet")
)
func (s *Service) GenerateCashoutID() (string, error) {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
const length int = 13
@ -196,6 +207,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
var totalOdds float32 = 1
for _, outcomeReq := range req.Outcomes {
fmt.Println("reqq: ", outcomeReq)
newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
if err != nil {
s.mongoLogger.Error("failed to generate outcome",
@ -211,6 +223,23 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
outcomes = append(outcomes, newOutcome)
}
outcomesHash, err := generateOutcomeHash(outcomes)
if err != nil {
s.mongoLogger.Error("failed to generate outcome hash",
zap.Int64("user_id", userID),
zap.Error(err),
)
return domain.CreateBetRes{}, err
}
count, err := s.GetBetCount(ctx, userID, outcomesHash)
if err != nil {
return domain.CreateBetRes{}, err
}
if count == 2 {
return domain.CreateBetRes{}, fmt.Errorf("bet already pleaced twice")
}
cashoutID, err := s.GenerateCashoutID()
if err != nil {
s.mongoLogger.Error("failed to generate cashout ID",
@ -221,12 +250,13 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
}
newBet := domain.CreateBet{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
CashoutID: cashoutID,
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
CashoutID: cashoutID,
OutcomesHash: outcomesHash,
}
switch role {
@ -241,7 +271,11 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
}
deductedAmount := req.Amount / 10
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
_, err = s.walletSvc.DeductFromWallet(ctx,
branch.WalletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{
Value: userID,
Valid: true,
}, domain.TRANSFER_DIRECT)
if err != nil {
s.mongoLogger.Error("failed to deduct from wallet",
zap.Int64("wallet_id", branch.WalletID),
@ -274,7 +308,10 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
}
deductedAmount := req.Amount / 10
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
_, err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{
Value: userID,
Valid: true,
}, domain.TRANSFER_DIRECT)
if err != nil {
s.mongoLogger.Error("wallet deduction failed",
zap.Int64("wallet_id", branch.WalletID),
@ -290,7 +327,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
newBet.IsShopBet = true
case domain.RoleCustomer:
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
wallets, err := s.walletSvc.GetCustomerWallet(ctx, userID)
if err != nil {
s.mongoLogger.Error("failed to get customer wallets",
zap.Int64("user_id", userID),
@ -298,16 +335,52 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
)
return domain.CreateBetRes{}, err
}
if req.Amount < wallets.RegularBalance.Float32() {
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID,
domain.ToCurrency(req.Amount), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT)
if err != nil {
s.mongoLogger.Error("wallet deduction failed for customer regular wallet",
zap.Int64("customer_id", wallets.CustomerID),
zap.Int64("customer_wallet_id", wallets.ID),
zap.Int64("regular wallet_id", wallets.RegularID),
zap.Float32("amount", req.Amount),
zap.Error(err),
)
return domain.CreateBetRes{}, err
}
} else {
combinedBalance := wallets.RegularBalance + wallets.StaticBalance
if req.Amount > combinedBalance.Float32() {
return domain.CreateBetRes{}, ErrTotalBalanceNotEnough
}
// Empty the regular balance
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID,
wallets.RegularBalance, domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT)
if err != nil {
s.mongoLogger.Error("wallet deduction failed for customer regular wallet",
zap.Int64("customer_id", wallets.CustomerID),
zap.Int64("customer_wallet_id", wallets.ID),
zap.Int64("regular wallet_id", wallets.RegularID),
zap.Float32("amount", req.Amount),
zap.Error(err),
)
return domain.CreateBetRes{}, err
}
// Empty remaining from static balance
remainingAmount := wallets.RegularBalance - domain.Currency(req.Amount)
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.StaticID,
remainingAmount, domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT)
if err != nil {
s.mongoLogger.Error("wallet deduction failed for customer static wallet",
zap.Int64("customer_id", wallets.CustomerID),
zap.Int64("customer_wallet_id", wallets.ID),
zap.Int64("static wallet_id", wallets.StaticID),
zap.Float32("amount", req.Amount),
zap.Error(err),
)
return domain.CreateBetRes{}, err
}
userWallet := wallets[0]
err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID, domain.ToCurrency(req.Amount))
if err != nil {
s.mongoLogger.Error("wallet deduction failed for customer",
zap.Int64("wallet_id", userWallet.ID),
zap.Float32("amount", req.Amount),
zap.Error(err),
)
return domain.CreateBetRes{}, err
}
newBet.UserID = domain.ValidInt64{Value: userID, Valid: true}
@ -321,6 +394,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type")
}
fmt.Println("Bet is: ", newBet)
bet, err := s.CreateBet(ctx, newBet)
if err != nil {
s.mongoLogger.Error("failed to create bet",
@ -636,6 +710,10 @@ func (s *Service) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.Ge
return s.betStore.GetBetByUserID(ctx, UserID)
}
func (s *Service) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
return s.betStore.GetBetCount(ctx, UserID, outcomesHash)
}
func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {
return s.betStore.UpdateCashOut(ctx, id, cashedOut)
}
@ -676,7 +754,8 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
amount = bet.Amount
}
err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount)
_, err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil {
s.mongoLogger.Error("failed to add winnings to wallet",
zap.Int64("wallet_id", customerWallet.RegularID),
@ -782,6 +861,55 @@ func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status d
}
func (s *Service) DeleteBet(ctx context.Context, id int64) error {
return s.betStore.DeleteBet(ctx, id)
func (s *Service) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) {
outcomes, err := s.betStore.UpdateBetOutcomeStatusForEvent(ctx, eventID, status)
if err != nil {
s.mongoLogger.Error("failed to update bet outcome status",
zap.Int64("eventID", eventID),
zap.Error(err),
)
return nil, err
}
return outcomes, nil
}
func (s *Service) SetBetToRemoved(ctx context.Context, id int64) error {
_, err := s.betStore.UpdateBetOutcomeStatusByBetID(ctx, id, domain.OUTCOME_STATUS_VOID)
if err != nil {
s.mongoLogger.Error("failed to update bet outcome to void", zap.Int64("id", id),
zap.Error(err),
)
return err
}
err = s.betStore.UpdateStatus(ctx, id, domain.OUTCOME_STATUS_VOID)
if err != nil {
s.mongoLogger.Error("failed to update bet to void", zap.Int64("id", id),
zap.Error(err),
)
return err
}
return nil
}
func generateOutcomeHash(outcomes []domain.CreateBetOutcome) (string, error) {
// should always be in the same order for producing the same hash
sort.Slice(outcomes, func(i, j int) bool {
if outcomes[i].EventID != outcomes[j].EventID {
return outcomes[i].EventID < outcomes[j].EventID
}
if outcomes[i].MarketID != outcomes[j].MarketID {
return outcomes[i].MarketID < outcomes[j].MarketID
}
return outcomes[i].OddID < outcomes[j].OddID
})
var sb strings.Builder
for _, o := range outcomes {
sb.WriteString(fmt.Sprintf("%d-%d-%d;", o.EventID, o.MarketID, o.OddID))
}
sum := sha256.Sum256([]byte(sb.String()))
return hex.EncodeToString(sum[:]), nil
}

View File

@ -11,7 +11,7 @@ type BranchStore interface {
GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error)
GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error)
GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error)
GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error)
GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error)
SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error)
UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error)
DeleteBranch(ctx context.Context, id int64) error

View File

@ -1,4 +1,4 @@
package branch
package branch
import (
"context"
@ -42,8 +42,8 @@ func (s *Service) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
func (s *Service) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) {
return s.branchStore.GetBranchOperations(ctx, branchID)
}
func (s *Service) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) {
return s.branchStore.GetAllBranches(ctx)
func (s *Service) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) {
return s.branchStore.GetAllBranches(ctx, filter)
}
func (s *Service) GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) {

View File

@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
@ -30,9 +31,9 @@ func NewClient(baseURL, secretKey string) *Client {
func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) {
payload := map[string]interface{}{
"amount": req.Amount,
"currency": req.Currency,
"email": req.Email,
"amount": fmt.Sprintf("%.2f", float64(req.Amount)/100),
"currency": req.Currency,
// "email": req.Email,
"first_name": req.FirstName,
"last_name": req.LastName,
"tx_ref": req.TxRef,
@ -40,6 +41,8 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR
"return_url": req.ReturnURL,
}
fmt.Printf("\n\nChapa Payload: %+v\n\n", payload)
payloadBytes, err := json.Marshal(payload)
if err != nil {
return domain.ChapaDepositResponse{}, fmt.Errorf("failed to marshal payload: %w", err)
@ -50,6 +53,8 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR
return domain.ChapaDepositResponse{}, fmt.Errorf("failed to create request: %w", err)
}
fmt.Printf("\n\nBase URL is: %+v\n\n", c.baseURL)
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
@ -59,6 +64,11 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body) // <-- Add this
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)
}
@ -77,7 +87,7 @@ func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositR
return domain.ChapaDepositResponse{
CheckoutURL: response.Data.CheckoutURL,
// Reference: req.TxRef,
Reference: req.TxRef,
}, nil
}
@ -165,6 +175,51 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain
}, nil
}
func (c *Client) ManualVerifyTransfer(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
url := fmt.Sprintf("%s/transfers/verify/%s", c.baseURL, txRef)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.secretKey)
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 {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
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 {
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
}
return &domain.ChapaVerificationResponse{
Status: string(status),
Amount: response.Amount,
Currency: response.Currency,
}, nil
}
func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil)
if err != nil {
@ -213,10 +268,6 @@ func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error)
}
func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawalRequest) (bool, error) {
// base, err := url.Parse(c.baseURL)
// if err != nil {
// return false, fmt.Errorf("invalid base URL: %w", err)
// }
endpoint := c.baseURL + "/transfers"
fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint)
@ -230,7 +281,9 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
return false, fmt.Errorf("failed to create request: %w", err)
}
c.setHeaders(httpReq)
// Set headers here
httpReq.Header.Set("Authorization", "Bearer "+c.secretKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
@ -239,7 +292,8 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, fmt.Errorf("chapa api returned status: %d", resp.StatusCode)
body, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("chapa api returned status: %d - %s", resp.StatusCode, string(body))
}
var response domain.ChapaWithdrawalResponse
@ -247,7 +301,7 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
return false, fmt.Errorf("failed to decode response: %w", err)
}
return response.Status == string(domain.WithdrawalStatusProcessing), nil
return response.Status == string(domain.WithdrawalStatusSuccessful), nil
}
func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) {

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -31,6 +32,7 @@ func NewService(
transferStore wallet.TransferStore,
walletStore wallet.Service,
userStore user.UserStore,
cfg *config.Config,
chapaClient *Client,
) *Service {
@ -38,6 +40,7 @@ func NewService(
transferStore: transferStore,
walletStore: walletStore,
userStore: userStore,
cfg: cfg,
chapaClient: chapaClient,
}
}
@ -58,7 +61,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
var senderWallet domain.Wallet
// Generate unique reference
reference := uuid.New().String()
// reference := uuid.New().String()
reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, uuid.New().String())
senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
if err != nil {
return "", fmt.Errorf("failed to get sender wallets: %w", err)
@ -83,25 +88,28 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
PaymentMethod: domain.TRANSFER_CHAPA,
ReferenceNumber: reference,
// ReceiverWalletID: 1,
SenderWalletID: senderWallet.ID,
Verified: false,
SenderWalletID: domain.ValidInt64{
Value: senderWallet.ID,
Valid: true,
},
Verified: false,
}
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
return "", fmt.Errorf("failed to save payment: %w", err)
}
// Initialize payment with Chapa
response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{
payload := domain.ChapaDepositRequest{
Amount: amount,
Currency: "ETB",
Email: user.Email,
FirstName: user.FirstName,
LastName: user.LastName,
TxRef: reference,
CallbackURL: "https://fortunebet.com/api/v1/payments/callback",
ReturnURL: "https://fortunebet.com/api/v1/payment-success",
})
CallbackURL: s.cfg.CHAPA_CALLBACK_URL,
ReturnURL: s.cfg.CHAPA_RETURN_URL,
}
// Initialize payment with Chapa
response, err := s.chapaClient.InitializePayment(ctx, payload)
fmt.Printf("\n\nChapa payload is: %+v\n\n", payload)
if err != nil {
// Update payment status to failed
@ -109,8 +117,17 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
return "", fmt.Errorf("failed to initialize payment: %w", err)
}
tempTransfer, err := s.transferStore.CreateTransfer(ctx, transfer)
if err != nil {
return "", fmt.Errorf("failed to save payment: %w", err)
}
fmt.Printf("\n\nTemp transfer is: %v\n\n", tempTransfer)
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)
@ -150,14 +167,16 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
reference := uuid.New().String()
createTransfer := domain.CreateTransfer{
Amount: domain.Currency(amount),
Type: domain.WITHDRAW,
ReceiverWalletID: 1,
SenderWalletID: withdrawWallet.ID,
Status: string(domain.PaymentStatusPending),
Verified: false,
ReferenceNumber: reference,
PaymentMethod: domain.TRANSFER_CHAPA,
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)
@ -177,12 +196,16 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
}
success, err := s.chapaClient.InitiateTransfer(ctx, transferReq)
if err != nil || !success {
// Update withdrawal status to failed
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)
@ -204,42 +227,68 @@ func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error)
return banks, nil
}
func (s *Service) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
// First check if we already have a verified record
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 && transfer.Verified {
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, // Convert from cents/kobo
Amount: float64(transfer.Amount) / 100,
Currency: "ETB",
}, nil
}
// If not verified or not found, verify with Chapa
verification, err := s.chapaClient.VerifyPayment(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify payment: %w", err)
// Validate sender wallet
if !transfer.SenderWalletID.Valid {
return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
}
// Update our records if payment is successful
if verification.Status == domain.PaymentStatusCompleted {
err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true)
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 update verification status: %w", err)
return nil, fmt.Errorf("failed to verify deposit with Chapa: %w", err)
}
// Credit user's wallet
err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID, transfer.Amount)
if err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %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
if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{}); 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 &domain.ChapaVerificationResponse{
Status: string(verification.Status),
Amount: float64(verification.Amount),
Currency: verification.Currency,
}, nil
return verification, nil
}
func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error {
@ -265,13 +314,18 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
// verified = true
// }
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
return fmt.Errorf("failed to update payment status: %w", err)
}
// If payment is completed, credit user's wallet
if transfer.Status == string(domain.PaymentStatusCompleted) {
if err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID, payment.Amount); err != nil {
if transfer.Status == string(domain.PaymentStatusSuccessful) {
if err := s.transferStore.UpdateTransferVerification(ctx, payment.ID, true); err != nil {
return fmt.Errorf("failed to update payment status: %w", err)
}
if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{
ReferenceNumber: domain.ValidString{
Value: transfer.Reference,
},
}); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err)
}
}
@ -302,13 +356,12 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
// verified = true
// }
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 wallet
if payment.Status == string(domain.PaymentStatusFailed) {
if err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID, transfer.Amount); err != nil {
if payment.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
} else {
if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err)
}
}

View File

@ -7,6 +7,7 @@ import (
"io"
"log"
"net/http"
"slices"
"strconv"
"sync"
"time"
@ -202,8 +203,10 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
}
func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) {
sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91}
// sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91}
sportIDs := []int{1}
// TODO: Add the league skipping again when we have dynamic leagues
// b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
// if err != nil {
// log.Printf("❌ Failed to open leagues file %v", err)
@ -214,7 +217,6 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
var page int = 0
var limit int = 200
var count int = 0
log.Printf("Sport ID %d", sportID)
for page <= totalPages {
page = page + 1
url := fmt.Sprintf(url, sportID, s.token, page)
@ -252,11 +254,13 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
// doesn't make sense to save and check back to back, but for now it can be here
// no this its fine to keep it here
// but change the league id to bet365 id later
//Automatically feature the league if its in the list
err = s.store.SaveLeague(ctx, domain.League{
ID: leagueID,
Name: ev.League.Name,
IsActive: true,
SportID: convertInt32(ev.SportID),
ID: leagueID,
Name: ev.League.Name,
IsActive: true,
IsFeatured: slices.Contains(domain.FeaturedLeagues, leagueID),
SportID: convertInt32(ev.SportID),
})
if err != nil {

View File

@ -0,0 +1 @@
package institutions

View File

@ -0,0 +1,44 @@
package institutions
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
)
type Service struct {
repo repository.BankRepository
}
func New(repo repository.BankRepository) *Service {
return &Service{repo: repo}
}
func (s *Service) Create(ctx context.Context, bank *domain.Bank) error {
return s.repo.CreateBank(ctx, bank)
}
func (s *Service) Update(ctx context.Context, bank *domain.Bank) error {
return s.repo.UpdateBank(ctx, bank)
}
func (s *Service) GetByID(ctx context.Context, id int64) (*domain.Bank, error) {
return s.repo.GetBankByID(ctx, int(id))
}
func (s *Service) Delete(ctx context.Context, id int64) error {
return s.repo.DeleteBank(ctx, int(id))
}
func (s *Service) List(ctx context.Context) ([]*domain.Bank, error) {
banks, err := s.repo.GetAllBanks(ctx, nil, nil)
if err != nil {
return nil, err
}
result := make([]*domain.Bank, len(banks))
for i := range banks {
result[i] = &banks[i]
}
return result, nil
}

View File

@ -0,0 +1,83 @@
package issuereporting
import (
"context"
"errors"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
)
type Service struct {
repo repository.ReportedIssueRepository
}
func New(repo repository.ReportedIssueRepository) *Service {
return &Service{repo: repo}
}
func (s *Service) CreateReportedIssue(ctx context.Context, issue domain.ReportedIssue) (domain.ReportedIssue, error) {
params := dbgen.CreateReportedIssueParams{
// Map fields from domain.ReportedIssue to dbgen.CreateReportedIssueParams here.
// Example:
// Title: issue.Title,
// Description: issue.Description,
// CustomerID: issue.CustomerID,
// Status: issue.Status,
// Add other fields as necessary.
}
dbIssue, err := s.repo.CreateReportedIssue(ctx, params)
if err != nil {
return domain.ReportedIssue{}, err
}
// Map dbgen.ReportedIssue to domain.ReportedIssue
reportedIssue := domain.ReportedIssue{
ID: dbIssue.ID,
Subject: dbIssue.Subject,
Description: dbIssue.Description,
CustomerID: dbIssue.CustomerID,
Status: dbIssue.Status,
CreatedAt: dbIssue.CreatedAt.Time,
UpdatedAt: dbIssue.UpdatedAt.Time,
// Add other fields as necessary
}
return reportedIssue, nil
}
func (s *Service) GetIssuesForCustomer(ctx context.Context, customerID int64, limit, offset int) ([]domain.ReportedIssue, error) {
dbIssues, err := s.repo.ListReportedIssuesByCustomer(ctx, customerID, int32(limit), int32(offset))
if err != nil {
return nil, err
}
reportedIssues := make([]domain.ReportedIssue, len(dbIssues))
for i, dbIssue := range dbIssues {
reportedIssues[i] = domain.ReportedIssue{
ID: dbIssue.ID,
Subject: dbIssue.Subject,
Description: dbIssue.Description,
CustomerID: dbIssue.CustomerID,
Status: dbIssue.Status,
CreatedAt: dbIssue.CreatedAt.Time,
UpdatedAt: dbIssue.UpdatedAt.Time,
// Add other fields as necessary
}
}
return reportedIssues, nil
}
func (s *Service) GetAllIssues(ctx context.Context, limit, offset int) ([]dbgen.ReportedIssue, error) {
return s.repo.ListReportedIssues(ctx, int32(limit), int32(offset))
}
func (s *Service) UpdateIssueStatus(ctx context.Context, issueID int64, status string) error {
validStatuses := map[string]bool{"pending": true, "in_progress": true, "resolved": true, "rejected": true}
if !validStatuses[status] {
return errors.New("invalid status")
}
return s.repo.UpdateReportedIssueStatus(ctx, issueID, status)
}
func (s *Service) DeleteIssue(ctx context.Context, issueID int64) error {
return s.repo.DeleteReportedIssue(ctx, issueID)
}

View File

@ -9,6 +9,7 @@ import (
type Service interface {
SaveLeague(ctx context.Context, l domain.League) error
GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error)
GetFeaturedLeagues(ctx context.Context) ([]domain.League, error)
SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error
UpdateLeague(ctx context.Context, league domain.UpdateLeague) error
}

View File

@ -25,6 +25,10 @@ func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter)
return s.store.GetAllLeagues(ctx, filter)
}
func (s *service) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) {
return s.store.GetFeaturedLeagues(ctx)
}
func (s *service) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error {
return s.store.SetLeagueActive(ctx, leagueId, isActive)
}

View File

@ -8,6 +8,8 @@ import (
)
type NotificationStore interface {
GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error)
GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error)
SendNotification(ctx context.Context, notification *domain.Notification) error
MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error
ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error)

View File

@ -2,6 +2,7 @@ package notificationservice
import (
"context"
"encoding/json"
"errors"
"log/slog"
"sync"
@ -11,23 +12,32 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
afro "github.com/amanuelabay/afrosms-go"
"github.com/gorilla/websocket"
"github.com/redis/go-redis/v9"
)
type Service struct {
repo repository.NotificationRepository
Hub *ws.NotificationHub
connections sync.Map
notificationCh chan *domain.Notification
stopCh chan struct{}
config *config.Config
logger *slog.Logger
repo repository.NotificationRepository
Hub *ws.NotificationHub
notificationStore NotificationStore
connections sync.Map
notificationCh chan *domain.Notification
stopCh chan struct{}
config *config.Config
logger *slog.Logger
redisClient *redis.Client
}
func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service {
hub := ws.NewNotificationHub()
rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr, // e.g., "redis:6379"
})
svc := &Service{
repo: repo,
Hub: hub,
@ -36,11 +46,13 @@ func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *confi
notificationCh: make(chan *domain.Notification, 1000),
stopCh: make(chan struct{}),
config: cfg,
redisClient: rdb,
}
go hub.Run()
go svc.startWorker()
go svc.startRetryWorker()
go svc.RunRedisSubscriber(context.Background())
return svc
}
@ -255,7 +267,8 @@ func (s *Service) retryFailedNotifications() {
go func(notification *domain.Notification) {
for attempt := 0; attempt < 3; attempt++ {
time.Sleep(time.Duration(attempt) * time.Second)
if notification.DeliveryChannel == domain.DeliveryChannelSMS {
switch notification.DeliveryChannel {
case domain.DeliveryChannelSMS:
if err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
@ -264,7 +277,7 @@ func (s *Service) retryFailedNotifications() {
s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID)
return
}
} else if notification.DeliveryChannel == domain.DeliveryChannelEmail {
case domain.DeliveryChannelEmail:
if err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
@ -287,3 +300,176 @@ func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int
// func (s *Service) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error){
// return s.repo.Get(ctx, filter)
// }
func (s *Service) RunRedisSubscriber(ctx context.Context) {
pubsub := s.redisClient.Subscribe(ctx, "live_metrics")
defer pubsub.Close()
ch := pubsub.Channel()
for msg := range ch {
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(msg.Payload), &parsed); err != nil {
s.logger.Error("invalid Redis message format", "payload", msg.Payload, "error", err)
continue
}
eventType, _ := parsed["type"].(string)
payload := parsed["payload"]
recipientID, hasRecipient := parsed["recipient_id"]
recipientType, _ := parsed["recipient_type"].(string)
message := map[string]interface{}{
"type": eventType,
"payload": payload,
}
if hasRecipient {
message["recipient_id"] = recipientID
message["recipient_type"] = recipientType
}
s.Hub.Broadcast <- message
}
}
func (s *Service) UpdateLiveWalletMetrics(ctx context.Context, companies []domain.GetCompany, branches []domain.BranchWallet) error {
const key = "live_metrics"
companyBalances := make([]domain.CompanyWalletBalance, 0, len(companies))
for _, c := range companies {
companyBalances = append(companyBalances, domain.CompanyWalletBalance{
CompanyID: c.ID,
CompanyName: c.Name,
Balance: float64(c.WalletBalance.Float32()),
})
}
branchBalances := make([]domain.BranchWalletBalance, 0, len(branches))
for _, b := range branches {
branchBalances = append(branchBalances, domain.BranchWalletBalance{
BranchID: b.ID,
BranchName: b.Name,
CompanyID: b.CompanyID,
Balance: float64(b.Balance.Float32()),
})
}
payload := domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: companyBalances,
BranchBalances: branchBalances,
}
updatedData, err := json.Marshal(payload)
if err != nil {
return err
}
if err := s.redisClient.Set(ctx, key, updatedData, 0).Err(); err != nil {
return err
}
if err := s.redisClient.Publish(ctx, key, updatedData).Err(); err != nil {
return err
}
return nil
}
func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) {
const key = "live_metrics"
var metric domain.LiveMetric
val, err := s.redisClient.Get(ctx, key).Result()
if err == redis.Nil {
// Key does not exist yet, return zero-valued struct
return domain.LiveMetric{}, nil
} else if err != nil {
return domain.LiveMetric{}, err
}
if err := json.Unmarshal([]byte(val), &metric); err != nil {
return domain.LiveMetric{}, err
}
return metric, nil
}
func (s *Service) UpdateLiveWalletMetricForWallet(ctx context.Context, wallet domain.Wallet) {
var (
payload domain.LiveWalletMetrics
event map[string]interface{}
key = "live_metrics"
)
// Try company first
company, companyErr := s.notificationStore.GetCompanyByWalletID(ctx, wallet.ID)
if companyErr == nil {
payload = domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: []domain.CompanyWalletBalance{{
CompanyID: company.ID,
CompanyName: company.Name,
Balance: float64(wallet.Balance),
}},
BranchBalances: []domain.BranchWalletBalance{},
}
event = map[string]interface{}{
"type": "LIVE_WALLET_METRICS_UPDATE",
"recipient_id": company.ID,
"recipient_type": "company",
"payload": payload,
}
} else {
// Try branch next
branch, branchErr := s.notificationStore.GetBranchByWalletID(ctx, wallet.ID)
if branchErr == nil {
payload = domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: []domain.CompanyWalletBalance{},
BranchBalances: []domain.BranchWalletBalance{{
BranchID: branch.ID,
BranchName: branch.Name,
CompanyID: branch.CompanyID,
Balance: float64(wallet.Balance),
}},
}
event = map[string]interface{}{
"type": "LIVE_WALLET_METRICS_UPDATE",
"recipient_id": branch.ID,
"recipient_type": "branch",
"payload": payload,
}
} else {
// Neither company nor branch matched this wallet
s.logger.Warn("wallet not linked to any company or branch", "walletID", wallet.ID)
return
}
}
// Save latest metric to Redis
if jsonBytes, err := json.Marshal(payload); err == nil {
s.redisClient.Set(ctx, key, jsonBytes, 0)
} else {
s.logger.Error("failed to marshal wallet metrics payload", "walletID", wallet.ID, "err", err)
}
// Publish via Redis
if jsonEvent, err := json.Marshal(event); err == nil {
s.redisClient.Publish(ctx, key, jsonEvent)
} else {
s.logger.Error("failed to marshal event payload", "walletID", wallet.ID, "err", err)
}
// Broadcast over WebSocket
s.Hub.Broadcast <- event
}
func (s *Service) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
return s.notificationStore.GetCompanyByWalletID(ctx, walletID)
}
func (s *Service) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
return s.notificationStore.GetBranchByWalletID(ctx, walletID)
}

View File

@ -1,4 +1,4 @@
package odds
package odds
import (
"context"

View File

@ -124,7 +124,7 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo
walletID := wallets[0].ID
currentBonus := float64(wallets[0].Balance)
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBonus+referral.RewardAmount)*100)))
_, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+referral.RewardAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil {
s.logger.Error("Failed to add referral reward to wallet", "walletID", walletID, "referrerID", referrerID, "error", err)
return err
@ -162,7 +162,7 @@ func (s *Service) ProcessDepositBonus(ctx context.Context, userPhone string, amo
walletID := wallets[0].ID
bonus := amount * (settings.CashbackPercentage / 100)
currentBonus := float64(wallets[0].Balance)
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBonus+bonus)*100)))
_, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+bonus)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil {
s.logger.Error("Failed to add deposit bonus to wallet", "walletID", walletID, "userID", userID, "bonus", bonus, "error", err)
return err
@ -216,7 +216,7 @@ func (s *Service) ProcessBetReferral(ctx context.Context, userPhone string, betA
walletID := wallets[0].ID
currentBalance := float64(wallets[0].Balance)
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBalance+bonus)*100)))
_, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBalance+bonus)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil {
s.logger.Error("Failed to add bet referral bonus to wallet", "walletID", walletID, "referrerID", referrerID, "bonus", bonus, "error", err)
return err

View File

@ -2,9 +2,14 @@ package report
import (
"context"
"encoding/csv"
"errors"
"fmt"
"log/slog"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -454,32 +459,299 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
return performances, nil
}
func (s *Service) GenerateReport(timeFrame domain.TimeFrame) (*domain.Report, error) {
now := time.Now()
var start, end time.Time
switch timeFrame {
case domain.Daily:
start = now.AddDate(0, 0, -1)
end = now
case domain.Weekly:
start = now.AddDate(0, 0, -7)
end = now
case domain.Monthly:
start = now.AddDate(0, -1, 0)
end = now
}
report, err := s.repo.GenerateReport(timeFrame, start, end)
func (s *Service) GenerateReport(ctx context.Context, period string) error {
data, err := s.fetchReportData(ctx, period)
if err != nil {
return nil, err
return fmt.Errorf("fetch data: %w", err)
}
if err := s.repo.SaveReport(report); err != nil {
return nil, err
filePath := fmt.Sprintf("/host-desktop/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04"))
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("create file: %w", err)
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// Summary section
writer.Write([]string{"Sports Betting Reports (Periodic)"})
writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"})
writer.Write([]string{
period,
fmt.Sprintf("%d", data.TotalBets),
fmt.Sprintf("%.2f", data.TotalCashIn),
fmt.Sprintf("%.2f", data.TotalCashOut),
fmt.Sprintf("%.2f", data.CashBacks),
fmt.Sprintf("%.2f", data.Deposits),
fmt.Sprintf("%.2f", data.Withdrawals),
fmt.Sprintf("%d", data.TotalTickets),
})
writer.Write([]string{}) // Empty line for spacing
// Virtual Game Summary section
writer.Write([]string{"Virtual Game Reports (Periodic)"})
writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"})
for _, row := range data.VirtualGameStats {
writer.Write([]string{
row.GameName,
fmt.Sprintf("%d", row.NumBets),
fmt.Sprintf("%.2f", row.TotalTransaction),
})
}
return report, nil
writer.Write([]string{}) // Empty line
writer.Write([]string{"Company Reports (Periodic)"})
writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
for _, cr := range data.CompanyReports {
writer.Write([]string{
fmt.Sprintf("%d", cr.CompanyID),
cr.CompanyName,
fmt.Sprintf("%d", cr.TotalBets),
fmt.Sprintf("%.2f", cr.TotalCashIn),
fmt.Sprintf("%.2f", cr.TotalCashOut),
fmt.Sprintf("%.2f", cr.TotalCashBacks),
})
}
writer.Write([]string{}) // Empty line
writer.Write([]string{"Branch Reports (Periodic)"})
writer.Write([]string{"Branch ID", "Branch Name", "Company ID", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
for _, br := range data.BranchReports {
writer.Write([]string{
fmt.Sprintf("%d", br.BranchID),
br.BranchName,
fmt.Sprintf("%d", br.CompanyID),
fmt.Sprintf("%d", br.TotalBets),
fmt.Sprintf("%.2f", br.TotalCashIn),
fmt.Sprintf("%.2f", br.TotalCashOut),
fmt.Sprintf("%.2f", br.TotalCashBacks),
})
}
var totalBets int64
var totalCashIn, totalCashOut, totalCashBacks float64
for _, cr := range data.CompanyReports {
totalBets += cr.TotalBets
totalCashIn += cr.TotalCashIn
totalCashOut += cr.TotalCashOut
totalCashBacks += cr.TotalCashBacks
}
writer.Write([]string{})
writer.Write([]string{"Total Summary"})
writer.Write([]string{"Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
writer.Write([]string{
fmt.Sprintf("%d", totalBets),
fmt.Sprintf("%.2f", totalCashIn),
fmt.Sprintf("%.2f", totalCashOut),
fmt.Sprintf("%.2f", totalCashBacks),
})
return nil
}
func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) {
from, to := getTimeRange(period)
// companyID := int64(0)
// Basic metrics
totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to)
cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to)
cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to)
cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to)
// Wallet Transactions
transactions, _ := s.repo.GetWalletTransactionsInRange(ctx, from, to)
var totalDeposits, totalWithdrawals float64
for _, tx := range transactions {
switch strings.ToLower(tx.Type.String) {
case "deposit":
totalDeposits += float64(tx.TotalAmount)
case "withdraw":
totalWithdrawals += float64(tx.TotalAmount)
}
}
// Ticket Count
totalTickets, _ := s.repo.GetAllTicketsInRange(ctx, from, to)
// Virtual Game Summary
virtualGameStats, _ := s.repo.GetVirtualGameSummaryInRange(ctx, from, to)
// Convert []dbgen.GetVirtualGameSummaryInRangeRow to []domain.VirtualGameStat
var virtualGameStatsDomain []domain.VirtualGameStat
for _, row := range virtualGameStats {
var totalTransaction float64
switch v := row.TotalTransactionSum.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalTransaction = val
}
case float64:
totalTransaction = v
case int:
totalTransaction = float64(v)
default:
totalTransaction = 0
}
virtualGameStatsDomain = append(virtualGameStatsDomain, domain.VirtualGameStat{
GameName: row.GameName,
NumBets: row.NumberOfBets,
TotalTransaction: totalTransaction,
})
}
companyRows, _ := s.repo.GetCompanyWiseReport(ctx, from, to)
var companyReports []domain.CompanyReport
for _, row := range companyRows {
var totalCashIn, totalCashOut, totalCashBacks float64
switch v := row.TotalCashMade.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashIn = val
}
case float64:
totalCashIn = v
case int:
totalCashIn = float64(v)
default:
totalCashIn = 0
}
switch v := row.TotalCashOut.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashOut = val
}
case float64:
totalCashOut = v
case int:
totalCashOut = float64(v)
default:
totalCashOut = 0
}
switch v := row.TotalCashBacks.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashBacks = val
}
case float64:
totalCashBacks = v
case int:
totalCashBacks = float64(v)
default:
totalCashBacks = 0
}
companyReports = append(companyReports, domain.CompanyReport{
CompanyID: row.CompanyID.Int64,
CompanyName: row.CompanyName,
TotalBets: row.TotalBets,
TotalCashIn: totalCashIn,
TotalCashOut: totalCashOut,
TotalCashBacks: totalCashBacks,
})
}
branchRows, _ := s.repo.GetBranchWiseReport(ctx, from, to)
var branchReports []domain.BranchReport
for _, row := range branchRows {
var totalCashIn, totalCashOut, totalCashBacks float64
switch v := row.TotalCashMade.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashIn = val
}
case float64:
totalCashIn = v
case int:
totalCashIn = float64(v)
default:
totalCashIn = 0
}
switch v := row.TotalCashOut.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashOut = val
}
case float64:
totalCashOut = v
case int:
totalCashOut = float64(v)
default:
totalCashOut = 0
}
switch v := row.TotalCashBacks.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashBacks = val
}
case float64:
totalCashBacks = v
case int:
totalCashBacks = float64(v)
default:
totalCashBacks = 0
}
branchReports = append(branchReports, domain.BranchReport{
BranchID: row.BranchID.Int64,
BranchName: row.BranchName,
CompanyID: row.CompanyID,
TotalBets: row.TotalBets,
TotalCashIn: totalCashIn,
TotalCashOut: totalCashOut,
TotalCashBacks: totalCashBacks,
})
}
return domain.ReportData{
TotalBets: totalBets,
TotalCashIn: cashIn,
TotalCashOut: cashOut,
CashBacks: cashBacks,
Deposits: totalDeposits,
Withdrawals: totalWithdrawals,
TotalTickets: totalTickets.TotalTickets,
VirtualGameStats: virtualGameStatsDomain,
CompanyReports: companyReports,
BranchReports: branchReports,
}, nil
}
func getTimeRange(period string) (time.Time, time.Time) {
now := time.Now()
switch strings.ToLower(period) {
case "daily":
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
end := start.Add(5 * time.Minute)
return start, end
case "weekly":
weekday := int(now.Weekday())
if weekday == 0 {
weekday = 7
}
start := now.AddDate(0, 0, -weekday+1)
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, now.Location())
end := start.AddDate(0, 0, 7)
return start, end
case "monthly":
start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
end := start.AddDate(0, 1, 0)
return start, end
default:
// Default to daily
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
end := start.Add(24 * time.Hour)
return start, end
}
}
// func (s *Service) GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) {

View File

@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"log/slog"
"net/http"
"strconv"
@ -17,30 +16,33 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
)
type Service struct {
repo *repository.Store
config *config.Config
logger *slog.Logger
client *http.Client
betSvc bet.Service
oddSvc odds.ServiceImpl
eventSvc event.Service
leagueSvc league.Service
repo *repository.Store
config *config.Config
logger *slog.Logger
client *http.Client
betSvc bet.Service
oddSvc odds.ServiceImpl
eventSvc event.Service
leagueSvc league.Service
notificationSvc *notificationservice.Service
}
func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service) *Service {
func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service, notificationSvc *notificationservice.Service) *Service {
return &Service{
repo: repo,
config: cfg,
logger: logger,
client: &http.Client{Timeout: 10 * time.Second},
betSvc: betSvc,
oddSvc: oddSvc,
eventSvc: eventSvc,
leagueSvc: leagueSvc,
repo: repo,
config: cfg,
logger: logger,
client: &http.Client{Timeout: 10 * time.Second},
betSvc: betSvc,
oddSvc: oddSvc,
eventSvc: eventSvc,
leagueSvc: leagueSvc,
notificationSvc: notificationSvc,
}
}
@ -48,6 +50,127 @@ var (
ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed")
)
func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, resultRes json.RawMessage, sportID int64) error {
// TODO: Optimize this since there could be many outcomes with the same event_id and market_id that could be updated the same time
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID, true)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)
}
if len(outcomes) == 0 {
s.logger.Info("No bets have been placed on event", "event", eventID)
}
// if len(outcomes) == 0 {
// fmt.Printf("🕛 No bets have been placed on event %v (%d/%d) \n", eventID, i+1, len(events))
// } else {
// fmt.Printf("✅ %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events))
// }
for _, outcome := range outcomes {
if outcome.Expires.After(time.Now()) {
s.logger.Warn("Outcome is not expired yet", "event_id", outcome.EventID, "outcome_id", outcome.ID)
return fmt.Errorf("Outcome has not expired yet")
}
parseResult, err := s.parseResult(ctx, resultRes, outcome, sportID)
if err != nil {
s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
return err
}
outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status)
if err != nil {
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
return err
}
if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING {
s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID)
return fmt.Errorf("Error while updating outcome")
}
status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
if err != nil {
if err != bet.ErrOutcomesNotCompleted {
s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
}
return err
}
s.logger.Info("Updating bet status", "bet_id", outcome.BetID, "status", status.String())
err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status)
if err != nil {
s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
return err
}
}
return nil
}
func (s *Service) RefundAllOutcomes(ctx context.Context, eventID int64) error {
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID, false)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)
}
if len(outcomes) == 0 {
s.logger.Info("No bets have been placed on event", "event", eventID)
}
outcomes, err = s.betSvc.UpdateBetOutcomeStatusForEvent(ctx, eventID, domain.OUTCOME_STATUS_VOID)
if err != nil {
s.logger.Error("Failed to update all outcomes for event")
}
// Get all the unique bet_ids and how many outcomes have this bet_id
betIDSet := make(map[int64]int64)
for _, outcome := range outcomes {
betIDSet[outcome.BetID] += 1
}
for betID := range betIDSet {
status, err := s.betSvc.CheckBetOutcomeForBet(ctx, betID)
if err != nil {
if err != bet.ErrOutcomesNotCompleted {
s.logger.Error("Failed to check bet outcome for bet", "event_id", eventID, "error", err)
}
return err
}
err = s.betSvc.UpdateStatus(ctx, betID, status)
if err != nil {
s.logger.Error("Failed to update bet status", "event id", eventID, "error", err)
continue
}
}
return nil
// for _, outcome := range outcomes {
// outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, domain.OUTCOME_STATUS_VOID)
// if err != nil {
// s.logger.Error("Failed to refund all outcome status", "bet_outcome_id", outcome.ID, "error", err)
// return err
// }
// // Check if all the bet outcomes have been set to refund for
// status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
// if err != nil {
// if err != bet.ErrOutcomesNotCompleted {
// s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
// }
// return err
// }
// err = s.betSvc.UpdateStatus(ctx, outcome.BetID, domain.OUTCOME_STATUS_VOID)
// if err != nil {
// s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
// return err
// }
// }
}
func (s *Service) FetchAndProcessResults(ctx context.Context) error {
// TODO: Optimize this because there could be many bet outcomes for the same odd
// Take market id and match result as param and update all the bet outcomes at the same time
@ -58,29 +181,15 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
}
fmt.Printf("⚠️ Expired Events: %d \n", len(events))
removed := 0
errs := make([]error, 0, len(events))
for i, event := range events {
for _, event := range events {
eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event id")
errs = append(errs, fmt.Errorf("failed to parse event id %s: %w", event.ID, err))
continue
}
outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID)
if err != nil {
s.logger.Error("Failed to get pending bet outcomes", "error", err)
errs = append(errs, fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err))
s.logger.Error("Failed to parse", "eventID", event.ID, "err", err)
continue
}
if len(outcomes) == 0 {
fmt.Printf("🕛 No bets have been placed on event %v (%d/%d) \n", event.ID, i+1, len(events))
} else {
fmt.Printf("✅ %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events))
}
isDeleted := true
result, err := s.fetchResult(ctx, eventID)
if err != nil {
if err == ErrEventIsNotActive {
@ -108,78 +217,41 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
}
// TODO: Figure out what to do with the events that have been cancelled or postponed, etc...
if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) {
s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus)
fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events))
isDeleted = false
// if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) {
// s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus)
// fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events))
// isDeleted = false
// continue
// }
// notification := &domain.Notification{
// RecipientID: recipientID,
// Type: domain.NOTIFICATION_TYPE_WALLET,
// Level: domain.NotificationLevelWarning,
// Reciever: domain.NotificationRecieverSideAdmin,
// DeliveryChannel: domain.DeliveryChannelInApp,
// Payload: domain.NotificationPayload{
// Headline: "Wallet Threshold Alert",
// Message: message,
// },
// Priority: 2, // Medium priority
// }
switch timeStatusParsed {
case int64(domain.TIME_STATUS_NOT_STARTED), int64(domain.TIME_STATUS_IN_PLAY):
continue
}
for j, outcome := range outcomes {
fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
case int64(domain.TIME_STATUS_TO_BE_FIXED):
s.logger.Warn("Event needs to be rescheduled or corrected", "event_id", eventID)
// Admin users will be able to review the events
if outcome.Expires.After(time.Now()) {
isDeleted = false
s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID)
continue
}
parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID)
case int64(domain.TIME_STATUS_ENDED), int64(domain.TIME_STATUS_WALKOVER), int64(domain.TIME_STATUS_DECIDED_BY_FA):
err = s.UpdateResultForOutcomes(ctx, eventID, result.Results[0], sportID)
if err != nil {
isDeleted = false
s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err))
continue
}
outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status)
if err != nil {
isDeleted = false
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
continue
}
if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING {
fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID)
isDeleted = false
continue
s.logger.Error("Error while updating result for event", "event_id", eventID)
}
fmt.Printf("✅ Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
if err != nil {
if err != bet.ErrOutcomesNotCompleted {
s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
}
isDeleted = false
continue
}
fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String())
err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status)
if err != nil {
s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
isDeleted = false
continue
}
fmt.Printf("✅ Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n",
outcome.BetID,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
}
if isDeleted {
removed += 1
fmt.Printf("⚠️ Removing Event %v \n", event.ID)
s.logger.Info("Removing Event", "eventID", event.ID)
err = s.repo.DeleteEvent(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err)
@ -190,17 +262,91 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err)
return err
}
removed += 1
case int64(domain.TIME_STATUS_ABANDONED), int64(domain.TIME_STATUS_CANCELLED), int64(domain.TIME_STATUS_REMOVED):
s.logger.Info("Event abandoned/cancelled/removed", "event_id", eventID, "status", timeStatusParsed)
err = s.RefundAllOutcomes(ctx, eventID)
s.logger.Info("Removing Event", "eventID", event.ID)
err = s.repo.DeleteEvent(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err)
return err
}
err = s.repo.DeleteOddsForEvent(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err)
return err
}
removed += 1
}
// for j, outcome := range outcomes {
// fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
// outcome.MarketName,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// if outcome.Expires.After(time.Now()) {
// isDeleted = false
// s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID)
// continue
// }
// parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID)
// if err != nil {
// isDeleted = false
// s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
// errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err))
// continue
// }
// outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status)
// if err != nil {
// isDeleted = false
// s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
// continue
// }
// if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING {
// fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
// outcome.MarketName,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID)
// isDeleted = false
// continue
// }
// fmt.Printf("✅ Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
// outcome.MarketName,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
// if err != nil {
// if err != bet.ErrOutcomesNotCompleted {
// s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
// }
// isDeleted = false
// continue
// }
// fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String())
// err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status)
// if err != nil {
// s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
// isDeleted = false
// continue
// }
// fmt.Printf("✅ Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n",
// outcome.BetID,
// event.HomeTeam+" "+event.AwayTeam, event.ID,
// j+1, len(outcomes))
// }
}
fmt.Printf("🗑️ Removed Events: %d \n", removed)
if len(errs) > 0 {
s.logger.Error("Errors occurred while processing results", "errors", errs)
for _, err := range errs {
fmt.Println("Error:", err)
}
return fmt.Errorf("errors occurred while processing results: %v", errs)
}
s.logger.Info("Total Number of Removed Events", "count", removed)
s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events))
return nil
}
@ -211,10 +357,9 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
s.logger.Error("Failed to fetch events")
return 0, err
}
fmt.Printf("⚠️ Expired Events: %d \n", len(events))
updated := 0
for i, event := range events {
fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events))
for _, event := range events {
// fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events))
eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event id")
@ -232,7 +377,6 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
}
if result.Success != 1 || len(result.Results) == 0 {
s.logger.Error("Invalid API response", "event_id", eventID)
fmt.Printf("⚠️ Invalid API response for event %v \n", result)
continue
}
@ -282,12 +426,13 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
continue
}
updated++
fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events))
// fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events))
s.logger.Info("Updated Event Status", "event ID", event.ID, "status", eventStatus)
// Update the league because the league country code is only found on the result response
leagueID, err := strconv.ParseInt(commonResp.League.ID, 10, 64)
if err != nil {
log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID)
// log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID)
s.logger.Error("Invalid League ID", "leagueID", commonResp.League.ID)
continue
}
@ -304,12 +449,11 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error
})
if err != nil {
log.Printf("❌ Error Updating League %v", commonResp.League.Name)
log.Printf("err:%v", err)
s.logger.Error("Error Updating League", "League Name", commonResp.League.Name, "err", err)
continue
}
fmt.Printf("✅ Updated League %v with country code %v \n", leagueID, commonResp.League.CC)
// fmt.Printf("✅ Updated League %v with country code %v \n", leagueID, commonResp.League.CC)
s.logger.Info("Updated League with country code", "leagueID", leagueID, "code", commonResp.League.CC)
}
if updated == 0 {

View File

@ -0,0 +1,14 @@
package settings
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type SettingStore interface {
GetSettingList(ctx context.Context) (domain.SettingList, error)
GetSettings(ctx context.Context) ([]domain.Setting, error)
GetSetting(ctx context.Context, key string) (domain.Setting, error)
SaveSetting(ctx context.Context, key, value string) (domain.Setting, error)
}

View File

@ -0,0 +1,32 @@
package settings
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Service struct {
settingStore SettingStore
}
func NewService(settingStore SettingStore) *Service {
return &Service{
settingStore: settingStore,
}
}
func (s *Service) GetSettingList(ctx context.Context) (domain.SettingList, error) {
return s.settingStore.GetSettingList(ctx)
}
func (s *Service) GetSettings(ctx context.Context) ([]domain.Setting, error) {
return s.settingStore.GetSettings(ctx)
}
func (s *Service) GetSetting(ctx context.Context, key string) (domain.Setting, error) {
return s.settingStore.GetSetting(ctx, key)
}
func (s *Service) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) {
return s.settingStore.SaveSetting(ctx, key, value)
}

View File

@ -2,24 +2,255 @@ package ticket
import (
"context"
"encoding/json"
"errors"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
"go.uber.org/zap"
)
var (
// ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events")
// ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending")
ErrTicketHasExpired = errors.New("Ticket has expired")
ErrNoEventsAvailable = errors.New("Not enough events available with the given filters")
ErrEventHasBeenRemoved = errors.New("Event has been removed")
ErrTooManyOutcomesForTicket = errors.New("Too many odds/outcomes for a single ticket")
ErrTicketAmountTooHigh = errors.New("Cannot create a ticket with an amount above limit")
ErrTicketLimitForSingleUser = errors.New("Number of Ticket Limit reached")
ErrTicketWinningTooHigh = errors.New("Total Winnings over set limit")
ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid")
)
type Service struct {
ticketStore TicketStore
ticketStore TicketStore
eventSvc event.Service
prematchSvc odds.ServiceImpl
mongoLogger *zap.Logger
settingSvc settings.Service
notificationSvc *notificationservice.Service
}
func NewService(ticketStore TicketStore) *Service {
func NewService(
ticketStore TicketStore,
eventSvc event.Service,
prematchSvc odds.ServiceImpl,
mongoLogger *zap.Logger,
settingSvc settings.Service,
notificationSvc *notificationservice.Service,
) *Service {
return &Service{
ticketStore: ticketStore,
ticketStore: ticketStore,
eventSvc: eventSvc,
prematchSvc: prematchSvc,
mongoLogger: mongoLogger,
settingSvc: settingSvc,
notificationSvc: notificationSvc,
}
}
func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) {
return s.ticketStore.CreateTicket(ctx, ticket)
func (s *Service) GenerateTicketOutcome(ctx context.Context, settings domain.SettingList, eventID int64, marketID int64, oddID int64) (domain.CreateTicketOutcome, error) {
eventIDStr := strconv.FormatInt(eventID, 10)
marketIDStr := strconv.FormatInt(marketID, 10)
oddIDStr := strconv.FormatInt(oddID, 10)
event, err := s.eventSvc.GetUpcomingEventByID(ctx, eventIDStr)
if err != nil {
s.mongoLogger.Error("failed to fetch upcoming event by ID",
zap.Int64("event_id", eventID),
zap.Error(err),
)
return domain.CreateTicketOutcome{}, ErrEventHasBeenRemoved
}
// Checking to make sure the event hasn't already started
currentTime := time.Now()
if event.StartTime.Before(currentTime) {
s.mongoLogger.Error("event has already started",
zap.Int64("event_id", eventID),
zap.Time("event_start_time", event.StartTime),
zap.Time("current_time", currentTime),
)
return domain.CreateTicketOutcome{}, ErrTicketHasExpired
}
odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr)
if err != nil {
s.mongoLogger.Error("failed to get raw odds by market ID",
zap.Int64("event_id", eventID),
zap.Int64("market_id", marketID),
zap.Error(err),
)
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil)
}
type rawOddType struct {
ID string
Name string
Odds string
Header string
Handicap string
}
var selectedOdd rawOddType
var isOddFound bool = false
for _, raw := range odds.RawOdds {
var rawOdd rawOddType
rawBytes, err := json.Marshal(raw)
err = json.Unmarshal(rawBytes, &rawOdd)
if err != nil {
s.mongoLogger.Error("failed to unmarshal raw ods",
zap.Int64("event_id", eventID),
zap.String("rawOddID", rawOdd.ID),
zap.Error(err),
)
continue
}
if rawOdd.ID == oddIDStr {
selectedOdd = rawOdd
isOddFound = true
}
}
if !isOddFound {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil)
s.mongoLogger.Error("Invalid Odd ID",
zap.Int64("event_id", eventID),
zap.String("oddIDStr", oddIDStr),
)
return domain.CreateTicketOutcome{}, ErrRawOddInvalid
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
if err != nil {
s.mongoLogger.Error("failed to parse selected odd value",
zap.String("odd", selectedOdd.Odds),
zap.Int64("odd_id", oddID),
zap.Error(err),
)
return domain.CreateTicketOutcome{}, err
}
newOutcome := domain.CreateTicketOutcome{
EventID: eventID,
OddID: oddID,
MarketID: marketID,
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: event.StartTime,
}
// outcomes = append(outcomes, )
return newOutcome, nil
}
func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, clientIP string) (domain.Ticket, int64, error) {
settingsList, err := s.settingSvc.GetSettingList(ctx)
// Check to see if the number of outcomes is above a set limit
if len(req.Outcomes) > int(settingsList.MaxNumberOfOutcomes) {
return domain.Ticket{}, 0, ErrTooManyOutcomesForTicket
}
// Check to see if the amount is above a set limit
if req.Amount > settingsList.BetAmountLimit.Float32() {
return domain.Ticket{}, 0, ErrTicketAmountTooHigh
}
count, err := s.CountTicketByIP(ctx, clientIP)
if err != nil {
s.mongoLogger.Error("failed to count number of ticket using ip",
zap.Error(err),
)
return domain.Ticket{}, 0, err
}
// Check to see how many tickets a single anonymous user has created
if count > settingsList.DailyTicketPerIP {
return domain.Ticket{}, 0, ErrTicketLimitForSingleUser
}
var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes))
var totalOdds float32 = 1
for _, outcomeReq := range req.Outcomes {
newOutcome, err := s.GenerateTicketOutcome(ctx, settingsList, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
if err != nil {
s.mongoLogger.Error("failed to generate outcome",
zap.Int64("event_id", outcomeReq.EventID),
zap.Int64("market_id", outcomeReq.MarketID),
zap.Int64("odd_id", outcomeReq.OddID),
zap.Error(err),
)
return domain.Ticket{}, 0, err
}
totalOdds *= float32(newOutcome.Odd)
outcomes = append(outcomes, newOutcome)
}
totalWinnings := req.Amount * totalOdds
// Check to see if the total winning amount is over a set limit
if totalWinnings > settingsList.TotalWinningLimit.Float32() {
s.mongoLogger.Error("Total Winnings over limit",
zap.Float32("Total Odds", totalOdds),
zap.Float32("amount", req.Amount),
zap.Float32("limit", settingsList.TotalWinningLimit.Float32()))
return domain.Ticket{}, 0, ErrTicketWinningTooHigh
}
ticket, err := s.ticketStore.CreateTicket(ctx, domain.CreateTicket{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
IP: clientIP,
})
if err != nil {
s.mongoLogger.Error("Error Creating Ticket", zap.Float32("Total Odds", totalOdds), zap.Float32("amount", req.Amount))
return domain.Ticket{}, 0, err
}
// Add the ticket id now that it has fetched from the database
for index := range outcomes {
outcomes[index].TicketID = ticket.ID
}
rows, err := s.CreateTicketOutcome(ctx, outcomes)
if err != nil {
s.mongoLogger.Error("Error Creating Ticket Outcomes", zap.Any("outcomes", outcomes))
return domain.Ticket{}, rows, err
}
// updates := domain.MetricUpdates{
// TotalLiveTicketsDelta: domain.PtrInt64(1),
// }
// if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil {
// // handle error
// }
return ticket, rows, nil
}
// func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) {
// return s.ticketStore.CreateTicket(ctx, ticket)
// }
func (s *Service) CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) {
return s.ticketStore.CreateTicketOutcome(ctx, outcomes)
}

View File

@ -10,18 +10,29 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
afro "github.com/amanuelabay/afrosms-go"
"github.com/resend/resend-go/v2"
"github.com/twilio/twilio-go"
twilioApi "github.com/twilio/twilio-go/rest/api/v2010"
"golang.org/x/crypto/bcrypt"
)
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium) error {
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.OtpProvider) error {
otpCode := helpers.GenerateOTP()
message := fmt.Sprintf("Welcome to Fortune bets, your OTP is %s please don't share with anyone.", otpCode)
switch medium {
case domain.OtpMediumSms:
if err := s.SendSMSOTP(ctx, sentTo, message); err != nil {
return err
switch provider {
case "twilio":
if err := s.SendTwilioSMSOTP(ctx, sentTo, message, provider); err != nil {
return err
}
case "afromessage":
if err := s.SendAfroMessageSMSOTP(ctx, sentTo, message, provider); err != nil {
return err
}
default:
return fmt.Errorf("invalid sms provider: %s", provider)
}
case domain.OtpMediumEmail:
if err := s.SendEmailOTP(ctx, sentTo, message); err != nil {
@ -51,7 +62,7 @@ func hashPassword(plaintextPassword string) ([]byte, error) {
return hash, nil
}
func (s *Service) SendSMSOTP(ctx context.Context, receiverPhone, message string) error {
func (s *Service) SendAfroMessageSMSOTP(ctx context.Context, receiverPhone, message string, provider domain.OtpProvider) error {
apiKey := s.config.AFRO_SMS_API_KEY
senderName := s.config.AFRO_SMS_SENDER_NAME
hostURL := s.config.ADRO_SMS_HOST_URL
@ -79,6 +90,29 @@ func (s *Service) SendSMSOTP(ctx context.Context, receiverPhone, message string)
}
}
func (s *Service) SendTwilioSMSOTP(ctx context.Context, receiverPhone, message string, provider domain.OtpProvider) error {
accountSid := s.config.TwilioAccountSid
authToken := s.config.TwilioAuthToken
senderPhone := s.config.TwilioSenderPhoneNumber
client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: accountSid,
Password: authToken,
})
params := &twilioApi.CreateMessageParams{}
params.SetTo(receiverPhone)
params.SetFrom(senderPhone)
params.SetBody(message)
_, err := client.Api.CreateMessage(params)
if err != nil {
return fmt.Errorf("Error sending SMS message: %s" + err.Error())
}
return nil
}
func (s *Service) SendEmailOTP(ctx context.Context, receiverEmail, message string) error {
apiKey := s.config.ResendApiKey
client := resend.NewClient(apiKey)

View File

@ -10,7 +10,7 @@ import (
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
}
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.OtpProvider) error {
var err error
// check if user exists
switch medium {
@ -26,7 +26,7 @@ func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium,
}
// send otp based on the medium
return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium)
return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium, provider)
}
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) { // normal
// get otp

View File

@ -8,7 +8,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.OtpProvider) error {
var err error
// check if user exists
@ -23,7 +23,7 @@ func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, se
return err
}
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium)
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium, provider)
}

View File

@ -128,7 +128,7 @@ func (s *AleaPlayService) processTransaction(ctx context.Context, tx *domain.Vir
}
tx.WalletID = wallets[0].ID
if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil {
if _, err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
return fmt.Errorf("wallet update failed: %w", err)
}

View File

@ -13,6 +13,13 @@ type VirtualGameService interface {
GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error)
ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error)
ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error)
ProcessTournamentWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error)
ProcessPromoWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error)
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error)
RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error)
AddFavoriteGame(ctx context.Context, userID, gameID int64) error
RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error
ListFavoriteGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error)
}

View File

@ -1,6 +1,7 @@
package virtualgameservice
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
@ -8,7 +9,12 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"math/rand/v2"
"net/http"
"sort"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
@ -43,14 +49,15 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
return "", err
}
sessionToken := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano())
sessionId := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano())
token, err := jwtutil.CreatePopOKJwt(
userID,
user.PhoneNumber,
user.CompanyID,
user.FirstName,
currency,
"en",
mode,
sessionToken,
sessionId,
s.config.PopOK.SecretKey,
24*time.Hour,
)
@ -59,19 +66,33 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
return "", err
}
// Record game launch as a transaction (for history and recommendation purposes)
tx := &domain.VirtualGameHistory{
SessionID: sessionId, // Optional: populate if session tracking is implemented
UserID: userID,
CompanyID: user.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: toInt64Ptr(gameID),
TransactionType: "LAUNCH",
Amount: 0,
Currency: currency,
ExternalTransactionID: sessionId,
Status: "COMPLETED",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.repo.CreateVirtualGameHistory(ctx, tx); err != nil {
s.logger.Error("Failed to record game launch transaction", "error", err)
// Do not fail game launch on logging error — just log and continue
}
params := fmt.Sprintf(
"partnerId=%s&gameId=%s&gameMode=%s&lang=en&platform=%s&externalToken=%s",
s.config.PopOK.ClientID, gameID, mode, s.config.PopOK.Platform, token,
)
// params = fmt.Sprintf(
// "partnerId=%s&gameId=%sgameMode=%s&lang=en&platform=%s",
// "1", "1", "fun", "111",
// )
// signature := s.generateSignature(params)
return fmt.Sprintf("%s?%s", s.config.PopOK.BaseURL, params), nil
// return fmt.Sprintf("%s?%s", s.config.PopOK.BaseURL, params), nil
}
func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error {
@ -117,7 +138,7 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall
return errors.New("unknown transaction type")
}
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(amount))
_, err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil {
s.logger.Error("Failed to update wallet", "walletID", walletID, "userID", session.UserID, "amount", amount, "error", err)
return err
@ -148,6 +169,8 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall
func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error) {
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
fmt.Printf("\n\nClaims: %+v\n\n", claims)
fmt.Printf("\n\nExternal token: %+v\n\n", req.ExternalToken)
if err != nil {
s.logger.Error("Failed to parse JWT", "error", err)
return nil, fmt.Errorf("invalid token")
@ -184,15 +207,18 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets")
}
if err := s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents)); err != nil {
if _, err := s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT); err != nil {
return nil, fmt.Errorf("insufficient balance")
}
// Create transaction record
tx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
CompanyID: claims.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: req.GameID,
TransactionType: "BET",
Amount: -amountCents, // Negative for bets
Amount: amountCents, // Negative for bets
Currency: req.Currency,
ExternalTransactionID: req.TransactionID,
Status: "COMPLETED",
@ -219,6 +245,8 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
return nil, fmt.Errorf("invalid token")
}
fmt.Printf("\n\nClaims: %+v\n\n", claims)
// 2. Check for duplicate transaction (idempotency)
existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil {
@ -245,7 +273,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
// 4. Credit to wallet
if err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents)); err != nil {
if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
s.logger.Error("Failed to credit wallet", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("wallet credit failed")
}
@ -257,6 +285,9 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
// 5. Create transaction record
tx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
CompanyID: claims.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: req.GameID,
TransactionType: "WIN",
Amount: amountCents,
Currency: req.Currency,
@ -277,14 +308,175 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
}, nil
}
func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) {
func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) {
// 1. Validate token and get user ID
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
if err != nil {
s.logger.Error("Invalid token in cancel request", "error", err)
s.logger.Error("Invalid token in tournament win request", "error", err)
return nil, fmt.Errorf("invalid token")
}
// 2. Check for duplicate tournament win transaction
existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil {
s.logger.Error("Failed to check existing tournament transaction", "error", err)
return nil, fmt.Errorf("transaction check failed")
}
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) / 100
}
return &domain.PopOKWinResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", existingTx.ID),
Balance: balance,
}, nil
}
// 3. Convert amount to cents
amountCents := int64(req.Amount * 100)
// 4. Credit user wallet
if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
s.logger.Error("Failed to credit wallet for tournament", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("wallet credit failed")
}
// 5. Log tournament win transaction
tx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
TransactionType: "TOURNAMENT_WIN",
Amount: amountCents,
Currency: req.Currency,
ExternalTransactionID: req.TransactionID,
Status: "COMPLETED",
CreatedAt: time.Now(),
}
if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil {
s.logger.Error("Failed to record tournament win transaction", "error", err)
return nil, fmt.Errorf("transaction recording failed")
}
// 6. Fetch updated 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) / 100,
}, nil
}
func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) {
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
if err != nil {
s.logger.Error("Invalid token in promo win request", "error", err)
return nil, fmt.Errorf("invalid token")
}
existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil {
s.logger.Error("Failed to check existing promo transaction", "error", err)
return nil, fmt.Errorf("transaction check failed")
}
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) / 100
}
return &domain.PopOKWinResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", existingTx.ID),
Balance: balance,
}, nil
}
amountCents := int64(req.Amount * 100)
if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
s.logger.Error("Failed to credit wallet for promo", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("wallet credit failed")
}
tx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
TransactionType: "PROMO_WIN",
Amount: amountCents,
Currency: req.Currency,
ExternalTransactionID: req.TransactionID,
Status: "COMPLETED",
CreatedAt: time.Now(),
}
if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil {
s.logger.Error("Failed to create promo win transaction", "error", err)
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")
}
return &domain.PopOKWinResponse{
TransactionID: req.TransactionID,
ExternalTrxID: fmt.Sprintf("%v", tx.ID),
Balance: float64(wallets[0].Balance) / 100,
}, nil
}
// func (s *service) GenerateNewToken(ctx context.Context, req *domain.PopOKGenerateTokenRequest) (*domain.PopOKGenerateTokenResponse, error) {
// userID, err := strconv.ParseInt(req.PlayerID, 10, 64)
// if err != nil {
// s.logger.Error("Invalid player ID", "playerID", req.PlayerID, "error", err)
// return nil, fmt.Errorf("invalid player ID")
// }
// user, err := s.store.GetUserByID(ctx, userID)
// if err != nil {
// s.logger.Error("Failed to find user for token refresh", "userID", userID, "error", err)
// return nil, fmt.Errorf("user not found")
// }
// newSessionID := fmt.Sprintf("%d-%s-%d", userID, req.GameID, time.Now().UnixNano())
// token, err := jwtutil.CreatePopOKJwt(
// userID,
// user.FirstName,
// req.Currency,
// "en",
// req.Mode,
// newSessionID,
// s.config.PopOK.SecretKey,
// 24*time.Hour,
// )
// if err != nil {
// s.logger.Error("Failed to generate new token", "userID", userID, "error", err)
// return nil, fmt.Errorf("token generation failed")
// }
// return &domain.PopOKGenerateTokenResponse{
// NewToken: token,
// }, nil
// }
func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) {
// 1. Validate token and get user ID
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
// if err != nil {
// s.logger.Error("Invalid token in cancel request", "error", err)
// return nil, fmt.Errorf("invalid token")
// }
// 2. Find the original bet transaction
originalBet, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil {
@ -316,7 +508,7 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ
// 5. Refund the bet amount (absolute value since bet amount is negative)
refundAmount := -originalBet.Amount
if err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount)); err != nil {
if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
s.logger.Error("Failed to refund bet", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("refund failed")
}
@ -399,3 +591,171 @@ func (s *service) verifySignature(callback *domain.PopOKCallback) bool {
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
// Calculate hash: sha256(privateKey + time)
rawHash := s.config.PopOK.SecretKey + now
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
// Construct request payload
payload := map[string]interface{}{
"action": "gameList",
"platform": s.config.PopOK.Platform,
"partnerId": s.config.PopOK.ClientID,
"currency": currency,
"time": now,
"hash": hash,
}
bodyBytes, err := json.Marshal(payload)
if err != nil {
s.logger.Error("Failed to marshal game list request", "error", err)
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", s.config.PopOK.BaseURL+"/serviceApi.php", bytes.NewReader(bodyBytes))
if err != nil {
s.logger.Error("Failed to create game list request", "error", err)
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
s.logger.Error("Failed to send game list request", "error", err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("PopOK game list failed with status %d: %s", resp.StatusCode, string(b))
}
var gameList domain.PopOKGameListResponse
if err := json.NewDecoder(resp.Body).Decode(&gameList); err != nil {
s.logger.Error("Failed to decode game list response", "error", err)
return nil, err
}
if gameList.Code != 0 {
return nil, fmt.Errorf("PopOK error: %s", gameList.Message)
}
return gameList.Data.Slots, nil
}
func (s *service) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) {
// Fetch all available games
games, err := s.ListGames(ctx, "ETB")
if err != nil || len(games) == 0 {
return nil, fmt.Errorf("could not fetch games")
}
// Check if user has existing interaction
history, err := s.repo.GetUserGameHistory(ctx, userID)
if err != nil {
s.logger.Warn("No previous game history", "userID", userID)
}
recommendations := []domain.GameRecommendation{}
if len(history) > 0 {
// Score games based on interaction frequency
gameScores := map[int64]int{}
for _, h := range history {
if h.GameID != nil {
gameScores[*h.GameID]++
}
}
// Sort by score descending
sort.SliceStable(games, func(i, j int) bool {
return gameScores[int64(games[i].ID)] > gameScores[int64(games[j].ID)]
})
// Pick top 3
for _, g := range games[:min(3, len(games))] {
recommendations = append(recommendations, domain.GameRecommendation{
GameID: g.ID,
GameName: g.GameName,
Thumbnail: g.Thumbnail,
Bets: g.Bets,
Reason: "Based on your activity",
})
}
} else {
// Pick 3 random games for new users
rand.Shuffle(len(games), func(i, j int) {
games[i], games[j] = games[j], games[i]
})
for _, g := range games[:min(3, len(games))] {
recommendations = append(recommendations, domain.GameRecommendation{
GameID: g.ID,
GameName: g.GameName,
Thumbnail: g.Thumbnail,
Bets: g.Bets,
Reason: "Random pick",
})
}
}
return recommendations, nil
}
func toInt64Ptr(s string) *int64 {
id, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return nil
}
return &id
}
func (s *service) AddFavoriteGame(ctx context.Context, userID, gameID int64) error {
return s.repo.AddFavoriteGame(ctx, userID, gameID)
}
func (s *service) RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error {
return s.repo.RemoveFavoriteGame(ctx, userID, gameID)
}
func (s *service) ListFavoriteGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) {
gameIDs, err := s.repo.ListFavoriteGames(ctx, userID)
if err != nil {
s.logger.Error("Failed to list favorite games", "userID", userID, "error", err)
return nil, err
}
if len(gameIDs) == 0 {
return []domain.GameRecommendation{}, nil
}
allGames, err := s.ListGames(ctx, "ETB") // You can use dynamic currency if needed
if err != nil {
return nil, err
}
var favorites []domain.GameRecommendation
idMap := make(map[int64]bool)
for _, id := range gameIDs {
idMap[id] = true
}
for _, g := range allGames {
if idMap[int64(g.ID)] {
favorites = append(favorites, domain.GameRecommendation{
GameID: g.ID,
GameName: g.GameName,
Thumbnail: g.Thumbnail,
Bets: g.Bets,
Reason: "Marked as favorite",
})
}
}
return favorites, nil
}

View File

@ -0,0 +1,65 @@
package veli
import (
"context"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/go-resty/resty/v2"
)
type VeliClient struct {
client *resty.Client
config *config.Config
}
func NewVeliClient(cfg *config.Config) *VeliClient {
client := resty.New().
SetBaseURL(cfg.VeliGames.APIKey).
SetHeader("Accept", "application/json").
SetHeader("X-API-Key", cfg.VeliGames.APIKey).
SetTimeout(30 * time.Second)
return &VeliClient{
client: client,
config: cfg,
}
}
func (vc *VeliClient) Get(ctx context.Context, endpoint string, result interface{}) error {
resp, err := vc.client.R().
SetContext(ctx).
SetResult(result).
Get(endpoint)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
if resp.IsError() {
return fmt.Errorf("API error: %s", resp.Status())
}
return nil
}
func (vc *VeliClient) Post(ctx context.Context, endpoint string, body interface{}, result interface{}) error {
resp, err := vc.client.R().
SetContext(ctx).
SetBody(body).
SetResult(result).
Post(endpoint)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
if resp.IsError() {
return fmt.Errorf("API error: %s", resp.Status())
}
return nil
}
// Add other HTTP methods as needed (Put, Delete, etc.)

View File

@ -1,158 +1,162 @@
package veli
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"log/slog"
"net/url"
"time"
// import (
// "context"
// "fmt"
// "log/slog"
// "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
)
// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
// )
type VeliPlayService struct {
repo repository.VirtualGameRepository
walletSvc wallet.Service
config *config.VeliGamesConfig
logger *slog.Logger
}
// type Service struct {
// client *VeliClient
// gameRepo repository.VeliGameRepository
// playerRepo repository.VeliPlayerRepository
// txRepo repository.VeliTransactionRepository
// walletSvc wallet.Service
// logger domain.Logger
// }
func NewVeliPlayService(
repo repository.VirtualGameRepository,
walletSvc wallet.Service,
cfg *config.Config,
logger *slog.Logger,
) *VeliPlayService {
return &VeliPlayService{
repo: repo,
walletSvc: walletSvc,
config: &cfg.VeliGames,
logger: logger,
}
}
// func NewService(
// client *VeliClient,
// gameRepo repository.VeliGameRepository,
// playerRepo repository.VeliPlayerRepository,
// txRepo repository.VeliTransactionRepository,
// walletSvc wallet.Service,
// logger *slog.Logger,
// ) *Service {
// return &Service{
// client: client,
// gameRepo: gameRepo,
// playerRepo: playerRepo,
// txRepo: txRepo,
// walletSvc: walletSvc,
// logger: logger,
// }
// }
func (s *VeliPlayService) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) {
session := &domain.VirtualGameSession{
UserID: userID,
GameID: gameID,
SessionToken: generateSessionToken(userID),
Currency: currency,
Status: "ACTIVE",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
ExpiresAt: time.Now().Add(24 * time.Hour),
}
// func (s *Service) SyncGames(ctx context.Context) error {
// games, err := s.client.GetGameList(ctx)
// if err != nil {
// return fmt.Errorf("failed to get game list: %w", err)
// }
if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil {
return "", fmt.Errorf("failed to create game session: %w", err)
}
// for _, game := range games {
// existing, err := s.gameRepo.GetGameByID(ctx, game.ID)
// if err != nil && err != domain.ErrGameNotFound {
// return fmt.Errorf("failed to check existing game: %w", err)
// }
// Veli-specific parameters
params := url.Values{
"operator_key": []string{s.config.OperatorKey}, // Different from Alea's operator_id
"user_id": []string{fmt.Sprintf("%d", userID)},
"game_id": []string{gameID},
"currency": []string{currency},
"mode": []string{mode},
"timestamp": []string{fmt.Sprintf("%d", time.Now().Unix())},
}
// if existing == nil {
// // New game - create
// if err := s.gameRepo.CreateGame(ctx, game); err != nil {
// s.logger.Error("failed to create game", "game_id", game.ID, "error", err)
// continue
// }
// } else {
// // Existing game - update
// if err := s.gameRepo.UpdateGame(ctx, game); err != nil {
// s.logger.Error("failed to update game", "game_id", game.ID, "error", err)
// continue
// }
// }
// }
signature := s.generateSignature(params.Encode())
params.Add("signature", signature)
// return nil
// }
return fmt.Sprintf("%s/launch?%s", s.config.APIURL, params.Encode()), nil
}
// func (s *Service) LaunchGame(ctx context.Context, playerID, gameID string) (string, error) {
// // Verify player exists
// player, err := s.playerRepo.GetPlayer(ctx, playerID)
// if err != nil {
// return "", fmt.Errorf("failed to get player: %w", err)
// }
func (s *VeliPlayService) HandleCallback(ctx context.Context, callback *domain.VeliCallback) error {
if !s.verifyCallbackSignature(callback) {
return errors.New("invalid callback signature")
}
// // Verify game exists
// game, err := s.gameRepo.GetGameByID(ctx, gameID)
// if err != nil {
// return "", fmt.Errorf("failed to get game: %w", err)
// }
// Veli uses round_id instead of transaction_id for idempotency
existing, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.RoundID)
if err != nil || existing != nil {
s.logger.Warn("duplicate round detected", "round_id", callback.RoundID)
return nil
}
// // Call Veli API
// gameURL, err := s.client.LaunchGame(ctx, playerID, gameID)
// if err != nil {
// return "", fmt.Errorf("failed to launch game: %w", err)
// }
session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID)
if err != nil {
return fmt.Errorf("failed to get game session: %w", err)
}
// // Create game session record
// session := domain.GameSession{
// SessionID: fmt.Sprintf("%s-%s-%d", playerID, gameID, time.Now().Unix()),
// PlayerID: playerID,
// GameID: gameID,
// LaunchTime: time.Now(),
// Status: "active",
// }
// Convert amount based on event type (BET, WIN, etc.)
amount := convertAmount(callback.Amount, callback.EventType)
// if err := s.gameRepo.CreateGameSession(ctx, session); err != nil {
// s.logger.Error("failed to create game session", "error", err)
// }
tx := &domain.VirtualGameTransaction{
SessionID: session.ID,
UserID: session.UserID,
TransactionType: callback.EventType, // e.g., "bet_placed", "game_result"
Amount: amount,
Currency: callback.Currency,
ExternalTransactionID: callback.RoundID, // Veli uses round_id as the unique identifier
Status: "COMPLETED",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
GameSpecificData: domain.GameSpecificData{
Multiplier: callback.Multiplier, // Used for Aviator/Plinko
},
}
// return gameURL, nil
// }
if err := s.processTransaction(ctx, tx, session.UserID); err != nil {
return fmt.Errorf("failed to process transaction: %w", err)
}
// func (s *Service) PlaceBet(ctx context.Context, playerID, gameID string, amount float64) (*domain.VeliTransaction, error) {
// // 1. Verify player balance
// balance, err := s.walletRepo.GetBalance(ctx, playerID)
// if err != nil {
// return nil, fmt.Errorf("failed to get balance: %w", err)
// }
return nil
}
// if balance < amount {
// return nil, domain.ErrInsufficientBalance
// }
func (s *VeliPlayService) generateSignature(data string) string {
h := hmac.New(sha256.New, []byte(s.config.SecretKey))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
// // 2. Create transaction record
// tx := domain.VeliTransaction{
// TransactionID: generateTransactionID(),
// PlayerID: playerID,
// GameID: gameID,
// Amount: amount,
// Type: "bet",
// Status: "pending",
// CreatedAt: time.Now(),
// }
func (s *VeliPlayService) verifyCallbackSignature(cb *domain.VeliCallback) bool {
signData := fmt.Sprintf("%s%s%s%.2f%s%d",
cb.RoundID, // Veli uses round_id instead of transaction_id
cb.SessionID,
cb.EventType,
cb.Amount,
cb.Currency,
cb.Timestamp,
)
expectedSig := s.generateSignature(signData)
return expectedSig == cb.Signature
}
// if err := s.txRepo.CreateTransaction(ctx, tx); err != nil {
// return nil, fmt.Errorf("failed to create transaction: %w", err)
// }
func convertAmount(amount float64, eventType string) int64 {
cents := int64(amount * 100)
if eventType == "bet_placed" {
return -cents // Debit for bets
}
return cents // Credit for wins/results
}
// // 3. Call Veli API
// if err := s.client.PlaceBet(ctx, tx.TransactionID, playerID, gameID, amount); err != nil {
// // Update transaction status
// tx.Status = "failed"
// _ = s.txRepo.UpdateTransaction(ctx, tx)
// return nil, fmt.Errorf("failed to place bet: %w", err)
// }
func generateSessionToken(userID int64) string {
return fmt.Sprintf("veli-%d-%d", userID, time.Now().UnixNano())
}
// // 4. Deduct from wallet
// if err := s.walletRepo.DeductBalance(ctx, playerID, amount); err != nil {
// // Attempt to rollback
// _ = s.client.RollbackBet(ctx, tx.TransactionID)
// tx.Status = "failed"
// _ = s.txRepo.UpdateTransaction(ctx, tx)
// return nil, fmt.Errorf("failed to deduct balance: %w", err)
// }
func (s *VeliPlayService) processTransaction(ctx context.Context, tx *domain.VirtualGameTransaction, userID int64) error {
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
if err != nil || len(wallets) == 0 {
return errors.New("no wallet available for user")
}
tx.WalletID = wallets[0].ID
// // 5. Update transaction status
// tx.Status = "completed"
// if err := s.txRepo.UpdateTransaction(ctx, tx); err != nil {
// s.logger.Error("failed to update transaction status", "error", err)
// }
if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil {
return fmt.Errorf("wallet update failed: %w", err)
}
// return &tx, nil
// }
return s.repo.CreateVirtualGameTransaction(ctx, tx)
}
// // Implement SettleBet, RollbackBet, GetBalance, etc. following similar patterns
// func generateTransactionID() string {
// return fmt.Sprintf("tx-%d", time.Now().UnixNano())
// }

View File

@ -91,12 +91,11 @@ func (s *Service) checkWalletThresholds() {
// Initialize initial deposit if not set
s.mu.Lock()
if _, exists := s.initialDeposits[company.ID]; !exists {
initialDeposit, exists := s.initialDeposits[company.ID]
if !exists || wallet.Balance > initialDeposit {
s.initialDeposits[company.ID] = wallet.Balance
s.mu.Unlock()
continue
initialDeposit = wallet.Balance // update local variable
}
initialDeposit := s.initialDeposits[company.ID]
s.mu.Unlock()
if initialDeposit == 0 {

View File

@ -7,11 +7,14 @@ import (
)
type WalletStore interface {
// GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error)
// GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error)
CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error)
CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error)
GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error)
GetAllWallets(ctx context.Context) ([]domain.Wallet, error)
GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wallet, error)
GetAllCustomerWallets(ctx context.Context) ([]domain.GetCustomerWallet, error)
GetCustomerWallet(ctx context.Context, customerID int64) (domain.GetCustomerWallet, error)
GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error)
UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error

View File

@ -10,6 +10,7 @@ type Service struct {
walletStore WalletStore
transferStore TransferStore
notificationStore notificationservice.NotificationStore
notificationSvc *notificationservice.Service
logger *slog.Logger
}

Some files were not shown because too many files have changed in this diff Show More