Merge branch 'main' into ticket-bet

This commit is contained in:
Samuel Tariku 2025-06-25 22:48:59 +03:00
commit d6abaac828
56 changed files with 5321 additions and 1458 deletions

View File

@ -35,6 +35,8 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "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" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
@ -164,6 +166,8 @@ func main() {
go httpserver.SetupReportCronJobs(context.Background(), reportSvc) go httpserver.SetupReportCronJobs(context.Background(), reportSvc)
bankRepository := repository.NewBankRepository(store)
instSvc := institutions.New(bankRepository)
// Initialize report worker with CSV exporter // Initialize report worker with CSV exporter
// csvExporter := infrastructure.CSVExporter{ // csvExporter := infrastructure.CSVExporter{
// ExportPath: cfg.ReportExportPath, // Make sure to add this to your config // ExportPath: cfg.ReportExportPath, // Make sure to add this to your config
@ -198,10 +202,35 @@ func main() {
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc) httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc)
httpserver.StartTicketCrons(*ticketSvc) httpserver.StartTicketCrons(*ticketSvc)
// 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) // go httpserver.SetupReportCronJob(reportWorker)
// Initialize and start HTTP server // Initialize and start HTTP server
app := httpserver.NewApp( app := httpserver.NewApp(
issueReportingSvc,
instSvc,
currSvc, currSvc,
cfg.Port, cfg.Port,
v, v,

View File

@ -112,6 +112,23 @@ CREATE TABLE IF NOT EXISTS ticket_outcomes (
status INT NOT NULL DEFAULT 0, status INT NOT NULL DEFAULT 0,
expires TIMESTAMP NOT NULL 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 ( CREATE TABLE IF NOT EXISTS wallets (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
balance BIGINT NOT NULL DEFAULT 0, balance BIGINT NOT NULL DEFAULT 0,

View File

@ -30,6 +30,9 @@ CREATE TABLE virtual_game_transactions (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id), session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id),
user_id BIGINT NOT NULL REFERENCES users(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), wallet_id BIGINT NOT NULL REFERENCES wallets(id),
transaction_type VARCHAR(20) NOT NULL, transaction_type VARCHAR(20) NOT NULL,
amount BIGINT NOT NULL, amount BIGINT NOT NULL,
@ -44,6 +47,8 @@ CREATE TABLE virtual_game_histories (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(100), -- nullable session_id VARCHAR(100), -- nullable
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
company_id BIGINT,
provider VARCHAR(100),
wallet_id BIGINT, -- nullable wallet_id BIGINT, -- nullable
game_id BIGINT, -- nullable game_id BIGINT, -- nullable
transaction_type VARCHAR(20) NOT NULL, -- e.g., BET, WIN, CANCEL transaction_type VARCHAR(20) NOT NULL, -- e.g., BET, WIN, CANCEL
@ -56,6 +61,13 @@ CREATE TABLE virtual_game_histories (
updated_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 -- Optional: Indexes for performance
CREATE INDEX idx_virtual_game_user_id ON virtual_game_histories(user_id); 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_transaction_type ON virtual_game_histories(transaction_type);
@ -65,3 +77,7 @@ CREATE INDEX idx_virtual_game_external_transaction_id ON virtual_game_histories(
CREATE INDEX idx_virtual_game_sessions_user_id ON virtual_game_sessions(user_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_session_id ON virtual_game_transactions(session_id);
CREATE INDEX idx_virtual_game_transactions_user_id ON virtual_game_transactions(user_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

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

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

@ -1,34 +1,44 @@
-- name: GetTotalBetsMadeInRange :one -- name: GetTotalBetsMadeInRange :one
SELECT COUNT(*) AS total_bets SELECT COUNT(*) AS total_bets
FROM bets FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to');
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
);
-- name: GetTotalCashMadeInRange :one -- name: GetTotalCashMadeInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_made SELECT COALESCE(SUM(amount), 0) AS total_cash_made
FROM bets FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to');
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
);
-- name: GetTotalCashOutInRange :one -- name: GetTotalCashOutInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_out SELECT COALESCE(SUM(amount), 0) AS total_cash_out
FROM bets FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
AND cashed_out = true AND cashed_out = true;
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
);
-- name: GetTotalCashBacksInRange :one -- name: GetTotalCashBacksInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_backs SELECT COALESCE(SUM(amount), 0) AS total_cash_backs
FROM bets FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
AND status = 5 AND status = 5;
AND ( -- name: GetCompanyWiseReport :many
company_id = sqlc.narg('company_id') SELECT
OR sqlc.narg('company_id') IS NULL 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;

View File

@ -4,28 +4,26 @@ INSERT INTO virtual_game_sessions (
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6 $1, $2, $3, $4, $5, $6
) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at; ) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at;
-- name: GetVirtualGameSessionByToken :one -- name: GetVirtualGameSessionByToken :one
SELECT id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at SELECT id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at
FROM virtual_game_sessions FROM virtual_game_sessions
WHERE session_token = $1; WHERE session_token = $1;
-- name: UpdateVirtualGameSessionStatus :exec -- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions UPDATE virtual_game_sessions
SET status = $2, updated_at = CURRENT_TIMESTAMP SET status = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $1; WHERE id = $1;
-- name: CreateVirtualGameTransaction :one -- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions ( 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 ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10
) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at; ) 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 -- name: CreateVirtualGameHistory :one
INSERT INTO virtual_game_histories ( INSERT INTO virtual_game_histories (
session_id, session_id,
user_id, user_id,
company_id,
provider,
wallet_id, wallet_id,
game_id, game_id,
transaction_type, transaction_type,
@ -35,11 +33,13 @@ INSERT INTO virtual_game_histories (
reference_transaction_id, reference_transaction_id,
status status
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
) RETURNING ) RETURNING
id, id,
session_id, session_id,
user_id, user_id,
company_id,
provider,
wallet_id, wallet_id,
game_id, game_id,
transaction_type, transaction_type,
@ -50,25 +50,39 @@ INSERT INTO virtual_game_histories (
status, status,
created_at, created_at,
updated_at; updated_at;
-- name: GetVirtualGameTransactionByExternalID :one -- name: GetVirtualGameTransactionByExternalID :one
SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
FROM virtual_game_transactions FROM virtual_game_transactions
WHERE external_transaction_id = $1; WHERE external_transaction_id = $1;
-- name: UpdateVirtualGameTransactionStatus :exec -- name: UpdateVirtualGameTransactionStatus :exec
UPDATE virtual_game_transactions UPDATE virtual_game_transactions
SET status = $2, updated_at = CURRENT_TIMESTAMP SET status = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $1; WHERE id = $1;
-- name: GetVirtualGameSummaryInRange :many -- name: GetVirtualGameSummaryInRange :many
SELECT SELECT
c.name AS company_name,
vg.name AS game_name, vg.name AS game_name,
COUNT(vgh.id) AS number_of_bets, COUNT(vgt.id) AS number_of_bets,
COALESCE(SUM(vgh.amount), 0) AS total_transaction_sum COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum
FROM virtual_game_histories vgh FROM virtual_game_transactions vgt
JOIN virtual_games vg ON vgh.game_id = vg.id JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
WHERE vgh.transaction_type = 'BET' JOIN virtual_games vg ON vgs.game_id = vg.id
AND vgh.created_at BETWEEN $1 AND $2 JOIN companies c ON vgt.company_id = c.id
GROUP BY vg.name; 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

@ -56,3 +56,13 @@ UPDATE wallets
SET is_active = $1, SET is_active = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $2; 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;

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

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

@ -55,6 +55,24 @@ func (ns NullReferralstatus) Value() (driver.Value, error) {
return string(ns.Referralstatus), nil 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 { type Bet struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
@ -233,6 +251,13 @@ type ExchangeRate struct {
CreatedAt pgtype.Timestamp `json:"created_at"` 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 { type League struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -327,6 +352,18 @@ type RefreshToken struct {
Revoked bool `json:"revoked"` 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 { type Result struct {
ID int64 `json:"id"` ID int64 `json:"id"`
BetOutcomeID int64 `json:"bet_outcome_id"` BetOutcomeID int64 `json:"bet_outcome_id"`
@ -476,6 +513,8 @@ type VirtualGameHistory struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SessionID pgtype.Text `json:"session_id"` SessionID pgtype.Text `json:"session_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
WalletID pgtype.Int8 `json:"wallet_id"` WalletID pgtype.Int8 `json:"wallet_id"`
GameID pgtype.Int8 `json:"game_id"` GameID pgtype.Int8 `json:"game_id"`
TransactionType string `json:"transaction_type"` TransactionType string `json:"transaction_type"`
@ -504,6 +543,9 @@ type VirtualGameTransaction struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SessionID int64 `json:"session_id"` SessionID int64 `json:"session_id"`
UserID int64 `json:"user_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"` WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"` TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"` Amount int64 `json:"amount"`

View File

@ -11,24 +11,132 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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 const GetTotalBetsMadeInRange = `-- name: GetTotalBetsMadeInRange :one
SELECT COUNT(*) AS total_bets SELECT COUNT(*) AS total_bets
FROM bets FROM bets
WHERE created_at BETWEEN $1 AND $2 WHERE created_at BETWEEN $1 AND $2
AND (
company_id = $3
OR $3 IS NULL
)
` `
type GetTotalBetsMadeInRangeParams struct { type GetTotalBetsMadeInRangeParams struct {
From pgtype.Timestamp `json:"from"` From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"` To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
} }
func (q *Queries) GetTotalBetsMadeInRange(ctx context.Context, arg GetTotalBetsMadeInRangeParams) (int64, error) { func (q *Queries) GetTotalBetsMadeInRange(ctx context.Context, arg GetTotalBetsMadeInRangeParams) (int64, error) {
row := q.db.QueryRow(ctx, GetTotalBetsMadeInRange, arg.From, arg.To, arg.CompanyID) row := q.db.QueryRow(ctx, GetTotalBetsMadeInRange, arg.From, arg.To)
var total_bets int64 var total_bets int64
err := row.Scan(&total_bets) err := row.Scan(&total_bets)
return total_bets, err return total_bets, err
@ -39,20 +147,15 @@ SELECT COALESCE(SUM(amount), 0) AS total_cash_backs
FROM bets FROM bets
WHERE created_at BETWEEN $1 AND $2 WHERE created_at BETWEEN $1 AND $2
AND status = 5 AND status = 5
AND (
company_id = $3
OR $3 IS NULL
)
` `
type GetTotalCashBacksInRangeParams struct { type GetTotalCashBacksInRangeParams struct {
From pgtype.Timestamp `json:"from"` From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"` To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
} }
func (q *Queries) GetTotalCashBacksInRange(ctx context.Context, arg GetTotalCashBacksInRangeParams) (interface{}, error) { func (q *Queries) GetTotalCashBacksInRange(ctx context.Context, arg GetTotalCashBacksInRangeParams) (interface{}, error) {
row := q.db.QueryRow(ctx, GetTotalCashBacksInRange, arg.From, arg.To, arg.CompanyID) row := q.db.QueryRow(ctx, GetTotalCashBacksInRange, arg.From, arg.To)
var total_cash_backs interface{} var total_cash_backs interface{}
err := row.Scan(&total_cash_backs) err := row.Scan(&total_cash_backs)
return total_cash_backs, err return total_cash_backs, err
@ -62,20 +165,15 @@ const GetTotalCashMadeInRange = `-- name: GetTotalCashMadeInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_made SELECT COALESCE(SUM(amount), 0) AS total_cash_made
FROM bets FROM bets
WHERE created_at BETWEEN $1 AND $2 WHERE created_at BETWEEN $1 AND $2
AND (
company_id = $3
OR $3 IS NULL
)
` `
type GetTotalCashMadeInRangeParams struct { type GetTotalCashMadeInRangeParams struct {
From pgtype.Timestamp `json:"from"` From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"` To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
} }
func (q *Queries) GetTotalCashMadeInRange(ctx context.Context, arg GetTotalCashMadeInRangeParams) (interface{}, error) { func (q *Queries) GetTotalCashMadeInRange(ctx context.Context, arg GetTotalCashMadeInRangeParams) (interface{}, error) {
row := q.db.QueryRow(ctx, GetTotalCashMadeInRange, arg.From, arg.To, arg.CompanyID) row := q.db.QueryRow(ctx, GetTotalCashMadeInRange, arg.From, arg.To)
var total_cash_made interface{} var total_cash_made interface{}
err := row.Scan(&total_cash_made) err := row.Scan(&total_cash_made)
return total_cash_made, err return total_cash_made, err
@ -86,20 +184,15 @@ SELECT COALESCE(SUM(amount), 0) AS total_cash_out
FROM bets FROM bets
WHERE created_at BETWEEN $1 AND $2 WHERE created_at BETWEEN $1 AND $2
AND cashed_out = true AND cashed_out = true
AND (
company_id = $3
OR $3 IS NULL
)
` `
type GetTotalCashOutInRangeParams struct { type GetTotalCashOutInRangeParams struct {
From pgtype.Timestamp `json:"from"` From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"` To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
} }
func (q *Queries) GetTotalCashOutInRange(ctx context.Context, arg GetTotalCashOutInRangeParams) (interface{}, error) { func (q *Queries) GetTotalCashOutInRange(ctx context.Context, arg GetTotalCashOutInRangeParams) (interface{}, error) {
row := q.db.QueryRow(ctx, GetTotalCashOutInRange, arg.From, arg.To, arg.CompanyID) row := q.db.QueryRow(ctx, GetTotalCashOutInRange, arg.From, arg.To)
var total_cash_out interface{} var total_cash_out interface{}
err := row.Scan(&total_cash_out) err := row.Scan(&total_cash_out)
return total_cash_out, err return total_cash_out, err

View File

@ -11,10 +11,31 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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 const CreateVirtualGameHistory = `-- name: CreateVirtualGameHistory :one
INSERT INTO virtual_game_histories ( INSERT INTO virtual_game_histories (
session_id, session_id,
user_id, user_id,
company_id,
provider,
wallet_id, wallet_id,
game_id, game_id,
transaction_type, transaction_type,
@ -24,11 +45,13 @@ INSERT INTO virtual_game_histories (
reference_transaction_id, reference_transaction_id,
status status
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
) RETURNING ) RETURNING
id, id,
session_id, session_id,
user_id, user_id,
company_id,
provider,
wallet_id, wallet_id,
game_id, game_id,
transaction_type, transaction_type,
@ -44,6 +67,8 @@ INSERT INTO virtual_game_histories (
type CreateVirtualGameHistoryParams struct { type CreateVirtualGameHistoryParams struct {
SessionID pgtype.Text `json:"session_id"` SessionID pgtype.Text `json:"session_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
WalletID pgtype.Int8 `json:"wallet_id"` WalletID pgtype.Int8 `json:"wallet_id"`
GameID pgtype.Int8 `json:"game_id"` GameID pgtype.Int8 `json:"game_id"`
TransactionType string `json:"transaction_type"` TransactionType string `json:"transaction_type"`
@ -58,6 +83,8 @@ func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtua
row := q.db.QueryRow(ctx, CreateVirtualGameHistory, row := q.db.QueryRow(ctx, CreateVirtualGameHistory,
arg.SessionID, arg.SessionID,
arg.UserID, arg.UserID,
arg.CompanyID,
arg.Provider,
arg.WalletID, arg.WalletID,
arg.GameID, arg.GameID,
arg.TransactionType, arg.TransactionType,
@ -72,6 +99,8 @@ func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtua
&i.ID, &i.ID,
&i.SessionID, &i.SessionID,
&i.UserID, &i.UserID,
&i.CompanyID,
&i.Provider,
&i.WalletID, &i.WalletID,
&i.GameID, &i.GameID,
&i.TransactionType, &i.TransactionType,
@ -129,27 +158,47 @@ func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtua
const CreateVirtualGameTransaction = `-- name: CreateVirtualGameTransaction :one const CreateVirtualGameTransaction = `-- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions ( 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 ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10
) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at ) 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 { type CreateVirtualGameTransactionParams struct {
SessionID int64 `json:"session_id"` SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
WalletID int64 `json:"wallet_id"` CompanyID pgtype.Int8 `json:"company_id"`
TransactionType string `json:"transaction_type"` Provider pgtype.Text `json:"provider"`
Amount int64 `json:"amount"` WalletID int64 `json:"wallet_id"`
Currency string `json:"currency"` TransactionType string `json:"transaction_type"`
ExternalTransactionID string `json:"external_transaction_id"` Amount int64 `json:"amount"`
Status string `json:"status"` 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, row := q.db.QueryRow(ctx, CreateVirtualGameTransaction,
arg.SessionID, arg.SessionID,
arg.UserID, arg.UserID,
arg.CompanyID,
arg.Provider,
arg.WalletID, arg.WalletID,
arg.TransactionType, arg.TransactionType,
arg.Amount, arg.Amount,
@ -157,11 +206,13 @@ func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVi
arg.ExternalTransactionID, arg.ExternalTransactionID,
arg.Status, arg.Status,
) )
var i VirtualGameTransaction var i CreateVirtualGameTransactionRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.SessionID, &i.SessionID,
&i.UserID, &i.UserID,
&i.CompanyID,
&i.Provider,
&i.WalletID, &i.WalletID,
&i.TransactionType, &i.TransactionType,
&i.Amount, &i.Amount,
@ -199,22 +250,26 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken
const GetVirtualGameSummaryInRange = `-- name: GetVirtualGameSummaryInRange :many const GetVirtualGameSummaryInRange = `-- name: GetVirtualGameSummaryInRange :many
SELECT SELECT
c.name AS company_name,
vg.name AS game_name, vg.name AS game_name,
COUNT(vgh.id) AS number_of_bets, COUNT(vgt.id) AS number_of_bets,
COALESCE(SUM(vgh.amount), 0) AS total_transaction_sum COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum
FROM virtual_game_histories vgh FROM virtual_game_transactions vgt
JOIN virtual_games vg ON vgh.game_id = vg.id JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
WHERE vgh.transaction_type = 'BET' JOIN virtual_games vg ON vgs.game_id = vg.id
AND vgh.created_at BETWEEN $1 AND $2 JOIN companies c ON vgt.company_id = c.id
GROUP BY vg.name WHERE vgt.transaction_type = 'BET'
AND vgt.created_at BETWEEN $1 AND $2
GROUP BY c.name, vg.name
` `
type GetVirtualGameSummaryInRangeParams struct { type GetVirtualGameSummaryInRangeParams struct {
CreatedAt pgtype.Timestamp `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
CreatedAt_2 pgtype.Timestamp `json:"created_at_2"` CreatedAt_2 pgtype.Timestamptz `json:"created_at_2"`
} }
type GetVirtualGameSummaryInRangeRow struct { type GetVirtualGameSummaryInRangeRow struct {
CompanyName string `json:"company_name"`
GameName string `json:"game_name"` GameName string `json:"game_name"`
NumberOfBets int64 `json:"number_of_bets"` NumberOfBets int64 `json:"number_of_bets"`
TotalTransactionSum interface{} `json:"total_transaction_sum"` TotalTransactionSum interface{} `json:"total_transaction_sum"`
@ -229,7 +284,12 @@ func (q *Queries) GetVirtualGameSummaryInRange(ctx context.Context, arg GetVirtu
var items []GetVirtualGameSummaryInRangeRow var items []GetVirtualGameSummaryInRangeRow
for rows.Next() { for rows.Next() {
var i GetVirtualGameSummaryInRangeRow var i GetVirtualGameSummaryInRangeRow
if err := rows.Scan(&i.GameName, &i.NumberOfBets, &i.TotalTransactionSum); err != nil { if err := rows.Scan(
&i.CompanyName,
&i.GameName,
&i.NumberOfBets,
&i.TotalTransactionSum,
); err != nil {
return nil, err return nil, err
} }
items = append(items, i) items = append(items, i)
@ -246,9 +306,23 @@ FROM virtual_game_transactions
WHERE external_transaction_id = $1 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) row := q.db.QueryRow(ctx, GetVirtualGameTransactionByExternalID, externalTransactionID)
var i VirtualGameTransaction var i GetVirtualGameTransactionByExternalIDRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.SessionID, &i.SessionID,
@ -265,6 +339,47 @@ func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, ext
return i, err 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 const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions UPDATE virtual_game_sessions
SET status = $2, updated_at = CURRENT_TIMESTAMP SET status = $2, updated_at = CURRENT_TIMESTAMP

View File

@ -221,6 +221,50 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) {
return items, nil return items, nil
} }
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
`
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
}
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 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 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 FROM customer_wallet_details

View File

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

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

@ -97,15 +97,16 @@ func FromJSON(data []byte) (*Notification, error) {
func ReceiverFromRole(role Role) NotificationRecieverSide { func ReceiverFromRole(role Role) NotificationRecieverSide {
if role == RoleAdmin { switch role {
case RoleAdmin:
return NotificationRecieverSideAdmin return NotificationRecieverSideAdmin
} else if role == RoleCashier { case RoleCashier:
return NotificationRecieverSideCashier return NotificationRecieverSideCashier
} else if role == RoleBranchManager { case RoleBranchManager:
return NotificationRecieverSideBranchManager return NotificationRecieverSideBranchManager
} else if role == RoleCustomer { case RoleCustomer:
return NotificationRecieverSideCustomer return NotificationRecieverSideCustomer
} else { default:
return "" return ""
} }
} }

View File

@ -33,6 +33,8 @@ type ReportData struct {
Deposits float64 Deposits float64
TotalTickets int64 TotalTickets int64
VirtualGameStats []VirtualGameStat VirtualGameStats []VirtualGameStat
CompanyReports []CompanyReport
BranchReports []BranchReport
} }
type VirtualGameStat struct { type VirtualGameStat struct {
@ -366,3 +368,41 @@ type CashierPerformance struct {
LastActivity time.Time `json:"last_activity"` LastActivity time.Time `json:"last_activity"`
ActiveDays int `json:"active_days"` 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

@ -4,6 +4,30 @@ import (
"time" "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 { type VirtualGame struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -42,6 +66,8 @@ type VirtualGameHistory struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SessionID string `json:"session_id,omitempty"` // Optional, if session tracking is used SessionID string `json:"session_id,omitempty"` // Optional, if session tracking is used
UserID int64 `json:"user_id"` 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 WalletID *int64 `json:"wallet_id,omitempty"` // Optional if wallet detail is needed
GameID *int64 `json:"game_id,omitempty"` // Optional for game-level analysis GameID *int64 `json:"game_id,omitempty"` // Optional for game-level analysis
TransactionType string `json:"transaction_type"` // BET, WIN, CANCEL, etc. TransactionType string `json:"transaction_type"` // BET, WIN, CANCEL, etc.
@ -58,6 +84,9 @@ type VirtualGameTransaction struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SessionID int64 `json:"session_id"` SessionID int64 `json:"session_id"`
UserID int64 `json:"user_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"` WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc. TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc.
Amount int64 `json:"amount"` // Always in cents Amount int64 `json:"amount"` // Always in cents
@ -159,6 +188,11 @@ type PopOKWinResponse struct {
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
} }
type PopOKGenerateTokenRequest struct {
GameID string `json:"newGameId"`
Token string `json:"token"`
}
type PopOKCancelRequest struct { type PopOKCancelRequest struct {
ExternalToken string `json:"externalToken"` ExternalToken string `json:"externalToken"`
PlayerID string `json:"playerId"` PlayerID string `json:"playerId"`
@ -172,6 +206,10 @@ type PopOKCancelResponse struct {
Balance float64 `json:"balance"` Balance float64 `json:"balance"`
} }
type PopOKGenerateTokenResponse struct {
NewToken string `json:"newToken"`
}
type AleaPlayCallback struct { type AleaPlayCallback struct {
EventID string `json:"event_id"` EventID string `json:"event_id"`
TransactionID string `json:"transaction_id"` TransactionID string `json:"transaction_id"`

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

@ -317,6 +317,40 @@ func (s *Store) CountUnreadNotifications(ctx context.Context, userID int64) (int
return count, nil 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,
IsSuspended: 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) { // func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
// dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{ // dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{
// Limit: int32(limit), // Limit: int32(limit),

View File

@ -15,13 +15,15 @@ type ReportRepository interface {
SaveReport(report *domain.Report) error SaveReport(report *domain.Report) error
FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit int) ([]*domain.Report, error) FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit int) ([]*domain.Report, error)
GetTotalCashOutInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalCashMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalCashBacksInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (int64, error) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error)
GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error)
GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error)
GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, 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 { type ReportRepo struct {
@ -117,20 +119,18 @@ func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit in
return reports, nil return reports, nil
} }
func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (int64, error) { func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) {
params := dbgen.GetTotalBetsMadeInRangeParams{ params := dbgen.GetTotalBetsMadeInRangeParams{
From: ToPgTimestamp(from), From: ToPgTimestamp(from),
To: ToPgTimestamp(to), To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
} }
return r.store.queries.GetTotalBetsMadeInRange(ctx, params) return r.store.queries.GetTotalBetsMadeInRange(ctx, params)
} }
func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) { func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) {
params := dbgen.GetTotalCashBacksInRangeParams{ params := dbgen.GetTotalCashBacksInRangeParams{
From: ToPgTimestamp(from), From: ToPgTimestamp(from),
To: ToPgTimestamp(to), To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
} }
value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params) value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params)
if err != nil { if err != nil {
@ -139,11 +139,10 @@ func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time
return parseFloat(value) return parseFloat(value)
} }
func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) { func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) {
params := dbgen.GetTotalCashMadeInRangeParams{ params := dbgen.GetTotalCashMadeInRangeParams{
From: ToPgTimestamp(from), From: ToPgTimestamp(from),
To: ToPgTimestamp(to), To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
} }
value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params) value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params)
if err != nil { if err != nil {
@ -152,11 +151,10 @@ func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.
return parseFloat(value) return parseFloat(value)
} }
func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) { func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) {
params := dbgen.GetTotalCashOutInRangeParams{ params := dbgen.GetTotalCashOutInRangeParams{
From: ToPgTimestamp(from), From: ToPgTimestamp(from),
To: ToPgTimestamp(to), To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
} }
value, err := r.store.queries.GetTotalCashOutInRange(ctx, params) value, err := r.store.queries.GetTotalCashOutInRange(ctx, params)
if err != nil { if err != nil {
@ -183,8 +181,8 @@ func (r *ReportRepo) GetAllTicketsInRange(ctx context.Context, from, to time.Tim
func (r *ReportRepo) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) { func (r *ReportRepo) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) {
params := dbgen.GetVirtualGameSummaryInRangeParams{ params := dbgen.GetVirtualGameSummaryInRangeParams{
CreatedAt: ToPgTimestamp(from), CreatedAt: ToPgTimestamptz(from),
CreatedAt_2: ToPgTimestamp(to), CreatedAt_2: ToPgTimestamptz(to),
} }
return r.store.queries.GetVirtualGameSummaryInRange(ctx, params) return r.store.queries.GetVirtualGameSummaryInRange(ctx, params)
} }
@ -193,8 +191,8 @@ func ToPgTimestamp(t time.Time) pgtype.Timestamp {
return pgtype.Timestamp{Time: t, Valid: true} return pgtype.Timestamp{Time: t, Valid: true}
} }
func ToPgInt8(i int64) pgtype.Int8 { func ToPgTimestamptz(t time.Time) pgtype.Timestamptz {
return pgtype.Int8{Int64: i, Valid: true} return pgtype.Timestamptz{Time: t, Valid: true}
} }
func parseFloat(value interface{}) (float64, error) { func parseFloat(value interface{}) (float64, error) {
@ -218,3 +216,19 @@ func parseFloat(value interface{}) (float64, error) {
return 0, fmt.Errorf("unexpected type %T for value: %+v", v, v) 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

@ -19,6 +19,9 @@ type VirtualGameRepository interface {
GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error)
UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error
// WithTransaction(ctx context.Context, fn func(ctx context.Context) error) 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) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error) GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error)
@ -38,6 +41,26 @@ func NewVirtualGameRepository(store *Store) VirtualGameRepository {
return &VirtualGameRepo{store: store} 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 { func (r *VirtualGameRepo) CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error {
params := dbgen.CreateVirtualGameSessionParams{ params := dbgen.CreateVirtualGameSessionParams{
UserID: session.UserID, UserID: session.UserID,

View File

@ -275,3 +275,4 @@ func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter)
return total, nil return total, nil
} }

View File

@ -31,8 +31,8 @@ func NewClient(baseURL, secretKey string) *Client {
func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) { func (c *Client) InitializePayment(ctx context.Context, req domain.ChapaDepositRequest) (domain.ChapaDepositResponse, error) {
payload := map[string]interface{}{ payload := map[string]interface{}{
"amount": fmt.Sprintf("%.2f", float64(req.Amount)/100), "amount": fmt.Sprintf("%.2f", float64(req.Amount)/100),
"currency": req.Currency, "currency": req.Currency,
// "email": req.Email, // "email": req.Email,
"first_name": req.FirstName, "first_name": req.FirstName,
"last_name": req.LastName, "last_name": req.LastName,
@ -175,6 +175,51 @@ func (c *Client) ManualVerifyPayment(ctx context.Context, txRef string) (*domain
}, nil }, 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) { func (c *Client) FetchSupportedBanks(ctx context.Context) ([]domain.Bank, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil) req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/banks", nil)
if err != nil { if err != nil {
@ -223,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) { 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" endpoint := c.baseURL + "/transfers"
fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint) fmt.Printf("\n\nChapa withdrawal URL is %v\n\n", endpoint)
@ -240,7 +281,9 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
return false, fmt.Errorf("failed to create request: %w", err) 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) resp, err := c.httpClient.Do(httpReq)
if err != nil { if err != nil {
@ -249,7 +292,8 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { 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 var response domain.ChapaWithdrawalResponse
@ -257,7 +301,7 @@ func (c *Client) InitiateTransfer(ctx context.Context, req domain.ChapaWithdrawa
return false, fmt.Errorf("failed to decode response: %w", err) 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) { func (c *Client) VerifyTransfer(ctx context.Context, reference string) (*domain.ChapaVerificationResponse, error) {

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -60,7 +61,9 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
var senderWallet domain.Wallet var senderWallet domain.Wallet
// Generate unique reference // Generate unique reference
reference := uuid.New().String() // reference := uuid.New().String()
reference := fmt.Sprintf("chapa-deposit-%d-%s", userID, uuid.New().String())
senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID) senderWallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get sender wallets: %w", err) return "", fmt.Errorf("failed to get sender wallets: %w", err)
@ -92,8 +95,7 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
Verified: false, Verified: false,
} }
// Initialize payment with Chapa payload := domain.ChapaDepositRequest{
response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{
Amount: amount, Amount: amount,
Currency: "ETB", Currency: "ETB",
Email: user.Email, Email: user.Email,
@ -102,7 +104,12 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
TxRef: reference, TxRef: reference,
CallbackURL: s.cfg.CHAPA_CALLBACK_URL, CallbackURL: s.cfg.CHAPA_CALLBACK_URL,
ReturnURL: s.cfg.CHAPA_RETURN_URL, ReturnURL: s.cfg.CHAPA_RETURN_URL,
}) }
// Initialize payment with Chapa
response, err := s.chapaClient.InitializePayment(ctx, payload)
fmt.Printf("\n\nChapa payload is: %+v\n\n", payload)
if err != nil { if err != nil {
// Update payment status to failed // Update payment status to failed
@ -110,10 +117,14 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
return "", fmt.Errorf("failed to initialize payment: %w", err) return "", fmt.Errorf("failed to initialize payment: %w", err)
} }
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { tempTransfer, err := s.transferStore.CreateTransfer(ctx, transfer)
if err != nil {
return "", fmt.Errorf("failed to save payment: %w", err) return "", fmt.Errorf("failed to save payment: %w", err)
} }
fmt.Printf("\n\nTemp transfer is: %v\n\n", tempTransfer)
return response.CheckoutURL, nil return response.CheckoutURL, nil
} }
@ -185,12 +196,16 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
} }
success, err := s.chapaClient.InitiateTransfer(ctx, transferReq) success, err := s.chapaClient.InitiateTransfer(ctx, transferReq)
if err != nil || !success { if err != nil {
// Update withdrawal status to failed
_ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed)) _ = s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusFailed))
return nil, fmt.Errorf("failed to initiate transfer: %w", err) 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 // Update withdrawal status to processing
if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil { if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.WithdrawalStatusProcessing)); err != nil {
return nil, fmt.Errorf("failed to update withdrawal status: %w", err) return nil, fmt.Errorf("failed to update withdrawal status: %w", err)
@ -212,50 +227,68 @@ func (s *Service) GetSupportedBanks(ctx context.Context) ([]domain.Bank, error)
return banks, nil return banks, nil
} }
func (s *Service) ManualVerifTransaction(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) { func (s *Service) ManuallyVerify(ctx context.Context, txRef string) (*domain.ChapaVerificationResponse, error) {
// First check if we already have a verified record // Lookup transfer by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, txRef) 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{ return &domain.ChapaVerificationResponse{
Status: string(domain.PaymentStatusCompleted), Status: string(domain.PaymentStatusCompleted),
Amount: float64(transfer.Amount) / 100, // Convert from cents/kobo Amount: float64(transfer.Amount) / 100,
Currency: "ETB", Currency: "ETB",
}, nil }, nil
} }
fmt.Printf("\n\nSender wallet ID is:%v\n\n", transfer.SenderWalletID.Value) // Validate sender wallet
fmt.Printf("\n\nTransfer is:%v\n\n", transfer)
// just making sure that the sender id is valid
if !transfer.SenderWalletID.Valid { if !transfer.SenderWalletID.Valid {
return nil, fmt.Errorf("sender wallet id is invalid: %v \n", transfer.SenderWalletID) return nil, fmt.Errorf("invalid sender wallet ID: %v", transfer.SenderWalletID)
} }
// If not verified or not found, verify with Chapa var verification *domain.ChapaVerificationResponse
verification, err := s.chapaClient.VerifyPayment(ctx, txRef)
if err != nil {
return nil, fmt.Errorf("failed to verify payment: %w", err)
}
// Update our records if payment is successful // Decide verification method based on type
if verification.Status == domain.PaymentStatusCompleted { switch strings.ToLower(string(transfer.Type)) {
err = s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true) case "deposit":
// Use Chapa Payment Verification
verification, err = s.chapaClient.ManualVerifyPayment(ctx, txRef)
if err != nil { 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 if verification.Status == string(domain.PaymentStatusSuccessful) {
err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, transfer.Amount) // Mark verified
if err != nil { if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err) 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{ return verification, nil
Status: string(verification.Status),
Amount: float64(verification.Amount),
Currency: verification.Currency,
}, nil
} }
func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error { func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domain.ChapaWebHookTransfer) error {
@ -281,12 +314,13 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
// verified = true // 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 payment is completed, credit user's wallet
if transfer.Status == string(domain.PaymentStatusCompleted) { 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{ if _, err := s.walletStore.AddToWallet(ctx, payment.SenderWalletID.Value, payment.Amount, domain.ValidInt64{}, domain.TRANSFER_CHAPA, domain.PaymentDetails{
ReferenceNumber: domain.ValidString{ ReferenceNumber: domain.ValidString{
Value: transfer.Reference, Value: transfer.Reference,
@ -322,12 +356,11 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
// verified = true // verified = true
// } // }
if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { if payment.Status == string(domain.PaymentStatusSuccessful) {
return fmt.Errorf("failed to update payment status: %w", err) 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
// If payment is completed, credit user's wallet } else {
if payment.Status == string(domain.PaymentStatusFailed) {
if _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil { 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) return fmt.Errorf("failed to credit user wallet: %w", err)
} }

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

@ -8,6 +8,8 @@ import (
) )
type NotificationStore interface { 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 SendNotification(ctx context.Context, notification *domain.Notification) error
MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error
ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error)

View File

@ -12,6 +12,8 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
afro "github.com/amanuelabay/afrosms-go" afro "github.com/amanuelabay/afrosms-go"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@ -19,20 +21,21 @@ import (
) )
type Service struct { type Service struct {
repo repository.NotificationRepository repo repository.NotificationRepository
Hub *ws.NotificationHub Hub *ws.NotificationHub
connections sync.Map notificationStore NotificationStore
notificationCh chan *domain.Notification connections sync.Map
stopCh chan struct{} notificationCh chan *domain.Notification
config *config.Config stopCh chan struct{}
logger *slog.Logger config *config.Config
redisClient *redis.Client logger *slog.Logger
redisClient *redis.Client
} }
func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service { func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service {
hub := ws.NewNotificationHub() hub := ws.NewNotificationHub()
rdb := redis.NewClient(&redis.Options{ rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr, // e.g., “redis:6379” Addr: cfg.RedisAddr, // e.g., "redis:6379"
}) })
svc := &Service{ svc := &Service{
@ -264,7 +267,8 @@ func (s *Service) retryFailedNotifications() {
go func(notification *domain.Notification) { go func(notification *domain.Notification) {
for attempt := 0; attempt < 3; attempt++ { for attempt := 0; attempt < 3; attempt++ {
time.Sleep(time.Duration(attempt) * time.Second) 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 { if err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil { if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
@ -273,7 +277,7 @@ func (s *Service) retryFailedNotifications() {
s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID) s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID)
return 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 { if err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil { if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
@ -303,52 +307,60 @@ func (s *Service) RunRedisSubscriber(ctx context.Context) {
ch := pubsub.Channel() ch := pubsub.Channel()
for msg := range ch { for msg := range ch {
var payload domain.LiveMetric var parsed map[string]interface{}
if err := json.Unmarshal([]byte(msg.Payload), &payload); err != nil { if err := json.Unmarshal([]byte(msg.Payload), &parsed); err != nil {
s.logger.Error("[NotificationSvc.runRedisSubscriber] failed unmarshal metric", "error", err) s.logger.Error("invalid Redis message format", "payload", msg.Payload, "error", err)
continue continue
} }
// Broadcast via WebSocket Hub
s.Hub.Broadcast <- map[string]interface{}{ eventType, _ := parsed["type"].(string)
"type": "LIVE_METRIC_UPDATE", payload := parsed["payload"]
recipientID, hasRecipient := parsed["recipient_id"]
recipientType, _ := parsed["recipient_type"].(string)
message := map[string]interface{}{
"type": eventType,
"payload": payload, "payload": payload,
} }
if hasRecipient {
message["recipient_id"] = recipientID
message["recipient_type"] = recipientType
}
s.Hub.Broadcast <- message
} }
} }
func (s *Service) UpdateLiveMetrics(ctx context.Context, updates domain.MetricUpdates) error { func (s *Service) UpdateLiveWalletMetrics(ctx context.Context, companies []domain.GetCompany, branches []domain.BranchWallet) error {
const key = "live_metrics" const key = "live_metrics"
val, err := s.redisClient.Get(ctx, key).Result() companyBalances := make([]domain.CompanyWalletBalance, 0, len(companies))
var metric domain.LiveMetric for _, c := range companies {
if err == redis.Nil { companyBalances = append(companyBalances, domain.CompanyWalletBalance{
metric = domain.LiveMetric{} CompanyID: c.ID,
} else if err != nil { CompanyName: c.Name,
return err Balance: float64(c.WalletBalance.Float32()),
} else { })
if err := json.Unmarshal([]byte(val), &metric); err != nil {
return err
}
} }
// Apply increments if provided branchBalances := make([]domain.BranchWalletBalance, 0, len(branches))
if updates.TotalCashSportsbookDelta != nil { for _, b := range branches {
metric.TotalCashSportsbook += *updates.TotalCashSportsbookDelta branchBalances = append(branchBalances, domain.BranchWalletBalance{
} BranchID: b.ID,
if updates.TotalCashSportGamesDelta != nil { BranchName: b.Name,
metric.TotalCashSportGames += *updates.TotalCashSportGamesDelta CompanyID: b.CompanyID,
} Balance: float64(b.Balance.Float32()),
if updates.TotalLiveTicketsDelta != nil { })
metric.TotalLiveTickets += *updates.TotalLiveTicketsDelta
}
if updates.TotalUnsettledCashDelta != nil {
metric.TotalUnsettledCash += *updates.TotalUnsettledCashDelta
}
if updates.TotalGamesDelta != nil {
metric.TotalGames += *updates.TotalGamesDelta
} }
updatedData, err := json.Marshal(metric) payload := domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: companyBalances,
BranchBalances: branchBalances,
}
updatedData, err := json.Marshal(payload)
if err != nil { if err != nil {
return err return err
} }
@ -357,11 +369,9 @@ func (s *Service) UpdateLiveMetrics(ctx context.Context, updates domain.MetricUp
return err return err
} }
if err := s.redisClient.Publish(ctx, "live_metrics", updatedData).Err(); err != nil { if err := s.redisClient.Publish(ctx, key, updatedData).Err(); err != nil {
return err return err
} }
s.logger.Info("[NotificationSvc.UpdateLiveMetrics] Live metrics updated and broadcasted")
return nil return nil
} }
@ -383,3 +393,83 @@ func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error)
return metric, nil 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

@ -476,6 +476,7 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
defer writer.Flush() defer writer.Flush()
// Summary section // 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", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"})
writer.Write([]string{ writer.Write([]string{
period, period,
@ -491,6 +492,7 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
writer.Write([]string{}) // Empty line for spacing writer.Write([]string{}) // Empty line for spacing
// Virtual Game Summary section // Virtual Game Summary section
writer.Write([]string{"Virtual Game Reports (Periodic)"})
writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"}) writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"})
for _, row := range data.VirtualGameStats { for _, row := range data.VirtualGameStats {
writer.Write([]string{ writer.Write([]string{
@ -500,18 +502,66 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
}) })
} }
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 return nil
} }
func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) { func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) {
from, to := getTimeRange(period) from, to := getTimeRange(period)
companyID := int64(0) // companyID := int64(0)
// Basic metrics // Basic metrics
totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to, companyID) totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to)
cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to, companyID) cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to)
cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to, companyID) cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to)
cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to, companyID) cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to)
// Wallet Transactions // Wallet Transactions
transactions, _ := s.repo.GetWalletTransactionsInRange(ctx, from, to) transactions, _ := s.repo.GetWalletTransactionsInRange(ctx, from, to)
@ -555,6 +605,113 @@ func (s *Service) fetchReportData(ctx context.Context, period string) (domain.Re
}) })
} }
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{ return domain.ReportData{
TotalBets: totalBets, TotalBets: totalBets,
TotalCashIn: cashIn, TotalCashIn: cashIn,
@ -564,6 +721,8 @@ func (s *Service) fetchReportData(ctx context.Context, period string) (domain.Re
Withdrawals: totalWithdrawals, Withdrawals: totalWithdrawals,
TotalTickets: totalTickets.TotalTickets, TotalTickets: totalTickets.TotalTickets,
VirtualGameStats: virtualGameStatsDomain, VirtualGameStats: virtualGameStatsDomain,
CompanyReports: companyReports,
BranchReports: branchReports,
}, nil }, nil
} }
@ -595,8 +754,6 @@ func getTimeRange(period string) (time.Time, time.Time) {
} }
} }
// func (s *Service) GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) { // func (s *Service) GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) {
// // Get company bet activity // // Get company bet activity
// companyBets, err := s.betStore.GetCompanyBetActivity(ctx, filter) // companyBets, err := s.betStore.GetCompanyBetActivity(ctx, filter)

View File

@ -236,13 +236,13 @@ func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq,
return domain.Ticket{}, rows, err return domain.Ticket{}, rows, err
} }
updates := domain.MetricUpdates{ // updates := domain.MetricUpdates{
TotalLiveTicketsDelta: domain.PtrInt64(1), // TotalLiveTicketsDelta: domain.PtrInt64(1),
} // }
if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil { // if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil {
// handle error // // handle error
} // }
return ticket, rows, nil return ticket, rows, nil
} }

View File

@ -13,8 +13,13 @@ type VirtualGameService interface {
GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error)
ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error)
ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, 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) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error)
RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, 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

@ -52,6 +52,7 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
sessionId := 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( token, err := jwtutil.CreatePopOKJwt(
userID, userID,
user.CompanyID,
user.FirstName, user.FirstName,
currency, currency,
"en", "en",
@ -69,6 +70,8 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
tx := &domain.VirtualGameHistory{ tx := &domain.VirtualGameHistory{
SessionID: sessionId, // Optional: populate if session tracking is implemented SessionID: sessionId, // Optional: populate if session tracking is implemented
UserID: userID, UserID: userID,
CompanyID: user.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: toInt64Ptr(gameID), GameID: toInt64Ptr(gameID),
TransactionType: "LAUNCH", TransactionType: "LAUNCH",
Amount: 0, Amount: 0,
@ -211,8 +214,11 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
// Create transaction record // Create transaction record
tx := &domain.VirtualGameTransaction{ tx := &domain.VirtualGameTransaction{
UserID: claims.UserID, UserID: claims.UserID,
CompanyID: claims.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: req.GameID,
TransactionType: "BET", TransactionType: "BET",
Amount: -amountCents, // Negative for bets Amount: amountCents, // Negative for bets
Currency: req.Currency, Currency: req.Currency,
ExternalTransactionID: req.TransactionID, ExternalTransactionID: req.TransactionID,
Status: "COMPLETED", Status: "COMPLETED",
@ -279,6 +285,9 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
// 5. Create transaction record // 5. Create transaction record
tx := &domain.VirtualGameTransaction{ tx := &domain.VirtualGameTransaction{
UserID: claims.UserID, UserID: claims.UserID,
CompanyID: claims.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: req.GameID,
TransactionType: "WIN", TransactionType: "WIN",
Amount: amountCents, Amount: amountCents,
Currency: req.Currency, Currency: req.Currency,
@ -299,6 +308,167 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
}, nil }, nil
} }
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 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) { func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) {
// 1. Validate token and get user ID // 1. Validate token and get user ID
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
@ -480,7 +650,7 @@ func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopO
func (s *service) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) { func (s *service) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) {
// Fetch all available games // Fetch all available games
games, err := s.ListGames(ctx, "ETB") // currency can be dynamic games, err := s.ListGames(ctx, "ETB")
if err != nil || len(games) == 0 { if err != nil || len(games) == 0 {
return nil, fmt.Errorf("could not fetch games") return nil, fmt.Errorf("could not fetch games")
} }
@ -544,3 +714,48 @@ func toInt64Ptr(s string) *int64 {
} }
return &id 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

@ -7,6 +7,8 @@ import (
) )
type WalletStore interface { 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) CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error)
CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error) CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error)
GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error) GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error)

View File

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

View File

@ -69,7 +69,18 @@ func (s *Service) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWalle
} }
func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error { func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error {
return s.walletStore.UpdateBalance(ctx, id, balance) err := s.walletStore.UpdateBalance(ctx, id, balance)
if err != nil {
return err
}
wallet, err := s.GetWalletByID(ctx, id)
if err != nil {
return err
}
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
return nil
} }
func (s *Service) AddToWallet( func (s *Service) AddToWallet(
@ -84,6 +95,8 @@ func (s *Service) AddToWallet(
return domain.Transfer{}, err return domain.Transfer{}, err
} }
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
// Log the transfer here for reference // Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
Amount: amount, Amount: amount,
@ -121,6 +134,8 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
return domain.Transfer{}, nil return domain.Transfer{}, nil
} }
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
// Log the transfer here for reference // Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
Amount: amount, Amount: amount,

