Merge branch 'main' into ticket-bet

This commit is contained in:
Samuel Tariku 2025-08-23 21:56:31 +03:00
commit 8ca4758917
11 changed files with 409 additions and 434 deletions

View File

@ -153,7 +153,7 @@ func main() {
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger)
veliCLient := veli.NewClient(cfg, walletSvc) veliCLient := veli.NewClient(cfg, walletSvc)
veliVirtualGameService := veli.New(veliCLient, walletSvc, cfg) veliVirtualGameService := veli.New(veliCLient, walletSvc, wallet.TransferStore(store), cfg)
recommendationSvc := recommendation.NewService(recommendationRepo) recommendationSvc := recommendation.NewService(recommendationRepo)
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
@ -187,7 +187,7 @@ func main() {
logger, logger,
) )
go httpserver.SetupReportCronJobs(context.Background(), reportSvc) go httpserver.SetupReportCronJobs(context.Background(), reportSvc, "C:/Users/User/Desktop")
go httpserver.ProcessBetCashback(context.TODO(), betSvc) go httpserver.ProcessBetCashback(context.TODO(), betSvc)
bankRepository := repository.NewBankRepository(store) bankRepository := repository.NewBankRepository(store)

View File

@ -1,59 +1,57 @@
-- name: GetCompanyWiseReport :many -- name: GetCompanyWiseReport :many
SELECT b.company_id, SELECT
c.name AS company_name, b.company_id,
COUNT(*) AS total_bets, c.name AS company_name,
COALESCE(SUM(b.amount), 0) AS total_cash_made, COUNT(*) AS total_bets,
COALESCE( COALESCE(SUM(b.amount), 0) AS total_cash_made,
SUM( COALESCE(
CASE SUM(
WHEN b.cashed_out THEN b.amount CASE
ELSE 0 WHEN sb.cashed_out THEN b.amount -- use actual cashed_out flag from shop_bets
END ELSE 0
), END
0 ), 0
) AS total_cash_out, ) AS total_cash_out,
COALESCE( COALESCE(
SUM( SUM(
CASE CASE
WHEN b.status = 5 THEN b.amount WHEN b.status = 5 THEN b.amount
ELSE 0 ELSE 0
END END
), ), 0
0 ) AS total_cash_backs
) AS total_cash_backs
FROM shop_bet_detail b FROM shop_bet_detail b
JOIN companies c ON b.company_id = c.id JOIN companies c ON b.company_id = c.id
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out
WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
GROUP BY b.company_id, GROUP BY b.company_id, c.name;
c.name;
-- name: GetBranchWiseReport :many -- name: GetBranchWiseReport :many
SELECT b.branch_id, SELECT
br.name AS branch_name, b.branch_id,
br.company_id, br.name AS branch_name,
COUNT(*) AS total_bets, br.company_id,
COALESCE(SUM(b.amount), 0) AS total_cash_made, COUNT(*) AS total_bets,
COALESCE( COALESCE(SUM(b.amount), 0) AS total_cash_made,
SUM( COALESCE(
CASE SUM(
WHEN b.cashed_out THEN b.amount CASE
ELSE 0 WHEN sb.cashed_out THEN b.amount -- use cashed_out from shop_bets
END ELSE 0
), END
0 ), 0
) AS total_cash_out, ) AS total_cash_out,
COALESCE( COALESCE(
SUM( SUM(
CASE CASE
WHEN b.status = 5 THEN b.amount WHEN b.status = 5 THEN b.amount
ELSE 0 ELSE 0
END END
), ), 0
0 ) AS total_cash_backs
) AS total_cash_backs
FROM shop_bet_detail b FROM shop_bet_detail b
JOIN branches br ON b.branch_id = br.id JOIN branches br ON b.branch_id = br.id
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out
WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
GROUP BY b.branch_id, GROUP BY b.branch_id, br.name, br.company_id;
br.name,
br.company_id;

View File

@ -12,35 +12,33 @@ import (
) )
const GetBranchWiseReport = `-- name: GetBranchWiseReport :many const GetBranchWiseReport = `-- name: GetBranchWiseReport :many
SELECT b.branch_id, SELECT
br.name AS branch_name, b.branch_id,
br.company_id, br.name AS branch_name,
COUNT(*) AS total_bets, br.company_id,
COALESCE(SUM(b.amount), 0) AS total_cash_made, COUNT(*) AS total_bets,
COALESCE( COALESCE(SUM(b.amount), 0) AS total_cash_made,
SUM( COALESCE(
CASE SUM(
WHEN b.cashed_out THEN b.amount CASE
ELSE 0 WHEN sb.cashed_out THEN b.amount -- use cashed_out from shop_bets
END ELSE 0
), END
0 ), 0
) AS total_cash_out, ) AS total_cash_out,
COALESCE( COALESCE(
SUM( SUM(
CASE CASE
WHEN b.status = 5 THEN b.amount WHEN b.status = 5 THEN b.amount
ELSE 0 ELSE 0
END END
), ), 0
0 ) AS total_cash_backs
) AS total_cash_backs
FROM shop_bet_detail b FROM shop_bet_detail b
JOIN branches br ON b.branch_id = br.id JOIN branches br ON b.branch_id = br.id
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out
WHERE b.created_at BETWEEN $1 AND $2 WHERE b.created_at BETWEEN $1 AND $2
GROUP BY b.branch_id, GROUP BY b.branch_id, br.name, br.company_id
br.name,
br.company_id
` `
type GetBranchWiseReportParams struct { type GetBranchWiseReportParams struct {
@ -87,33 +85,32 @@ func (q *Queries) GetBranchWiseReport(ctx context.Context, arg GetBranchWiseRepo
} }
const GetCompanyWiseReport = `-- name: GetCompanyWiseReport :many const GetCompanyWiseReport = `-- name: GetCompanyWiseReport :many
SELECT b.company_id, SELECT
c.name AS company_name, b.company_id,
COUNT(*) AS total_bets, c.name AS company_name,
COALESCE(SUM(b.amount), 0) AS total_cash_made, COUNT(*) AS total_bets,
COALESCE( COALESCE(SUM(b.amount), 0) AS total_cash_made,
SUM( COALESCE(
CASE SUM(
WHEN b.cashed_out THEN b.amount CASE
ELSE 0 WHEN sb.cashed_out THEN b.amount -- use actual cashed_out flag from shop_bets
END ELSE 0
), END
0 ), 0
) AS total_cash_out, ) AS total_cash_out,
COALESCE( COALESCE(
SUM( SUM(
CASE CASE
WHEN b.status = 5 THEN b.amount WHEN b.status = 5 THEN b.amount
ELSE 0 ELSE 0
END END
), ), 0
0 ) AS total_cash_backs
) AS total_cash_backs
FROM shop_bet_detail b FROM shop_bet_detail b
JOIN companies c ON b.company_id = c.id JOIN companies c ON b.company_id = c.id
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out
WHERE b.created_at BETWEEN $1 AND $2 WHERE b.created_at BETWEEN $1 AND $2
GROUP BY b.company_id, GROUP BY b.company_id, c.name
c.name
` `
type GetCompanyWiseReportParams struct { type GetCompanyWiseReportParams struct {

View File

@ -488,6 +488,7 @@ type LiveWalletMetrics struct {
BranchBalances []BranchWalletBalance `json:"branch_balances"` BranchBalances []BranchWalletBalance `json:"branch_balances"`
} }
// Company-level aggregated report
type CompanyReport struct { type CompanyReport struct {
CompanyID int64 CompanyID int64
CompanyName string CompanyName string
@ -497,6 +498,7 @@ type CompanyReport struct {
TotalCashBacks float64 TotalCashBacks float64
} }
// Branch-level aggregated report
type BranchReport struct { type BranchReport struct {
BranchID int64 BranchID int64
BranchName string BranchName string
@ -506,3 +508,14 @@ type BranchReport struct {
TotalCashOut float64 TotalCashOut float64
TotalCashBacks float64 TotalCashBacks float64
} }
// type CompanyReport struct {
// CompanyID int64
// Branches []BranchReport
// }
// type BranchReport struct {
// BranchID int64
// Rows []interface{} // replace with the actual row type, e.g., dbgen.GetWalletTransactionsInRangeRow
// }

View File

@ -136,6 +136,7 @@ type CancelRequest struct {
CorrelationID string `json:"correlationId,omitempty"` CorrelationID string `json:"correlationId,omitempty"`
ProviderID string `json:"providerId"` ProviderID string `json:"providerId"`
BrandID string `json:"brandId"` BrandID string `json:"brandId"`
IsAdjustment bool `json:"isAdjustment,omitempty"`
AdjustmentRefund *struct { AdjustmentRefund *struct {
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
Currency string `json:"currency"` Currency string `json:"currency"`

View File

@ -459,87 +459,76 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
return performances, nil return performances, nil
} }
func (s *Service) GenerateReport(ctx context.Context, period string) error { func (s *Service) GenerateReport(ctx context.Context, from, to time.Time) error {
data, err := s.fetchReportData(ctx, period) // Hardcoded output directory
outputDir := "C:/Users/User/Desktop/reports"
// Ensure directory exists
if err := os.MkdirAll(outputDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create report directory: %w", err)
}
companies, branchMap, err := s.fetchReportData(ctx, from, to)
if err != nil { if err != nil {
return fmt.Errorf("fetch data: %w", err) return err
} }
// Ensure the reports directory exists // per-company reports
if err := os.MkdirAll("reports", os.ModePerm); err != nil { for _, company := range companies {
return fmt.Errorf("creating reports directory: %w", err) branches := branchMap[company.CompanyID]
if err := writeCompanyCSV(company, branches, from, to, outputDir); err != nil {
return fmt.Errorf("company %d CSV: %w", company.CompanyID, err)
}
} }
filePath := fmt.Sprintf("reports/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04")) // summary report
if err := writeSummaryCSV(companies, from, to, outputDir); err != nil {
return fmt.Errorf("summary CSV: %w", err)
}
return nil
}
// writeCompanyCSV writes the company report to CSV in the hardcoded folder
func writeCompanyCSV(company domain.CompanyReport, branches []domain.BranchReport, from, to time.Time, outputDir string) error {
period := fmt.Sprintf("%s to %s", from.Format("2006-01-02"), to.Format("2006-01-02"))
filePath := fmt.Sprintf("%s/company_%d_%s_%s_%s.csv",
outputDir,
company.CompanyID,
from.Format("2006-01-02"),
to.Format("2006-01-02"),
time.Now().Format("2006-01-02_15-04"),
)
file, err := os.Create(filePath) file, err := os.Create(filePath)
if err != nil { if err != nil {
return fmt.Errorf("create file: %w", err) return fmt.Errorf("create company csv: %w", err)
} }
defer file.Close() defer file.Close()
writer := csv.NewWriter(file) writer := csv.NewWriter(file)
defer writer.Flush() defer writer.Flush()
// Summary section // Company summary section
if err := writer.Write([]string{"Sports Betting Reports (Periodic)"}); err != nil { writer.Write([]string{"Company Betting Report"})
return fmt.Errorf("write header: %w", err) writer.Write([]string{"Period", "Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
} writer.Write([]string{
if err := writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"}); err != nil {
return fmt.Errorf("write header row: %w", err)
}
if err := writer.Write([]string{
period, period,
fmt.Sprintf("%d", data.TotalBets), fmt.Sprintf("%d", company.CompanyID),
fmt.Sprintf("%.2f", data.TotalCashIn), company.CompanyName,
fmt.Sprintf("%.2f", data.TotalCashOut), fmt.Sprintf("%d", company.TotalBets),
fmt.Sprintf("%.2f", data.CashBacks), fmt.Sprintf("%.2f", company.TotalCashIn),
fmt.Sprintf("%.2f", data.Deposits), fmt.Sprintf("%.2f", company.TotalCashOut),
fmt.Sprintf("%.2f", data.Withdrawals), fmt.Sprintf("%.2f", company.TotalCashBacks),
fmt.Sprintf("%d", data.TotalTickets), })
}); err != nil {
return fmt.Errorf("write summary row: %w", err)
}
writer.Write([]string{}) // Empty line writer.Write([]string{}) // Empty line
// Virtual Game Summary section // Branch reports
writer.Write([]string{"Virtual Game Reports (Periodic)"}) writer.Write([]string{"Branch Reports"})
writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"})
for _, row := range data.VirtualGameStats {
if err := writer.Write([]string{
row.GameName,
fmt.Sprintf("%d", row.NumBets),
fmt.Sprintf("%.2f", row.TotalTransaction),
}); err != nil {
return fmt.Errorf("write virtual game row: %w", err)
}
}
writer.Write([]string{}) // Empty line
// Company Reports
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 {
if err := 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),
}); err != nil {
return fmt.Errorf("write company row: %w", err)
}
}
writer.Write([]string{}) // Empty line
// Branch Reports
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"}) 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 { for _, br := range branches {
if err := writer.Write([]string{ writer.Write([]string{
fmt.Sprintf("%d", br.BranchID), fmt.Sprintf("%d", br.BranchID),
br.BranchName, br.BranchName,
fmt.Sprintf("%d", br.CompanyID), fmt.Sprintf("%d", br.CompanyID),
@ -547,211 +536,142 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
fmt.Sprintf("%.2f", br.TotalCashIn), fmt.Sprintf("%.2f", br.TotalCashIn),
fmt.Sprintf("%.2f", br.TotalCashOut), fmt.Sprintf("%.2f", br.TotalCashOut),
fmt.Sprintf("%.2f", br.TotalCashBacks), fmt.Sprintf("%.2f", br.TotalCashBacks),
}); err != nil { })
return fmt.Errorf("write branch row: %w", err)
}
} }
// Total Summary if err := writer.Error(); err != nil {
return fmt.Errorf("flush error: %w", err)
}
return nil
}
// writeSummaryCSV writes the summary report to CSV in the hardcoded folder
func writeSummaryCSV(companies []domain.CompanyReport, from, to time.Time, outputDir string) error {
period := fmt.Sprintf("%s to %s", from.Format("2006-01-02"), to.Format("2006-01-02"))
filePath := fmt.Sprintf("%s/summary_%s_%s_%s.csv",
outputDir,
from.Format("2006-01-02"),
to.Format("2006-01-02"),
time.Now().Format("2006-01-02_15-04"),
)
file, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("create summary csv: %w", err)
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// Global summary
writer.Write([]string{"Global Betting Summary"})
writer.Write([]string{"Period", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
var totalBets int64 var totalBets int64
var totalCashIn, totalCashOut, totalCashBacks float64 var totalIn, totalOut, totalBack float64
for _, cr := range data.CompanyReports { for _, c := range companies {
totalBets += cr.TotalBets totalBets += c.TotalBets
totalCashIn += cr.TotalCashIn totalIn += c.TotalCashIn
totalCashOut += cr.TotalCashOut totalOut += c.TotalCashOut
totalCashBacks += cr.TotalCashBacks totalBack += c.TotalCashBacks
} }
writer.Write([]string{}) // Empty line writer.Write([]string{
writer.Write([]string{"Total Summary"}) period,
writer.Write([]string{"Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
if err := writer.Write([]string{
fmt.Sprintf("%d", totalBets), fmt.Sprintf("%d", totalBets),
fmt.Sprintf("%.2f", totalCashIn), fmt.Sprintf("%.2f", totalIn),
fmt.Sprintf("%.2f", totalCashOut), fmt.Sprintf("%.2f", totalOut),
fmt.Sprintf("%.2f", totalCashBacks), fmt.Sprintf("%.2f", totalBack),
}); err != nil { })
return fmt.Errorf("write total summary row: %w", err) writer.Write([]string{}) // Empty line
// Company breakdown
writer.Write([]string{"Company Reports"})
writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
for _, cr := range companies {
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),
})
}
if err := writer.Error(); err != nil {
return fmt.Errorf("flush error: %w", err)
} }
return nil return nil
} }
func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) { func (s *Service) fetchReportData(ctx context.Context, from, to time.Time) (
from, to := getTimeRange(period) []domain.CompanyReport, map[int64][]domain.BranchReport, error,
// companyID := int64(0) ) {
// --- company level ---
// Basic metrics companyRows, err := s.repo.GetCompanyWiseReport(ctx, from, to)
totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to) if err != nil {
cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to) return nil, nil, fmt.Errorf("company-wise report: %w", err)
cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to)
cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to)
// Wallet Transactions
transactions, _ := s.repo.GetWalletTransactionsInRange(ctx, from, to)
var totalDeposits, totalWithdrawals float64
for _, tx := range transactions {
switch strings.ToLower(tx.Type.String) {
case "deposit":
totalDeposits += float64(tx.TotalAmount)
case "withdraw":
totalWithdrawals += float64(tx.TotalAmount)
}
} }
// Ticket Count companies := make([]domain.CompanyReport, 0, len(companyRows))
totalTickets, _ := s.repo.GetAllTicketsInRange(ctx, from, to)
// Virtual Game Summary
virtualGameStats, _ := s.repo.GetVirtualGameSummaryInRange(ctx, from, to)
// Convert []dbgen.GetVirtualGameSummaryInRangeRow to []domain.VirtualGameStat
var virtualGameStatsDomain []domain.VirtualGameStat
for _, row := range virtualGameStats {
var totalTransaction float64
switch v := row.TotalTransactionSum.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalTransaction = val
}
case float64:
totalTransaction = v
case int:
totalTransaction = float64(v)
default:
totalTransaction = 0
}
virtualGameStatsDomain = append(virtualGameStatsDomain, domain.VirtualGameStat{
GameName: row.GameName,
NumBets: row.NumberOfBets,
TotalTransaction: totalTransaction,
})
}
companyRows, _ := s.repo.GetCompanyWiseReport(ctx, from, to)
var companyReports []domain.CompanyReport
for _, row := range companyRows { for _, row := range companyRows {
var totalCashIn, totalCashOut, totalCashBacks float64 companies = append(companies, domain.CompanyReport{
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, CompanyID: row.CompanyID,
CompanyName: row.CompanyName, CompanyName: row.CompanyName,
TotalBets: row.TotalBets, TotalBets: row.TotalBets,
TotalCashIn: totalCashIn, TotalCashIn: toFloat(row.TotalCashMade),
TotalCashOut: totalCashOut, TotalCashOut: toFloat(row.TotalCashOut),
TotalCashBacks: totalCashBacks, TotalCashBacks: toFloat(row.TotalCashBacks),
}) })
} }
branchRows, _ := s.repo.GetBranchWiseReport(ctx, from, to) // --- branch level ---
var branchReports []domain.BranchReport branchRows, err := s.repo.GetBranchWiseReport(ctx, from, to)
if err != nil {
return nil, nil, fmt.Errorf("branch-wise report: %w", err)
}
branchMap := make(map[int64][]domain.BranchReport)
for _, row := range branchRows { for _, row := range branchRows {
var totalCashIn, totalCashOut, totalCashBacks float64 branch := domain.BranchReport{
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, BranchID: row.BranchID,
BranchName: row.BranchName, BranchName: row.BranchName,
CompanyID: row.CompanyID, CompanyID: row.CompanyID,
TotalBets: row.TotalBets, TotalBets: row.TotalBets,
TotalCashIn: totalCashIn, TotalCashIn: toFloat(row.TotalCashMade),
TotalCashOut: totalCashOut, TotalCashOut: toFloat(row.TotalCashOut),
TotalCashBacks: totalCashBacks, TotalCashBacks: toFloat(row.TotalCashBacks),
}) }
branchMap[row.CompanyID] = append(branchMap[row.CompanyID], branch)
} }
return domain.ReportData{ return companies, branchMap, nil
TotalBets: totalBets,
TotalCashIn: cashIn,
TotalCashOut: cashOut,
CashBacks: cashBacks,
Deposits: totalDeposits,
Withdrawals: totalWithdrawals,
TotalTickets: totalTickets.TotalTickets,
VirtualGameStats: virtualGameStatsDomain,
CompanyReports: companyReports,
BranchReports: branchReports,
}, nil
} }
func getTimeRange(period string) (time.Time, time.Time) { // helper to unify float conversions
func toFloat(val interface{}) float64 {
switch v := val.(type) {
case string:
if f, err := strconv.ParseFloat(v, 64); err == nil {
return f
}
case float64:
return v
case int:
return float64(v)
case int64:
return float64(v)
}
return 0
}
func GetTimeRange(period string) (time.Time, time.Time) {
now := time.Now() now := time.Now()
switch strings.ToLower(period) { switch strings.ToLower(period) {
case "daily": case "daily":
@ -809,3 +729,31 @@ func calculatePerformanceScore(perf domain.BranchPerformance) float64 {
return profitScore + customerScore + betScore + winRateScore return profitScore + customerScore + betScore + winRateScore
} }
// toCompanyReport converts grouped data into []CompanyReport
// func toCompanyReport(grouped map[int64]map[int64][]interface{}) []domain.CompanyReport {
// companyReports := []domain.CompanyReport{}
// for companyID, branches := range grouped {
// companyReport := domain.CompanyReport{
// CompanyID: companyID,
// Branches: []domain.BranchReport{},
// }
// for branchID, rows := range branches {
// branchReport := domain.BranchReport{
// BranchID: branchID,
// Rows: rows,
// }
// companyReport.Branches = append(companyReport.Branches, branchReport)
// }
// companyReports = append(companyReports, companyReport)
// }
// return companyReports
// }
// // toBranchReport converts []interface{} to []BranchReport
// func toBranchReport(rows []interface{}, branchID int64) domain.BranchReport {
// return domain.BranchReport{
// BranchID: branchID,
// Rows: rows,
// }
// }

View File

@ -19,16 +19,18 @@ var (
) )
type service struct { type service struct {
client *Client client *Client
walletSvc *wallet.Service walletSvc *wallet.Service
cfg *config.Config transfetStore wallet.TransferStore
cfg *config.Config
} }
func New(client *Client, walletSvc *wallet.Service, cfg *config.Config) VeliVirtualGameService { func New(client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) VeliVirtualGameService {
return &service{ return &service{
client: client, client: client,
walletSvc: walletSvc, walletSvc: walletSvc,
cfg: cfg, transfetStore: transferStore,
cfg: cfg,
} }
} }
@ -338,7 +340,7 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
// --- 1. Validate PlayerID --- // --- 1. Validate PlayerID ---
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid PlayerID %s", req.PlayerID) return nil, fmt.Errorf("invalid PlayerID %q", req.PlayerID)
} }
// --- 2. Get player wallets --- // --- 2. Get player wallets ---
@ -358,18 +360,23 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
bonusBalance = float64(playerWallets[1].Balance) bonusBalance = float64(playerWallets[1].Balance)
} }
// --- 3. Refund handling --- // --- 3. Determine refund amount based on IsAdjustment ---
var refundAmount float64 var refundAmount float64
if req.AdjustmentRefund.Amount > 0 { if req.IsAdjustment {
if req.AdjustmentRefund.Amount <= 0 {
return nil, fmt.Errorf("missing adjustmentRefund for adjustment cancel")
}
refundAmount = req.AdjustmentRefund.Amount refundAmount = req.AdjustmentRefund.Amount
} else { } else {
// If cancelType = CANCEL_BET and no explicit adjustmentRefund, // Regular cancel: fetch original bet amount if needed
// we may need to look up the original bet transaction and refund that. originalTransfer, err := s.transfetStore.GetTransferByReference(ctx, req.RefTransactionID)
// TODO: implement transaction lookup if required by your domain. if err != nil {
return nil, fmt.Errorf("missing adjustmentRefund for CANCEL_BET") return nil, fmt.Errorf("failed to get original bet for cancellation: %w", err)
}
refundAmount = float64(originalTransfer.Amount)
} }
// For now, we assume refund goes back fully to real wallet // --- 4. Refund to wallet ---
usedReal := refundAmount usedReal := refundAmount
usedBonus := 0.0 usedBonus := 0.0
@ -392,7 +399,7 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
return nil, fmt.Errorf("failed to refund wallet: %w", err) return nil, fmt.Errorf("failed to refund wallet: %w", err)
} }
// --- 4. Reload balances after refund --- // --- 5. Reload balances after refund ---
updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to reload balances: %w", err) return nil, fmt.Errorf("failed to reload balances: %w", err)
@ -405,11 +412,11 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
bonusBalance = float64(updatedWallets[1].Balance) bonusBalance = float64(updatedWallets[1].Balance)
} }
// --- 5. Build response --- // --- 6. Build response ---
res := &domain.CancelResponse{ res := &domain.CancelResponse{
WalletTransactionID: req.TransactionID, WalletTransactionID: req.TransactionID,
Real: domain.BalanceDetail{ Real: domain.BalanceDetail{
Currency: req.AdjustmentRefund.Currency, Currency: "ETB",
Amount: realBalance, Amount: realBalance,
}, },
UsedRealAmount: usedReal, UsedRealAmount: usedReal,
@ -418,7 +425,7 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
if bonusBalance > 0 { if bonusBalance > 0 {
res.Bonus = &domain.BalanceDetail{ res.Bonus = &domain.BalanceDetail{
Currency: req.AdjustmentRefund.Currency, Currency: "ETB",
Amount: bonusBalance, Amount: bonusBalance,
} }
} }
@ -426,6 +433,12 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (
return res, nil return res, nil
} }
// Example helper to fetch original bet
// func (s *service) getOriginalBet(ctx context.Context, transactionID string) (*domain.BetRecord, error) {
// // TODO: implement actual lookup
// return &domain.BetRecord{Amount: 50}, nil
// }
func (s *service) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) { func (s *service) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) {
// --- Signature Params (flattened strings for signing) --- // --- Signature Params (flattened strings for signing) ---
sigParams := map[string]any{ sigParams := map[string]any{

View File

@ -140,15 +140,16 @@ func StartTicketCrons(ticketService ticket.Service, mongoLogger *zap.Logger) {
mongoLogger.Info("Cron jobs started for ticket service") mongoLogger.Info("Cron jobs started for ticket service")
} }
func SetupReportCronJobs(ctx context.Context, reportService *report.Service) { // SetupReportCronJobs schedules periodic report generation
c := cron.New(cron.WithSeconds()) // use WithSeconds for tighter intervals during testing func SetupReportCronJobs(ctx context.Context, reportService *report.Service, outputDir string) {
c := cron.New(cron.WithSeconds()) // use WithSeconds for testing
schedule := []struct { schedule := []struct {
spec string spec string
period string period string
}{ }{
// { // {
// spec: "*/300 * * * * *", // Every 5 minutes (300 seconds) // spec: "*/60 * * * * *", // Every 5 minutes
// period: "5min", // period: "5min",
// }, // },
{ {
@ -167,9 +168,36 @@ func SetupReportCronJobs(ctx context.Context, reportService *report.Service) {
for _, job := range schedule { for _, job := range schedule {
period := job.period period := job.period
if _, err := c.AddFunc(job.spec, func() { if _, err := c.AddFunc(job.spec, func() {
log.Printf("Running %s report at %s", period, time.Now().Format(time.RFC3339)) now := time.Now()
if err := reportService.GenerateReport(ctx, period); err != nil { var from, to time.Time
switch period {
// case "5min":
// from = now.Add(-5 * time.Minute)
// to = now
case "daily":
from = time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location())
to = time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location())
case "weekly":
// last Sunday -> Saturday
weekday := int(now.Weekday())
daysSinceSunday := (weekday + 7) % 7
from = time.Date(now.Year(), now.Month(), now.Day()-daysSinceSunday-7, 0, 0, 0, 0, now.Location())
to = from.AddDate(0, 0, 6).Add(time.Hour*23 + time.Minute*59 + time.Second*59)
case "monthly":
firstOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
from = firstOfMonth.AddDate(0, -1, 0)
to = firstOfMonth.Add(-time.Second)
default:
log.Printf("Unknown period: %s", period)
return
}
log.Printf("Running %s report for period %s -> %s", period, from.Format(time.RFC3339), to.Format(time.RFC3339))
if err := reportService.GenerateReport(ctx, from, to); err != nil {
log.Printf("Error generating %s report: %v", period, err) log.Printf("Error generating %s report: %v", period, err)
} else { } else {
log.Printf("Successfully generated %s report", period) log.Printf("Successfully generated %s report", period)
@ -180,7 +208,7 @@ func SetupReportCronJobs(ctx context.Context, reportService *report.Service) {
} }
c.Start() c.Start()
log.Println("Cron jobs started for report generation service") log.Printf("Cron jobs started. Reports will be saved to: %s", outputDir)
} }
func ProcessBetCashback(ctx context.Context, betService *betSvc.Service) { func ProcessBetCashback(ctx context.Context, betService *betSvc.Service) {

View File

@ -157,7 +157,7 @@ func (h *Handler) DownloadReportFile(c *fiber.Ctx) error {
}) })
} }
reportDir := "reports" reportDir := "C:/Users/User/Desktop/reports"
// Ensure reports directory exists // Ensure reports directory exists
if _, err := os.Stat(reportDir); os.IsNotExist(err) { if _, err := os.Stat(reportDir); os.IsNotExist(err) {
@ -207,7 +207,7 @@ func (h *Handler) DownloadReportFile(c *fiber.Ctx) error {
// @Failure 500 {object} domain.ErrorResponse "Failed to read report directory" // @Failure 500 {object} domain.ErrorResponse "Failed to read report directory"
// @Router /api/v1/report-files/list [get] // @Router /api/v1/report-files/list [get]
func (h *Handler) ListReportFiles(c *fiber.Ctx) error { func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
reportDir := "reports" reportDir := "C:/Users/User/Desktop/reports"
searchTerm := c.Query("search") searchTerm := c.Query("search")
page := c.QueryInt("page", 1) page := c.QueryInt("page", 1)
limit := c.QueryInt("limit", 20) limit := c.QueryInt("limit", 20)

View File

@ -110,7 +110,7 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error {
} }
func (h *Handler) HandleBet(c *fiber.Ctx) error { func (h *Handler) HandleBet(c *fiber.Ctx) error {
// Read the raw body to avoid parsing issues // Read the raw body
body := c.Body() body := c.Body()
if len(body) == 0 { if len(body) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
@ -119,26 +119,11 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
}) })
} }
// Try to identify the provider based on the request structure // Try parsing as Veli bet request
provider, err := identifyBetProvider(body) var veliReq domain.BetRequest
if err != nil { if err := json.Unmarshal(body, &veliReq); err == nil && veliReq.SessionID != "" && veliReq.BrandID != "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ // Process as Veli
Message: "Unrecognized request format", res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), veliReq)
Error: err.Error(),
})
}
switch provider {
case "veli":
var req domain.BetRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Veli bet request",
Error: err.Error(),
})
}
res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req)
if err != nil { if err != nil {
if errors.Is(err, veli.ErrDuplicateTransaction) { if errors.Is(err, veli.ErrDuplicateTransaction) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
@ -152,17 +137,13 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
}) })
} }
return c.JSON(res) return c.JSON(res)
}
case "popok": // Try parsing as PopOK bet request
var req domain.PopOKBetRequest var popokReq domain.PopOKBetRequest
if err := json.Unmarshal(body, &req); err != nil { if err := json.Unmarshal(body, &popokReq); err == nil && popokReq.ExternalToken != "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ // Process as PopOK
Message: "Invalid PopOK bet request", resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &popokReq)
Error: err.Error(),
})
}
resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req)
if err != nil { if err != nil {
code := fiber.StatusInternalServerError code := fiber.StatusInternalServerError
switch err.Error() { switch err.Error() {
@ -177,13 +158,13 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
}) })
} }
return c.JSON(resp) return c.JSON(resp)
default:
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Unsupported provider",
Error: "Request format doesn't match any supported provider",
})
} }
// If neither works
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Unsupported provider",
Error: "Request format doesn't match any supported provider",
})
} }
// identifyProvider examines the request body to determine the provider // identifyProvider examines the request body to determine the provider
@ -513,26 +494,25 @@ func (h *Handler) ListFavorites(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(games) return c.Status(fiber.StatusOK).JSON(games)
} }
func identifyBetProvider(body []byte) (string, error) { func IdentifyBetProvider(body []byte) (string, error) {
// Check for Veli signature fields // Check for Veli signature fields
var veliCheck struct { var veliCheck struct {
TransactionID string `json:"transaction_id"` SessionID string `json:"sessionId"`
GameID string `json:"game_id"` BrandID string `json:"brandId"`
} }
if json.Unmarshal(body, &veliCheck) == nil { if json.Unmarshal(body, &veliCheck) == nil {
if veliCheck.TransactionID != "" && veliCheck.GameID != "" { if veliCheck.SessionID != "" && veliCheck.BrandID != "" {
return "veli", nil return "veli", nil
} }
} }
// Check for PopOK signature fields // Check for PopOK signature fields
var popokCheck struct { var popokCheck struct {
Token string `json:"token"` Token string `json:"externalToken"`
PlayerID string `json:"player_id"`
BetAmount float64 `json:"bet_amount"`
} }
if json.Unmarshal(body, &popokCheck) == nil { if json.Unmarshal(body, &popokCheck) == nil {
if popokCheck.Token != "" && popokCheck.PlayerID != "" { if popokCheck.Token != "" {
return "popok", nil return "popok", nil
} }
} }
@ -544,23 +524,22 @@ func identifyBetProvider(body []byte) (string, error) {
func identifyWinProvider(body []byte) (string, error) { func identifyWinProvider(body []byte) (string, error) {
// Check for Veli signature fields // Check for Veli signature fields
var veliCheck struct { var veliCheck struct {
TransactionID string `json:"transaction_id"` SessionID string `json:"sessionId"`
WinAmount float64 `json:"win_amount"` BrandID string `json:"brandId"`
} }
if json.Unmarshal(body, &veliCheck) == nil { if json.Unmarshal(body, &veliCheck) == nil {
if veliCheck.TransactionID != "" && veliCheck.WinAmount > 0 { if veliCheck.SessionID != "" && veliCheck.BrandID != "" {
return "veli", nil return "veli", nil
} }
} }
// Check for PopOK signature fields // Check for PopOK signature fields
var popokCheck struct { var popokCheck struct {
Token string `json:"token"` Token string `json:"externalToken"`
PlayerID string `json:"player_id"`
WinAmount float64 `json:"win_amount"`
} }
if json.Unmarshal(body, &popokCheck) == nil { if json.Unmarshal(body, &popokCheck) == nil {
if popokCheck.Token != "" && popokCheck.PlayerID != "" { if popokCheck.Token != "" {
return "popok", nil return "popok", nil
} }
} }
@ -571,24 +550,22 @@ func identifyWinProvider(body []byte) (string, error) {
func identifyCancelProvider(body []byte) (string, error) { func identifyCancelProvider(body []byte) (string, error) {
// Check for Veli cancel signature // Check for Veli cancel signature
var veliCheck struct { var veliCheck struct {
TransactionID string `json:"transaction_id"` SessionID string `json:"sessionId"`
OriginalTxID string `json:"original_transaction_id"` BrandID string `json:"brandId"`
CancelReason string `json:"cancel_reason"`
} }
if json.Unmarshal(body, &veliCheck) == nil { if json.Unmarshal(body, &veliCheck) == nil {
if veliCheck.TransactionID != "" && veliCheck.OriginalTxID != "" { if veliCheck.SessionID != "" && veliCheck.BrandID != "" {
return "veli", nil return "veli", nil
} }
} }
// Check for PopOK cancel signature // Check for PopOK cancel signature
var popokCheck struct { var popokCheck struct {
Token string `json:"token"` Token string `json:"externalToken"`
PlayerID string `json:"player_id"`
OriginalTxID string `json:"original_transaction_id"`
} }
if json.Unmarshal(body, &popokCheck) == nil { if json.Unmarshal(body, &popokCheck) == nil {
if popokCheck.Token != "" && popokCheck.PlayerID != "" && popokCheck.OriginalTxID != "" { if popokCheck.Token != "" {
return "popok", nil return "popok", nil
} }
} }

View File

@ -294,7 +294,7 @@ func (a *App) initAppRoutes() {
//Report Routes //Report Routes
groupV1.Get("/reports/dashboard", a.authMiddleware, a.OnlyAdminAndAbove, h.GetDashboardReport) groupV1.Get("/reports/dashboard", a.authMiddleware, a.OnlyAdminAndAbove, h.GetDashboardReport)
groupV1.Get("/report-files/download/:filename", a.authMiddleware, a.OnlyAdminAndAbove, h.DownloadReportFile) groupV1.Get("/report-files/download/:filename", h.DownloadReportFile)
groupV1.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles) groupV1.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles)
//Alea Play Virtual Game Routes //Alea Play Virtual Game Routes