View File

@ -12,6 +12,8 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "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" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
@ -37,6 +39,8 @@ import (
) )
type App struct { type App struct {
issueReportingSvc *issuereporting.Service
instSvc *institutions.Service
currSvc *currency.Service currSvc *currency.Service
fiber *fiber.App fiber *fiber.App
aleaVirtualGameService alea.AleaVirtualGameService aleaVirtualGameService alea.AleaVirtualGameService
@ -70,6 +74,8 @@ type App struct {
} }
func NewApp( func NewApp(
issueReportingSvc *issuereporting.Service,
instSvc *institutions.Service,
currSvc *currency.Service, currSvc *currency.Service,
port int, validator *customvalidator.CustomValidator, port int, validator *customvalidator.CustomValidator,
settingSvc *settings.Service, settingSvc *settings.Service,
@ -113,6 +119,8 @@ func NewApp(
})) }))
s := &App{ s := &App{
issueReportingSvc: issueReportingSvc,
instSvc: instSvc,
currSvc: currSvc, currSvc: currSvc,
fiber: app, fiber: app,
port: port, port: port,

View File

@ -22,7 +22,7 @@ import (
// @Success 200 {object} domain.BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [post] // @Router /sport/bet [post]
func (h *Handler) CreateBet(c *fiber.Ctx) error { func (h *Handler) CreateBet(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
@ -82,7 +82,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
// @Success 200 {object} domain.BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /random/bet [post] // @Router /sport/random/bet [post]
func (h *Handler) RandomBet(c *fiber.Ctx) error { func (h *Handler) RandomBet(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
@ -207,7 +207,7 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
// @Success 200 {array} domain.BetRes // @Success 200 {array} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [get] // @Router /sport/bet [get]
func (h *Handler) GetAllBet(c *fiber.Ctx) error { func (h *Handler) GetAllBet(c *fiber.Ctx) error {
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
companyID := c.Locals("company_id").(domain.ValidInt64) companyID := c.Locals("company_id").(domain.ValidInt64)
@ -305,7 +305,7 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
// @Success 200 {object} domain.BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [get] // @Router /sport/bet/{id} [get]
func (h *Handler) GetBetByID(c *fiber.Ctx) error { func (h *Handler) GetBetByID(c *fiber.Ctx) error {
betID := c.Params("id") betID := c.Params("id")
id, err := strconv.ParseInt(betID, 10, 64) id, err := strconv.ParseInt(betID, 10, 64)
@ -351,7 +351,7 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
// @Success 200 {object} domain.BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/cashout/{id} [get] // @Router /sport/bet/cashout/{id} [get]
func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error { func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error {
cashoutID := c.Params("id") cashoutID := c.Params("id")
@ -392,7 +392,7 @@ type UpdateCashOutReq struct {
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [patch] // @Router /sport/bet/{id} [patch]
func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { func (h *Handler) UpdateCashOut(c *fiber.Ctx) error {
type UpdateCashOutReq struct { type UpdateCashOutReq struct {
CashedOut bool `json:"cashed_out" validate:"required" example:"true"` CashedOut bool `json:"cashed_out" validate:"required" example:"true"`
@ -455,7 +455,7 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error {
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [delete] // @Router /sport/bet/{id} [delete]
func (h *Handler) DeleteBet(c *fiber.Ctx) error { func (h *Handler) DeleteBet(c *fiber.Ctx) error {
betID := c.Params("id") betID := c.Params("id")
id, err := strconv.ParseInt(betID, 10, 64) id, err := strconv.ParseInt(betID, 10, 64)

View File

@ -32,7 +32,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
var req domain.ChapaDepositRequestPayload var req domain.ChapaDepositRequestPayload
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
fmt.Sprintln("We first first are here init Chapa payment") // fmt.Println("We first first are here init Chapa payment")
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Error: err.Error(), Error: err.Error(),
Message: "Failed to parse request body", Message: "Failed to parse request body",
@ -41,7 +41,7 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error {
amount := domain.Currency(req.Amount * 100) amount := domain.Currency(req.Amount * 100)
fmt.Sprintln("We are here init Chapa payment") fmt.Println("We are here init Chapa payment")
checkoutURL, err := h.chapaSvc.InitiateDeposit(c.Context(), userID, amount) checkoutURL, err := h.chapaSvc.InitiateDeposit(c.Context(), userID, amount)
if err != nil { if err != nil {
@ -79,7 +79,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
} }
switch chapaTransactionType.Type { switch chapaTransactionType.Type {
case h.Cfg.CHAPA_TRANSFER_TYPE: case h.Cfg.CHAPA_PAYMENT_TYPE:
chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer) chapaTransferVerificationRequest := new(domain.ChapaWebHookTransfer)
if err := c.BodyParser(chapaTransferVerificationRequest); err != nil { if err := c.BodyParser(chapaTransferVerificationRequest); err != nil {
@ -100,7 +100,7 @@ func (h *Handler) WebhookCallback(c *fiber.Ctx) error {
Data: chapaTransferVerificationRequest, Data: chapaTransferVerificationRequest,
Success: true, Success: true,
}) })
case h.Cfg.CHAPA_PAYMENT_TYPE: case h.Cfg.CHAPA_TRANSFER_TYPE:
chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment) chapaPaymentVerificationRequest := new(domain.ChapaWebHookPayment)
if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil { if err := c.BodyParser(chapaPaymentVerificationRequest); err != nil {
return domain.UnProcessableEntityResponse(c) return domain.UnProcessableEntityResponse(c)
@ -147,7 +147,7 @@ func (h *Handler) ManualVerifyTransaction(c *fiber.Ctx) error {
}) })
} }
verification, err := h.chapaSvc.ManualVerifTransaction(c.Context(), txRef) verification, err := h.chapaSvc.ManuallyVerify(c.Context(), txRef)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to verify Chapa transaction", Message: "Failed to verify Chapa transaction",

View File

@ -11,6 +11,8 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "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" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
@ -31,6 +33,8 @@ import (
) )
type Handler struct { type Handler struct {
issueReportingSvc *issuereporting.Service
instSvc *institutions.Service
currSvc *currency.Service currSvc *currency.Service
logger *slog.Logger logger *slog.Logger
settingSvc *settings.Service settingSvc *settings.Service
@ -61,6 +65,8 @@ type Handler struct {
} }
func New( func New(
issueReportingSvc *issuereporting.Service,
instSvc *institutions.Service,
currSvc *currency.Service, currSvc *currency.Service,
logger *slog.Logger, logger *slog.Logger,
settingSvc *settings.Service, settingSvc *settings.Service,
@ -90,6 +96,8 @@ func New(
mongoLoggerSvc *zap.Logger, mongoLoggerSvc *zap.Logger,
) *Handler { ) *Handler {
return &Handler{ return &Handler{
issueReportingSvc: issueReportingSvc,
instSvc: instSvc,
currSvc: currSvc, currSvc: currSvc,
logger: logger, logger: logger,
settingSvc: settingSvc, settingSvc: settingSvc,

View File

@ -0,0 +1,135 @@
package handlers
import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// @Summary Create a new bank
// @Tags Institutions - Banks
// @Accept json
// @Produce json
// @Param bank body domain.Bank true "Bank Info"
// @Success 201 {object} domain.Bank
// @Failure 400 {object} domain.ErrorResponse
// @Router /api/v1/banks [post]
func (h *Handler) CreateBank(c *fiber.Ctx) error {
var bank domain.Bank
if err := c.BodyParser(&bank); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
}
err := h.instSvc.Create(c.Context(), &bank)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(bank)
}
// @Summary Get a bank by ID
// @Tags Institutions - Banks
// @Produce json
// @Param id path int true "Bank ID"
// @Success 200 {object} domain.Bank
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/banks/{id} [get]
func (h *Handler) GetBankByID(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"})
}
bank, err := h.instSvc.GetByID(c.Context(), int64(id))
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bank not found"})
}
return c.JSON(bank)
}
// @Summary Update a bank
// @Tags Institutions - Banks
// @Accept json
// @Produce json
// @Param id path int true "Bank ID"
// @Param bank body domain.Bank true "Bank Info"
// @Success 200 {object} domain.Bank
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/banks/{id} [put]
func (h *Handler) UpdateBank(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"})
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to update bank",
Error: err.Error(),
})
}
var bank domain.Bank
if err := c.BodyParser(&bank); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to update bank",
Error: err.Error(),
})
}
bank.ID = id
err = h.instSvc.Update(c.Context(), &bank)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update bank",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Bank updated successfully",
StatusCode: fiber.StatusOK,
Success: true,
Data: bank,
})
// return c.JSON(bank)
}
// @Summary Delete a bank
// @Tags Institutions - Banks
// @Produce json
// @Param id path int true "Bank ID"
// @Success 204 {string} string "Deleted successfully"
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/banks/{id} [delete]
func (h *Handler) DeleteBank(c *fiber.Ctx) error {
id, err := c.ParamsInt("id")
if err != nil || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid bank ID"})
}
err = h.instSvc.Delete(c.Context(), int64(id))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.SendStatus(fiber.StatusNoContent)
}
// @Summary List all banks
// @Tags Institutions - Banks
// @Produce json
// @Success 200 {array} domain.Bank
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/banks [get]
func (h *Handler) ListBanks(c *fiber.Ctx) error {
banks, err := h.instSvc.List(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(banks)
}

View File

@ -0,0 +1,147 @@
package handlers
import (
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// CreateIssue godoc
// @Summary Report an issue
// @Description Allows a customer to report a new issue related to the betting platform
// @Tags Issues
// @Accept json
// @Produce json
// @Param issue body domain.ReportedIssue true "Issue to report"
// @Success 201 {object} domain.ReportedIssue
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues [post]
func (h *Handler) CreateIssue(c *fiber.Ctx) error {
var req domain.ReportedIssue
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
created, err := h.issueReportingSvc.CreateReportedIssue(c.Context(), req)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.Status(fiber.StatusCreated).JSON(created)
}
// GetCustomerIssues godoc
// @Summary Get reported issues by a customer
// @Description Returns all issues reported by a specific customer
// @Tags Issues
// @Produce json
// @Param customer_id path int true "Customer ID"
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {array} domain.ReportedIssue
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues/customer/{customer_id} [get]
func (h *Handler) GetCustomerIssues(c *fiber.Ctx) error {
customerID, err := strconv.ParseInt(c.Params("customer_id"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid customer ID")
}
limit, offset := getPaginationParams(c)
issues, err := h.issueReportingSvc.GetIssuesForCustomer(c.Context(), customerID, limit, offset)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(issues)
}
// GetAllIssues godoc
// @Summary Get all reported issues
// @Description Admin endpoint to list all reported issues with pagination
// @Tags Issues
// @Produce json
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {array} domain.ReportedIssue
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues [get]
func (h *Handler) GetAllIssues(c *fiber.Ctx) error {
limit, offset := getPaginationParams(c)
issues, err := h.issueReportingSvc.GetAllIssues(c.Context(), limit, offset)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(issues)
}
// UpdateIssueStatus godoc
// @Summary Update issue status
// @Description Admin endpoint to update the status of a reported issue
// @Tags Issues
// @Accept json
// @Param issue_id path int true "Issue ID"
// @Param status body object{status=string} true "New issue status (pending, in_progress, resolved, rejected)"
// @Success 204
// @Failure 400 {object} domain.ErrorResponse
// @Router /api/v1/issues/{issue_id}/status [patch]
func (h *Handler) UpdateIssueStatus(c *fiber.Ctx) error {
issueID, err := strconv.ParseInt(c.Params("issue_id"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid issue ID")
}
var body struct {
Status string `json:"status"`
}
if err := c.BodyParser(&body); err != nil || body.Status == "" {
return fiber.NewError(fiber.StatusBadRequest, "Invalid status payload")
}
if err := h.issueReportingSvc.UpdateIssueStatus(c.Context(), issueID, body.Status); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)
}
// DeleteIssue godoc
// @Summary Delete a reported issue
// @Description Admin endpoint to delete a reported issue
// @Tags Issues
// @Param issue_id path int true "Issue ID"
// @Success 204
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues/{issue_id} [delete]
func (h *Handler) DeleteIssue(c *fiber.Ctx) error {
issueID, err := strconv.ParseInt(c.Params("issue_id"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid issue ID")
}
if err := h.issueReportingSvc.DeleteIssue(c.Context(), issueID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)
}
func getPaginationParams(c *fiber.Ctx) (limit, offset int) {
limit = 20
offset = 0
if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 {
limit = l
}
if o, err := strconv.Atoi(c.Query("offset")); err == nil && o >= 0 {
offset = o
}
return
}

View File

@ -1,9 +1,5 @@
package handlers package handlers
import (
"github.com/gofiber/fiber/v2"
)
// @Summary Get virtual game recommendations // @Summary Get virtual game recommendations
// @Description Returns a list of recommended virtual games for a specific user // @Description Returns a list of recommended virtual games for a specific user
// @Tags Recommendations // @Tags Recommendations
@ -13,14 +9,15 @@ import (
// @Success 200 {object} domain.RecommendationSuccessfulResponse "Recommended games fetched successfully" // @Success 200 {object} domain.RecommendationSuccessfulResponse "Recommended games fetched successfully"
// @Failure 500 {object} domain.RecommendationErrorResponse "Failed to fetch recommendations" // @Failure 500 {object} domain.RecommendationErrorResponse "Failed to fetch recommendations"
// @Router /api/v1/virtual-games/recommendations/{userID} [get] // @Router /api/v1/virtual-games/recommendations/{userID} [get]
func (h *Handler) GetRecommendations(c *fiber.Ctx) error {
userID := c.Params("userID") // or from JWT // func (h *Handler) GetRecommendations(c *fiber.Ctx) error {
recommendations, err := h.recommendationSvc.GetRecommendations(c.Context(), userID) // userID := c.Params("userID") // or from JWT
if err != nil { // recommendations, err := h.recommendationSvc.GetRecommendations(c.Context(), userID)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recommendations") // if err != nil {
} // return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recommendations")
return c.JSON(fiber.Map{ // }
"message": "Recommended games fetched successfully", // return c.JSON(fiber.Map{
"recommended_games": recommendations, // "message": "Recommended games fetched successfully",
}) // "recommended_games": recommendations,
} // })
// }

View File

@ -11,17 +11,19 @@ import (
) )
type TransferWalletRes struct { type TransferWalletRes struct {
ID int64 `json:"id" example:"1"` ID int64 `json:"id"`
Amount float32 `json:"amount" example:"100.0"` Amount float32 `json:"amount"`
Verified bool `json:"verified" example:"true"` Verified bool `json:"verified"`
Type string `json:"type" example:"transfer"` Type string `json:"type"`
PaymentMethod string `json:"payment_method" example:"bank"` PaymentMethod string `json:"payment_method"`
ReceiverWalletID *int64 `json:"receiver_wallet_id" example:"1"` ReceiverWalletID *int64 `json:"receiver_wallet_id,omitempty"`
SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` SenderWalletID *int64 `json:"sender_wallet_id,omitempty"`
CashierID *int64 `json:"cashier_id" example:"789"` CashierID *int64 `json:"cashier_id,omitempty"`
CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` ReferenceNumber string `json:"reference_number"` // ← Add this
UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
type RefillRes struct { type RefillRes struct {
ID int64 `json:"id" example:"1"` ID int64 `json:"id" example:"1"`
Amount float32 `json:"amount" example:"100.0"` Amount float32 `json:"amount" example:"100.0"`
@ -35,33 +37,34 @@ type RefillRes struct {
UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"`
} }
func convertTransfer(transfer domain.Transfer) TransferWalletRes { func convertTransfer(t domain.Transfer) TransferWalletRes {
var receiverID *int64
if t.ReceiverWalletID.Valid {
receiverID = &t.ReceiverWalletID.Value
}
var senderID *int64
if t.SenderWalletID.Valid {
senderID = &t.SenderWalletID.Value
}
var cashierID *int64 var cashierID *int64
if transfer.CashierID.Valid { if t.CashierID.Valid {
cashierID = &transfer.CashierID.Value cashierID = &t.CashierID.Value
}
var receiverID *int64
if transfer.ReceiverWalletID.Valid {
receiverID = &transfer.ReceiverWalletID.Value
}
var senderId *int64
if transfer.SenderWalletID.Valid {
senderId = &transfer.SenderWalletID.Value
} }
return TransferWalletRes{ return TransferWalletRes{
ID: transfer.ID, ID: t.ID,
Amount: transfer.Amount.Float32(), Amount: float32(t.Amount),
Verified: transfer.Verified, Verified: t.Verified,
Type: string(transfer.Type), Type: string(t.Type),
PaymentMethod: string(transfer.PaymentMethod), PaymentMethod: string(t.PaymentMethod),
ReceiverWalletID: receiverID, ReceiverWalletID: receiverID,
SenderWalletID: senderId, SenderWalletID: senderID,
CashierID: cashierID, CashierID: cashierID,
CreatedAt: transfer.CreatedAt, ReferenceNumber: t.ReferenceNumber,
UpdatedAt: transfer.UpdatedAt, CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
} }
} }
@ -142,10 +145,11 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error {
var senderID int64 var senderID int64
//TODO: check to make sure that the cashiers aren't transferring TO branch wallet //TODO: check to make sure that the cashiers aren't transferring TO branch wallet
if role == domain.RoleCustomer { switch role {
case domain.RoleCustomer:
h.logger.Error("Unauthorized access", "userID", userID, "role", role) h.logger.Error("Unauthorized access", "userID", userID, "role", role)
return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil)
} else if role == domain.RoleBranchManager || role == domain.RoleAdmin || role == domain.RoleSuperAdmin { case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin:
company, err := h.companySvc.GetCompanyByID(c.Context(), companyID.Value) company, err := h.companySvc.GetCompanyByID(c.Context(), companyID.Value)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
@ -156,7 +160,7 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error {
} }
senderID = company.WalletID senderID = company.WalletID
h.logger.Error("Will", "userID", userID, "role", role) h.logger.Error("Will", "userID", userID, "role", role)
} else { default:
cashierBranch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID) cashierBranch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID)
if err != nil { if err != nil {
h.logger.Error("Failed to get branch", "user ID", userID, "error", err) h.logger.Error("Failed to get branch", "user ID", userID, "error", err)

View File

@ -1,6 +1,8 @@
package handlers package handlers
import ( import (
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -25,9 +27,9 @@ type launchVirtualGameRes struct {
// @Security Bearer // @Security Bearer
// @Param launch body launchVirtualGameReq true "Game launch details" // @Param launch body launchVirtualGameReq true "Game launch details"
// @Success 200 {object} launchVirtualGameRes // @Success 200 {object} launchVirtualGameRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} domain.ErrorResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /virtual-game/launch [post] // @Router /virtual-game/launch [post]
func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error { func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error {
@ -37,6 +39,12 @@ func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
} }
// companyID, ok := c.Locals("company_id").(int64)
// if !ok || companyID == 0 {
// h.logger.Error("Invalid company ID in context")
// return fiber.NewError(fiber.StatusUnauthorized, "Invalid company identification")
// }
var req launchVirtualGameReq var req launchVirtualGameReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse LaunchVirtualGame request", "error", err) h.logger.Error("Failed to parse LaunchVirtualGame request", "error", err)
@ -64,9 +72,9 @@ func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param callback body domain.PopOKCallback true "Callback data" // @Param callback body domain.PopOKCallback true "Callback data"
// @Success 200 {object} response.APIResponse // @Success 200 {object} domain.ErrorResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /virtual-game/callback [post] // @Router /virtual-game/callback [post]
func (h *Handler) HandleVirtualGameCallback(c *fiber.Ctx) error { func (h *Handler) HandleVirtualGameCallback(c *fiber.Ctx) error {
var callback domain.PopOKCallback var callback domain.PopOKCallback
@ -199,3 +207,119 @@ func (h *Handler) RecommendGames(c *fiber.Ctx) error {
return c.JSON(recommendations) return c.JSON(recommendations)
} }
func (h *Handler) HandleTournamentWin(c *fiber.Ctx) error {
var req domain.PopOKWinRequest
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Invalid tournament win request body", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request body",
})
}
resp, err := h.virtualGameSvc.ProcessTournamentWin(c.Context(), &req)
if err != nil {
h.logger.Error("Failed to process tournament win", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.JSON(resp)
}
func (h *Handler) HandlePromoWin(c *fiber.Ctx) error {
var req domain.PopOKWinRequest
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Invalid promo win request body", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request body",
})
}
resp, err := h.virtualGameSvc.ProcessPromoWin(c.Context(), &req)
if err != nil {
h.logger.Error("Failed to process promo win", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.JSON(resp)
}
// AddFavoriteGame godoc
// @Summary Add game to favorites
// @Description Adds a game to the user's favorite games list
// @Tags VirtualGames - Favourites
// @Accept json
// @Produce json
// @Param body body domain.FavoriteGameRequest true "Game ID to add"
// @Success 201 {string} domain.Response "created"
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/virtual-game/favorites [post]
func (h *Handler) AddFavorite(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
var req domain.FavoriteGameRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request")
}
err := h.virtualGameSvc.AddFavoriteGame(c.Context(), userID, req.GameID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Could not add favorite",
Error: err.Error(),
})
// return fiber.NewError(fiber.StatusInternalServerError, "Could not add favorite")
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Game added to favorites",
StatusCode: fiber.StatusCreated,
Success: true,
})
// return c.SendStatus(fiber.StatusCreated)
}
// RemoveFavoriteGame godoc
// @Summary Remove game from favorites
// @Description Removes a game from the user's favorites
// @Tags VirtualGames - Favourites
// @Produce json
// @Param gameID path int64 true "Game ID to remove"
// @Success 200 {string} domain.Response "removed"
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/virtual-game/favorites/{gameID} [delete]
func (h *Handler) RemoveFavorite(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
gameID, _ := strconv.ParseInt(c.Params("gameID"), 10, 64)
err := h.virtualGameSvc.RemoveFavoriteGame(c.Context(), userID, gameID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not remove favorite")
}
return c.SendStatus(fiber.StatusOK)
}
// ListFavoriteGames godoc
// @Summary Get user's favorite games
// @Description Lists the games that the user marked as favorite
// @Tags VirtualGames - Favourites
// @Produce json
// @Success 200 {array} domain.GameRecommendation
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/virtual-game/favorites [get]
func (h *Handler) ListFavorites(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
games, err := h.virtualGameSvc.ListFavoriteGames(c.Context(), userID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not fetch favorites")
}
return c.Status(fiber.StatusOK).JSON(games)
}

View File

@ -24,12 +24,13 @@ type UserClaim struct {
type PopOKClaim struct { type PopOKClaim struct {
jwt.RegisteredClaims jwt.RegisteredClaims
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Username string `json:"username"` Username string `json:"username"`
Currency string `json:"currency"` Currency string `json:"currency"`
Lang string `json:"lang"` Lang string `json:"lang"`
Mode string `json:"mode"` Mode string `json:"mode"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
CompanyID domain.ValidInt64 `json:"company_id"`
} }
type JwtConfig struct { type JwtConfig struct {
@ -54,7 +55,7 @@ func CreateJwt(userId int64, Role domain.Role, CompanyID domain.ValidInt64, key
return jwtToken, err return jwtToken, err
} }
func CreatePopOKJwt(userID int64, username, currency, lang, mode, sessionID, key string, expiry time.Duration) (string, error) { func CreatePopOKJwt(userID int64, CompanyID domain.ValidInt64, username, currency, lang, mode, sessionID, key string, expiry time.Duration) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, PopOKClaim{ token := jwt.NewWithClaims(jwt.SigningMethodHS256, PopOKClaim{
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
Issuer: "fortune-bet", Issuer: "fortune-bet",
@ -69,6 +70,7 @@ func CreatePopOKJwt(userID int64, username, currency, lang, mode, sessionID, key
Lang: lang, Lang: lang,
Mode: mode, Mode: mode,
SessionID: sessionID, SessionID: sessionID,
CompanyID: CompanyID,
}) })
return token.SignedString([]byte(key)) return token.SignedString([]byte(key))
} }

View File

@ -20,6 +20,8 @@ import (
func (a *App) initAppRoutes() { func (a *App) initAppRoutes() {
h := handlers.New( h := handlers.New(
a.issueReportingSvc,
a.instSvc,
a.currSvc, a.currSvc,
a.logger, a.logger,
a.settingSvc, a.settingSvc,
@ -182,14 +184,14 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/ticket/:id", h.GetTicketByID) a.fiber.Get("/ticket/:id", h.GetTicketByID)
// Bet Routes // Bet Routes
a.fiber.Post("/bet", a.authMiddleware, h.CreateBet) a.fiber.Post("/sport/bet", a.authMiddleware, h.CreateBet)
a.fiber.Get("/bet", a.authMiddleware, h.GetAllBet) a.fiber.Get("/sport/bet", a.authMiddleware, h.GetAllBet)
a.fiber.Get("/bet/:id", h.GetBetByID) a.fiber.Get("/sport/bet/:id", h.GetBetByID)
a.fiber.Get("/bet/cashout/:id", a.authMiddleware, h.GetBetByCashoutID) a.fiber.Get("/sport/bet/cashout/:id", a.authMiddleware, h.GetBetByCashoutID)
a.fiber.Patch("/bet/:id", a.authMiddleware, h.UpdateCashOut) a.fiber.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut)
a.fiber.Delete("/bet/:id", a.authMiddleware, h.DeleteBet) a.fiber.Delete("/sport/bet/:id", a.authMiddleware, h.DeleteBet)
a.fiber.Post("/random/bet", a.authMiddleware, h.RandomBet) a.fiber.Post("/sport/random/bet", a.authMiddleware, h.RandomBet)
// Wallet // Wallet
a.fiber.Get("/wallet", h.GetAllWallets) a.fiber.Get("/wallet", h.GetAllWallets)
@ -277,9 +279,20 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/bet", h.HandleBet) a.fiber.Post("/bet", h.HandleBet)
a.fiber.Post("/win", h.HandleWin) a.fiber.Post("/win", h.HandleWin)
a.fiber.Post("/cancel", h.HandleCancel) a.fiber.Post("/cancel", h.HandleCancel)
a.fiber.Post("/promoWin ", h.HandlePromoWin)
a.fiber.Post("/tournamentWin ", h.HandleTournamentWin)
a.fiber.Get("/popok/games", h.GetGameList) a.fiber.Get("/popok/games", h.GetGameList)
a.fiber.Get("/popok/games/recommend", a.authMiddleware, h.RecommendGames) a.fiber.Get("/popok/games/recommend", a.authMiddleware, h.RecommendGames)
group.Post("/virtual-game/favorites", a.authMiddleware, h.AddFavorite)
group.Delete("/virtual-game/favorites/:gameID", a.authMiddleware, h.RemoveFavorite)
group.Get("/virtual-game/favorites", a.authMiddleware, h.ListFavorites)
//Issue Reporting Routes
group.Post("/issues", a.authMiddleware, a.OnlyAdminAndAbove, h.CreateIssue)
group.Get("/issues/customer/:customer_id", a.authMiddleware, a.OnlyAdminAndAbove, h.GetCustomerIssues)
group.Get("/issues", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAllIssues)
group.Patch("/issues/:issue_id/status", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateIssueStatus)
group.Delete("/issues/:issue_id", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteIssue)
} }
///user/profile get ///user/profile get