transaction maker-checker fixes

This commit is contained in:
Yared Yemane 2025-07-11 15:48:59 +03:00
parent d5bfe98900
commit 2b9302b10b
34 changed files with 4833 additions and 1306 deletions

View File

@ -110,11 +110,13 @@ func main() {
notificationSvc := notificationservice.New(notificationRepo, logger, cfg) notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
var notificatioStore notificationservice.NotificationStore var notificatioStore notificationservice.NotificationStore
// var userStore user.UserStore
walletSvc := wallet.NewService( walletSvc := wallet.NewService(
wallet.WalletStore(store), wallet.WalletStore(store),
wallet.TransferStore(store), wallet.TransferStore(store),
notificatioStore, notificatioStore,
// userStore,
notificationSvc, notificationSvc,
logger, logger,
) )

View File

@ -35,6 +35,33 @@ WHERE (
AND ( AND (
is_active = sqlc.narg('is_active') is_active = sqlc.narg('is_active')
OR sqlc.narg('is_active') IS NULL OR sqlc.narg('is_active') IS NULL
)
AND (
name ILIKE '%' || sqlc.narg('search_term') || '%'
OR sqlc.narg('search_term') IS NULL
)
AND (
code ILIKE '%' || sqlc.narg('search_term') || '%'
OR sqlc.narg('search_term') IS NULL
)
ORDER BY name ASC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: CountBanks :one
SELECT COUNT(*)
FROM banks
WHERE (
country_id = $1
OR $1 IS NULL
)
AND (
is_active = $2
OR $2 IS NULL
)
AND (
name ILIKE '%' || $3 || '%'
OR code ILIKE '%' || $3 || '%'
OR $3 IS NULL
); );
-- name: UpdateBank :one -- name: UpdateBank :one

View File

@ -66,3 +66,9 @@ SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id,
FROM branches FROM branches
WHERE wallet_id = $1 WHERE wallet_id = $1
LIMIT 1; LIMIT 1;
-- -- name: GetCustomerByWalletID :one
-- SELECT id, first_name, last_name, email, phone_number,email_verified,phone_verified,company_id,suspended
-- FROM users
-- 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

View File

@ -11,6 +11,37 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
const CountBanks = `-- name: CountBanks :one
SELECT COUNT(*)
FROM banks
WHERE (
country_id = $1
OR $1 IS NULL
)
AND (
is_active = $2
OR $2 IS NULL
)
AND (
name ILIKE '%' || $3 || '%'
OR code ILIKE '%' || $3 || '%'
OR $3 IS NULL
)
`
type CountBanksParams struct {
CountryID int32 `json:"country_id"`
IsActive int32 `json:"is_active"`
Column3 pgtype.Text `json:"column_3"`
}
func (q *Queries) CountBanks(ctx context.Context, arg CountBanksParams) (int64, error) {
row := q.db.QueryRow(ctx, CountBanks, arg.CountryID, arg.IsActive, arg.Column3)
var count int64
err := row.Scan(&count)
return count, err
}
const CreateBank = `-- name: CreateBank :one const CreateBank = `-- name: CreateBank :one
INSERT INTO banks ( INSERT INTO banks (
slug, slug,
@ -106,15 +137,34 @@ WHERE (
is_active = $2 is_active = $2
OR $2 IS NULL OR $2 IS NULL
) )
AND (
name ILIKE '%' || $3 || '%'
OR $3 IS NULL
)
AND (
code ILIKE '%' || $3 || '%'
OR $3 IS NULL
)
ORDER BY name ASC
LIMIT $5 OFFSET $4
` `
type GetAllBanksParams struct { type GetAllBanksParams struct {
CountryID pgtype.Int4 `json:"country_id"` CountryID pgtype.Int4 `json:"country_id"`
IsActive pgtype.Int4 `json:"is_active"` IsActive pgtype.Int4 `json:"is_active"`
SearchTerm pgtype.Text `json:"search_term"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
} }
func (q *Queries) GetAllBanks(ctx context.Context, arg GetAllBanksParams) ([]Bank, error) { func (q *Queries) GetAllBanks(ctx context.Context, arg GetAllBanksParams) ([]Bank, error) {
rows, err := q.db.Query(ctx, GetAllBanks, arg.CountryID, arg.IsActive) rows, err := q.db.Query(ctx, GetAllBanks,
arg.CountryID,
arg.IsActive,
arg.SearchTerm,
arg.Offset,
arg.Limit,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }

4
go.mod
View File

@ -78,7 +78,7 @@ require (
) )
require ( require (
github.com/go-resty/resty/v2 v2.16.5 // github.com/go-resty/resty/v2 v2.16.5
github.com/twilio/twilio-go v1.26.3 github.com/twilio/twilio-go v1.26.3
) )
@ -87,6 +87,6 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/mock v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/redis/go-redis/v9 v9.10.0 // indirect github.com/redis/go-redis/v9 v9.10.0 // direct
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
) )

View File

@ -1 +0,0 @@
package domain

View File

@ -9,6 +9,9 @@ var (
ErrInsufficientBalance = errors.New("insufficient balance") ErrInsufficientBalance = errors.New("insufficient balance")
ErrInvalidWithdrawalAmount = errors.New("invalid withdrawal amount") ErrInvalidWithdrawalAmount = errors.New("invalid withdrawal amount")
ErrWithdrawalNotFound = errors.New("withdrawal not found") ErrWithdrawalNotFound = errors.New("withdrawal not found")
ErrPaymentNotFound = errors.New("payment not found")
ErrPaymentAlreadyExists = errors.New("payment with this reference already exists")
ErrInvalidPaymentAmount = errors.New("invalid payment amount")
) )
type PaymentStatus string type PaymentStatus string

View File

@ -79,5 +79,12 @@ func CalculateWinnings(amount Currency, totalOdds float32) Currency {
} }
type Pagination struct {
Total int `json:"total"`
TotalPages int `json:"total_pages"`
CurrentPage int `json:"current_page"`
Limit int `json:"limit"`
}
func PtrFloat64(v float64) *float64 { return &v } func PtrFloat64(v float64) *float64 { return &v }
func PtrInt64(v int64) *int64 { return &v } func PtrInt64(v int64) *int64 { return &v }

View File

@ -19,3 +19,10 @@ type Bank struct {
Currency string `json:"currency"` Currency string `json:"currency"`
BankLogo string `json:"bank_logo"` // URL or base64 BankLogo string `json:"bank_logo"` // URL or base64
} }
type InstResponse struct {
Message string `json:"message"`
Status string `json:"status"`
Data interface{} `json:"data"` // Changed to interface{} for flexibility
Pagination *Pagination `json:"pagination,omitempty"` // Made pointer and optional
}

View File

@ -10,3 +10,11 @@ type LogEntry struct {
Service string `json:"service" bson:"service"` Service string `json:"service" bson:"service"`
Env string `json:"env" bson:"env"` Env string `json:"env" bson:"env"`
} }
type LogResponse struct {
Message string `json:"message" bson:"message"`
Data []LogEntry `json:"data"`
Pagination Pagination `json:"pagination"`
}

View File

@ -28,8 +28,9 @@ const (
NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success" NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success"
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result" NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected"
NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required"
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin"
NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer" NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
NotificationRecieverSideCashier NotificationRecieverSide = "cashier" NotificationRecieverSideCashier NotificationRecieverSide = "cashier"

View File

@ -18,6 +18,12 @@ const (
ReportMonthly ReportFrequency = "monthly" ReportMonthly ReportFrequency = "monthly"
) )
type PaginatedFileResponse struct {
Response `json:",inline"`
Data []string `json:"data"`
Pagination Pagination `json:"pagination"`
}
type ReportRequest struct { type ReportRequest struct {
Frequency ReportFrequency Frequency ReportFrequency
StartDate time.Time StartDate time.Time

View File

@ -25,6 +25,30 @@ const (
TRANSFER_OTHER PaymentMethod = "other" TRANSFER_OTHER PaymentMethod = "other"
) )
type TransactionApproval struct {
ID int64
TransferID int64
ApprovedBy int64 // User ID of approver
Status string // "pending", "approved", "rejected"
Comments string
CreatedAt time.Time
UpdatedAt time.Time
}
type ApprovalAction string
const (
ApprovalActionApprove ApprovalAction = "approve"
ApprovalActionReject ApprovalAction = "reject"
)
type ApprovalRequest struct {
TransferID int64
Action ApprovalAction
Comments string
ApproverID int64
}
// Info for the payment providers // Info for the payment providers
type PaymentDetails struct { type PaymentDetails struct {
ReferenceNumber ValidString ReferenceNumber ValidString

View File

@ -13,7 +13,14 @@ import (
type BankRepository interface { type BankRepository interface {
CreateBank(ctx context.Context, bank *domain.Bank) error CreateBank(ctx context.Context, bank *domain.Bank) error
GetBankByID(ctx context.Context, id int) (*domain.Bank, error) GetBankByID(ctx context.Context, id int) (*domain.Bank, error)
GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error) GetAllBanks(
ctx context.Context,
countryID *int,
isActive *bool,
searchTerm *string,
page int,
pageSize int,
) ([]domain.Bank, int64, error)
UpdateBank(ctx context.Context, bank *domain.Bank) error UpdateBank(ctx context.Context, bank *domain.Bank) error
DeleteBank(ctx context.Context, id int) error DeleteBank(ctx context.Context, id int) error
} }
@ -63,28 +70,72 @@ func (r *BankRepo) GetBankByID(ctx context.Context, id int) (*domain.Bank, error
return mapDBBankToDomain(&dbBank), nil return mapDBBankToDomain(&dbBank), nil
} }
func (r *BankRepo) GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error) { func (r *BankRepo) GetAllBanks(
ctx context.Context,
countryID *int,
isActive *bool,
searchTerm *string,
page int,
pageSize int,
) ([]domain.Bank, int64, error) {
// Set default pagination values if not provided
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 50
}
offset := (page - 1) * pageSize
params := dbgen.GetAllBanksParams{ params := dbgen.GetAllBanksParams{
CountryID: pgtype.Int4{}, CountryID: pgtype.Int4{},
IsActive: pgtype.Int4{}, IsActive: pgtype.Int4{},
SearchTerm: pgtype.Text{},
Limit: pgtype.Int4{Int32: int32(pageSize), Valid: true},
Offset: pgtype.Int4{Int32: int32(offset), Valid: true},
} }
if countryID != nil { if countryID != nil {
params.CountryID = pgtype.Int4{Int32: int32(*countryID), Valid: true} params.CountryID = pgtype.Int4{Int32: int32(*countryID), Valid: true}
} }
if isActive != nil { if isActive != nil {
params.IsActive = pgtype.Int4{Int32: int32(*isActive), Valid: true} var activeInt int32
if *isActive {
activeInt = 1
} else {
activeInt = 0
}
params.IsActive = pgtype.Int4{Int32: activeInt, Valid: true}
}
if searchTerm != nil {
params.SearchTerm = pgtype.Text{String: *searchTerm, Valid: true}
} }
// Get paginated results
dbBanks, err := r.store.queries.GetAllBanks(ctx, params) dbBanks, err := r.store.queries.GetAllBanks(ctx, params)
if err != nil { if err != nil {
return nil, err return nil, 0, err
}
// Get total count for pagination
var countCountryID int32
if params.CountryID.Valid {
countCountryID = params.CountryID.Int32
}
total, err := r.store.queries.CountBanks(ctx, dbgen.CountBanksParams{
CountryID: countCountryID,
IsActive: params.IsActive.Int32,
})
if err != nil {
return nil, 0, err
} }
banks := make([]domain.Bank, len(dbBanks)) banks := make([]domain.Bank, len(dbBanks))
for i, b := range dbBanks { for i, b := range dbBanks {
banks[i] = *mapDBBankToDomain(&b) banks[i] = *mapDBBankToDomain(&b)
} }
return banks, nil
return banks, total, nil
} }
func (r *BankRepo) UpdateBank(ctx context.Context, bank *domain.Bank) error { func (r *BankRepo) UpdateBank(ctx context.Context, bank *domain.Bank) error {

View File

@ -159,3 +159,7 @@ func (s *Store) UpdateTransferStatus(ctx context.Context, id int64, status strin
return err return err
} }
func ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
return nil
}

View File

@ -14,12 +14,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
var (
ErrPaymentNotFound = errors.New("payment not found")
ErrPaymentAlreadyExists = errors.New("payment with this reference already exists")
ErrInvalidPaymentAmount = errors.New("invalid payment amount")
)
type Service struct { type Service struct {
transferStore wallet.TransferStore transferStore wallet.TransferStore
walletStore wallet.Service walletStore wallet.Service
@ -49,7 +43,7 @@ func NewService(
func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) { func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
// Validate amount // Validate amount
if amount <= 0 { if amount <= 0 {
return "", ErrInvalidPaymentAmount return "", domain.ErrInvalidPaymentAmount
} }
// Get user details // Get user details
@ -136,12 +130,6 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
return nil, domain.ErrInvalidWithdrawalAmount return nil, domain.ErrInvalidWithdrawalAmount
} }
// Get user details
// user, err := s.userStore.GetUserByID(ctx, userID)
// if err != nil {
// return nil, fmt.Errorf("failed to get user: %w", err)
// }
// Get user's wallet // Get user's wallet
wallets, err := s.walletStore.GetWalletsByUser(ctx, userID) wallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
if err != nil { if err != nil {
@ -300,7 +288,7 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
// Find payment by reference // Find payment by reference
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference) payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference)
if err != nil { if err != nil {
return ErrPaymentNotFound return domain.ErrPaymentNotFound
} }
if payment.Verified { if payment.Verified {
@ -330,7 +318,7 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
ReferenceNumber: domain.ValidString{ ReferenceNumber: domain.ValidString{
Value: transfer.Reference, Value: transfer.Reference,
}, },
}, fmt.Sprintf("Added %v to wallet using Chapa")); err != nil { }, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err) return fmt.Errorf("failed to credit user wallet: %w", err)
} }
} }
@ -342,7 +330,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
// Find payment by reference // Find payment by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference) transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference)
if err != nil { if err != nil {
return ErrPaymentNotFound return domain.ErrPaymentNotFound
} }
if transfer.Verified { if transfer.Verified {
@ -368,7 +356,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
} else { } else {
_, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{}, _, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{},
domain.TRANSFER_DIRECT, domain.PaymentDetails{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
fmt.Sprintf("Added %v to wallet by system because chapa withdraw is unsuccessful")) fmt.Sprintf("Added %v to wallet by system because chapa withdraw is unsuccessful", transfer.Amount))
if err != nil { if err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err) return fmt.Errorf("failed to credit user wallet: %w", err)
} }

View File

@ -31,14 +31,36 @@ func (s *Service) Delete(ctx context.Context, id int64) error {
return s.repo.DeleteBank(ctx, int(id)) return s.repo.DeleteBank(ctx, int(id))
} }
func (s *Service) List(ctx context.Context) ([]*domain.Bank, error) { func (s *Service) List(
banks, err := s.repo.GetAllBanks(ctx, nil, nil) ctx context.Context,
countryID *int,
isActive *bool,
searchTerm *string,
page int,
pageSize int,
) ([]*domain.Bank, *domain.Pagination, error) {
banks, total, err := s.repo.GetAllBanks(ctx, countryID, isActive, searchTerm, page, pageSize)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
result := make([]*domain.Bank, len(banks)) result := make([]*domain.Bank, len(banks))
for i := range banks { for i := range banks {
result[i] = &banks[i] result[i] = &banks[i]
} }
return result, nil
// Calculate pagination metadata
totalPages := int(total) / pageSize
if int(total)%pageSize != 0 {
totalPages++
}
pagination := &domain.Pagination{
Total: int(total),
TotalPages: totalPages,
CurrentPage: page,
Limit: pageSize,
}
return result, pagination, nil
} }

View File

@ -465,6 +465,11 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
return fmt.Errorf("fetch data: %w", err) return fmt.Errorf("fetch data: %w", err)
} }
// Ensure the reports directory exists
if err := os.MkdirAll("reports", os.ModePerm); err != nil {
return fmt.Errorf("creating reports directory: %w", err)
}
filePath := fmt.Sprintf("reports/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04")) filePath := fmt.Sprintf("reports/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04"))
file, err := os.Create(filePath) file, err := os.Create(filePath)
if err != nil { if err != nil {
@ -476,9 +481,13 @@ 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)"}) if err := writer.Write([]string{"Sports Betting Reports (Periodic)"}); err != nil {
writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"}) return fmt.Errorf("write header: %w", err)
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", data.TotalBets),
fmt.Sprintf("%.2f", data.TotalCashIn), fmt.Sprintf("%.2f", data.TotalCashIn),
@ -487,40 +496,50 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
fmt.Sprintf("%.2f", data.Deposits), fmt.Sprintf("%.2f", data.Deposits),
fmt.Sprintf("%.2f", data.Withdrawals), fmt.Sprintf("%.2f", data.Withdrawals),
fmt.Sprintf("%d", data.TotalTickets), fmt.Sprintf("%d", data.TotalTickets),
}) }); err != nil {
return fmt.Errorf("write summary row: %w", err)
}
writer.Write([]string{}) // Empty line for spacing writer.Write([]string{}) // Empty line
// Virtual Game Summary section // Virtual Game Summary section
writer.Write([]string{"Virtual Game Reports (Periodic)"}) 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{ if err := writer.Write([]string{
row.GameName, row.GameName,
fmt.Sprintf("%d", row.NumBets), fmt.Sprintf("%d", row.NumBets),
fmt.Sprintf("%.2f", row.TotalTransaction), fmt.Sprintf("%.2f", row.TotalTransaction),
}) }); err != nil {
return fmt.Errorf("write virtual game row: %w", err)
}
} }
writer.Write([]string{}) // Empty line writer.Write([]string{}) // Empty line
// Company Reports
writer.Write([]string{"Company Reports (Periodic)"}) writer.Write([]string{"Company Reports (Periodic)"})
writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
for _, cr := range data.CompanyReports { for _, cr := range data.CompanyReports {
writer.Write([]string{ if err := writer.Write([]string{
fmt.Sprintf("%d", cr.CompanyID), fmt.Sprintf("%d", cr.CompanyID),
cr.CompanyName, cr.CompanyName,
fmt.Sprintf("%d", cr.TotalBets), fmt.Sprintf("%d", cr.TotalBets),
fmt.Sprintf("%.2f", cr.TotalCashIn), fmt.Sprintf("%.2f", cr.TotalCashIn),
fmt.Sprintf("%.2f", cr.TotalCashOut), fmt.Sprintf("%.2f", cr.TotalCashOut),
fmt.Sprintf("%.2f", cr.TotalCashBacks), fmt.Sprintf("%.2f", cr.TotalCashBacks),
}) }); err != nil {
return fmt.Errorf("write company row: %w", err)
}
} }
writer.Write([]string{}) // Empty line writer.Write([]string{}) // Empty line
// Branch Reports
writer.Write([]string{"Branch Reports (Periodic)"}) 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 data.BranchReports {
writer.Write([]string{ if err := 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),
@ -528,9 +547,12 @@ 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
var totalBets int64 var totalBets int64
var totalCashIn, totalCashOut, totalCashBacks float64 var totalCashIn, totalCashOut, totalCashBacks float64
for _, cr := range data.CompanyReports { for _, cr := range data.CompanyReports {
@ -540,19 +562,22 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
totalCashBacks += cr.TotalCashBacks totalCashBacks += cr.TotalCashBacks
} }
writer.Write([]string{}) writer.Write([]string{}) // Empty line
writer.Write([]string{"Total Summary"}) writer.Write([]string{"Total Summary"})
writer.Write([]string{"Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) writer.Write([]string{"Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
writer.Write([]string{ if err := writer.Write([]string{
fmt.Sprintf("%d", totalBets), fmt.Sprintf("%d", totalBets),
fmt.Sprintf("%.2f", totalCashIn), fmt.Sprintf("%.2f", totalCashIn),
fmt.Sprintf("%.2f", totalCashOut), fmt.Sprintf("%.2f", totalCashOut),
fmt.Sprintf("%.2f", totalCashBacks), fmt.Sprintf("%.2f", totalCashBacks),
}) }); err != nil {
return fmt.Errorf("write total summary row: %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, period string) (domain.ReportData, error) {
from, to := getTimeRange(period) from, to := getTimeRange(period)
// companyID := int64(0) // companyID := int64(0)

View File

@ -41,14 +41,15 @@ func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
} }
if outcome.OddHeader == "Over" { switch outcome.OddHeader {
case "Over":
if totalGoals > threshold { if totalGoals > threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if totalGoals == threshold { } else if totalGoals == threshold {
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" { case "Under":
if totalGoals < threshold { if totalGoals < threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if totalGoals == threshold { } else if totalGoals == threshold {
@ -91,46 +92,48 @@ func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.Outcom
case domain.OUTCOME_STATUS_PENDING: case domain.OUTCOME_STATUS_PENDING:
return secondOutcome, nil return secondOutcome, nil
case domain.OUTCOME_STATUS_WIN: case domain.OUTCOME_STATUS_WIN:
if secondOutcome == domain.OUTCOME_STATUS_WIN { switch secondOutcome {
case domain.OUTCOME_STATUS_WIN:
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if secondOutcome == domain.OUTCOME_STATUS_LOSS { case domain.OUTCOME_STATUS_LOSS:
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_HALF { case domain.OUTCOME_STATUS_HALF:
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID { case domain.OUTCOME_STATUS_VOID:
return domain.OUTCOME_STATUS_HALF, nil return domain.OUTCOME_STATUS_HALF, nil
} else { default:
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
} }
case domain.OUTCOME_STATUS_LOSS: case domain.OUTCOME_STATUS_LOSS:
if secondOutcome == domain.OUTCOME_STATUS_LOSS || switch secondOutcome {
secondOutcome == domain.OUTCOME_STATUS_WIN || case domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF:
secondOutcome == domain.OUTCOME_STATUS_HALF {
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID { case domain.OUTCOME_STATUS_VOID:
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} else { default:
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
} }
case domain.OUTCOME_STATUS_VOID: case domain.OUTCOME_STATUS_VOID:
if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_LOSS { switch secondOutcome {
case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS:
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID || secondOutcome == domain.OUTCOME_STATUS_HALF { case domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_HALF:
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} else { default:
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
} }
case domain.OUTCOME_STATUS_HALF: case domain.OUTCOME_STATUS_HALF:
if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_HALF { switch secondOutcome {
case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF:
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} else if secondOutcome == domain.OUTCOME_STATUS_LOSS { case domain.OUTCOME_STATUS_LOSS:
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID { case domain.OUTCOME_STATUS_VOID:
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} else { default:
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
} }
@ -168,11 +171,12 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i
} }
adjustedHomeScore := float64(score.Home) adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away) adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" { // Home team switch outcome.OddHeader {
case "1": // Home team
adjustedHomeScore += handicap adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" { // Away team case "2": // Away team
adjustedAwayScore += handicap adjustedAwayScore += handicap
} else { default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
} }
@ -244,7 +248,8 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int })
} }
oddHeader := strings.TrimSpace(outcome.OddHeader) oddHeader := strings.TrimSpace(outcome.OddHeader)
if oddHeader == "Over" { switch oddHeader {
case "Over":
if totalGoals > threshold { if totalGoals > threshold {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil { if err != nil {
@ -261,7 +266,7 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int })
if err != nil { if err != nil {
return domain.OUTCOME_STATUS_ERROR, err return domain.OUTCOME_STATUS_ERROR, err
} }
} else if oddHeader == "Under" { case "Under":
if totalGoals < threshold { if totalGoals < threshold {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil { if err != nil {
@ -280,7 +285,7 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int })
return domain.OUTCOME_STATUS_ERROR, err return domain.OUTCOME_STATUS_ERROR, err
} }
} else { default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: '%s'", oddHeader) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: '%s'", oddHeader)
} }
@ -321,31 +326,33 @@ func evaluateTeamOddEven(outcome domain.BetOutcome, score struct{ Home, Away int
switch outcome.OddHeader { switch outcome.OddHeader {
case "1": case "1":
if outcome.OddHandicap == "Odd" { switch outcome.OddHandicap {
case "Odd":
if score.Home%2 == 1 { if score.Home%2 == 1 {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHandicap == "Even" { case "Even":
if score.Home%2 == 0 { if score.Home%2 == 0 {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else { default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
} }
case "2": case "2":
if outcome.OddHandicap == "Odd" { switch outcome.OddHandicap {
case "Odd":
if score.Away%2 == 1 { if score.Away%2 == 1 {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHandicap == "Even" { case "Even":
if score.Away%2 == 0 { if score.Away%2 == 0 {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else { default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
} }
default: default:
@ -480,17 +487,18 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away
// Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet
totalScore := float64(score.Home + score.Away) totalScore := float64(score.Home + score.Away)
if overUnderStr[0] == "O" { switch overUnderStr[0] {
case "O":
if totalScore > threshold { if totalScore > threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if overUnderStr[0] == "U" { case "U":
if totalScore < threshold { if totalScore < threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if overUnderStr[0] == "E" { case "E":
if totalScore == threshold { if totalScore == threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
@ -633,11 +641,12 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away
scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1] scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1]
var isScorePoints bool var isScorePoints bool
if scoreCheckSplit == "Yes" { switch scoreCheckSplit {
case "Yes":
isScorePoints = true isScorePoints = true
} else if scoreCheckSplit == "No" { case "No":
isScorePoints = false isScorePoints = false
} else { default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
} }
@ -853,15 +862,16 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa
} }
total := float64(score.Home + score.Away) total := float64(score.Home + score.Away)
overUnder := nameSplit[len(nameSplit)-2] overUnder := nameSplit[len(nameSplit)-2]
if overUnder == "Over" { switch overUnder {
case "Over":
if total < threshold { if total < threshold {
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} }
} else if overUnder == "Under" { case "Under":
if total > threshold { if total > threshold {
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} }
} else { default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName)
} }

View File

@ -73,7 +73,7 @@ func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, re
return fmt.Errorf("Outcome has not expired yet") return fmt.Errorf("Outcome has not expired yet")
} }
parseResult, err := s.parseResult(ctx, resultRes, outcome, sportID) parseResult, err := s.parseResult(resultRes, outcome, sportID)
if err != nil { if err != nil {
s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err) s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
@ -595,7 +595,7 @@ func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json.
// Get results for outcome // Get results for outcome
for i, outcome := range outcomes { for i, outcome := range outcomes {
// Parse the result based on sport type // Parse the result based on sport type
parsedResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) parsedResult, err := s.parseResult(result.Results[0], outcome, sportID)
if err != nil { if err != nil {
s.logger.Error("Failed to parse result for outcome", "event_id", outcome.EventID, "error", err) s.logger.Error("Failed to parse result for outcome", "event_id", outcome.EventID, "error", err)
return json.RawMessage{}, nil, fmt.Errorf("failed to parse result for outcome %d: %w", i, err) return json.RawMessage{}, nil, fmt.Errorf("failed to parse result for outcome %d: %w", i, err)
@ -643,7 +643,7 @@ func (s *Service) fetchResult(ctx context.Context, eventID int64) (domain.BaseRe
return resultResp, nil return resultResp, nil
} }
func (s *Service) parseResult(ctx context.Context, resultResp json.RawMessage, outcome domain.BetOutcome, sportID int64) (domain.CreateResult, error) { func (s *Service) parseResult(resultResp json.RawMessage, outcome domain.BetOutcome, sportID int64) (domain.CreateResult, error) {
var result domain.CreateResult var result domain.CreateResult
var err error var err error

View File

@ -34,11 +34,12 @@ func EvaluateNFLSpread(outcome domain.BetOutcome, score struct{ Home, Away int }
adjustedHomeScore := float64(score.Home) adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away) adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" { switch outcome.OddHeader {
case "1":
adjustedHomeScore += handicap adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" { case "2":
adjustedAwayScore += handicap adjustedAwayScore += handicap
} else { default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
} }
@ -63,14 +64,15 @@ func EvaluateNFLTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
} }
if outcome.OddHeader == "Over" { switch outcome.OddHeader {
case "Over":
if totalPoints > threshold { if totalPoints > threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold { } else if totalPoints == threshold {
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" { case "Under":
if totalPoints < threshold { if totalPoints < threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold { } else if totalPoints == threshold {
@ -109,11 +111,12 @@ func EvaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int
adjustedHomeScore := float64(score.Home) adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away) adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" { switch outcome.OddHeader {
case "1":
adjustedHomeScore += handicap adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" { case "2":
adjustedAwayScore += handicap adjustedAwayScore += handicap
} else { default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
} }
@ -139,14 +142,15 @@ func EvaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Awa
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
} }
if outcome.OddHeader == "Over" { switch outcome.OddHeader {
case "Over":
if totalPoints > threshold { if totalPoints > threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold { } else if totalPoints == threshold {
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" { case "Under":
if totalPoints < threshold { if totalPoints < threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold { } else if totalPoints == threshold {
@ -185,11 +189,12 @@ func EvaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away
adjustedHomeScore := float64(score.Home) adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away) adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" { switch outcome.OddHeader {
case "1":
adjustedHomeScore += handicap adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" { case "2":
adjustedAwayScore += handicap adjustedAwayScore += handicap
} else { default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
} }
@ -215,14 +220,15 @@ func EvaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Aw
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
} }
if outcome.OddHeader == "Over" { switch outcome.OddHeader {
case "Over":
if totalRuns > threshold { if totalRuns > threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if totalRuns == threshold { } else if totalRuns == threshold {
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" { case "Under":
if totalRuns < threshold { if totalRuns < threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if totalRuns == threshold { } else if totalRuns == threshold {

View File

@ -107,7 +107,7 @@ func (s *Service) SendTwilioSMSOTP(ctx context.Context, receiverPhone, message s
_, err := client.Api.CreateMessage(params) _, err := client.Api.CreateMessage(params)
if err != nil { if err != nil {
return fmt.Errorf("Error sending SMS message: %s" + err.Error()) return fmt.Errorf("%s", "Error sending SMS message: %s" + err.Error())
} }
return nil return nil

View File

@ -32,4 +32,16 @@ type TransferStore interface {
GetTransferByID(ctx context.Context, id int64) (domain.TransferDetail, error) GetTransferByID(ctx context.Context, id int64) (domain.TransferDetail, error)
UpdateTransferVerification(ctx context.Context, id int64, verified bool) error UpdateTransferVerification(ctx context.Context, id int64, verified bool) error
UpdateTransferStatus(ctx context.Context, id int64, status string) error UpdateTransferStatus(ctx context.Context, id int64, status string) error
// InitiateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error)
// ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error
// RejectTransfer(ctx context.Context, approval domain.ApprovalRequest) error
// GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error)
// GetTransferApprovalHistory(ctx context.Context, transferID int64) ([]domain.TransactionApproval, error)
}
type ApprovalStore interface {
CreateApproval(ctx context.Context, approval domain.TransactionApproval) error
UpdateApprovalStatus(ctx context.Context, approvalID int64, status string, comments string) error
GetApprovalsByTransfer(ctx context.Context, transferID int64) ([]domain.TransactionApproval, error)
GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error)
} }

View File

@ -7,19 +7,24 @@ import (
) )
type Service struct { type Service struct {
// approvalStore ApprovalStore
walletStore WalletStore walletStore WalletStore
transferStore TransferStore transferStore TransferStore
notificationStore notificationservice.NotificationStore notificationStore notificationservice.NotificationStore
notificationSvc *notificationservice.Service notificationSvc *notificationservice.Service
logger *slog.Logger logger *slog.Logger
// userStore user.UserStore
} }
func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger) *Service { func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger) *Service {
return &Service{ return &Service{
walletStore: walletStore, walletStore: walletStore,
transferStore: transferStore, transferStore: transferStore,
// approvalStore: approvalStore,
notificationStore: notificationStore, notificationStore: notificationStore,
notificationSvc: notificationSvc, notificationSvc: notificationSvc,
logger: logger, logger: logger,
// userStore: userStore,
// userStore users
} }
} }

View File

@ -172,3 +172,214 @@ func (s *Service) SendTransferNotification(ctx context.Context, senderWallet dom
return nil return nil
} }
func ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
return nil
}
/////////////////////////////////Transaction Make-Cheker/////////////////////////////////////////////////
// func (s *Service) InitiateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
// // Set transfer as unverified by default
// transfer.Verified = false
// // Create the transfer record
// newTransfer, err := s.transferStore.CreateTransfer(ctx, transfer)
// if err != nil {
// return domain.Transfer{}, err
// }
// // Create approval record
// approval := domain.TransactionApproval{
// TransferID: newTransfer.ID,
// Status: "pending",
// }
// if err := s.approvalStore.CreateApproval(ctx, approval); err != nil {
// return domain.Transfer{}, err
// }
// // Notify approvers
// go s.notifyApprovers(ctx, newTransfer.ID)
// return newTransfer, nil
// }
// func (s *Service) ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
// // Get the transfer
// transfer, err := s.transferStore.GetTransferByID(ctx, approval.TransferID)
// if err != nil {
// return err
// }
// // Only allow approval of pending transfers
// if transfer.Status != "pending" {
// return errors.New("only pending transfers can be approved")
// }
// // Update approval record
// if err := s.approvalStore.UpdateApprovalStatus(ctx, approval.TransferID, "approved", approval.Comments); err != nil {
// return err
// }
// // Execute the actual transfer
// if transfer.Type == domain.WALLET {
// _, err = s.executeWalletTransfer(ctx, transfer)
// return err
// }
// // For other transfer types, implement similar execution logic
// return nil
// }
// func (s *Service) executeWalletTransfer(ctx context.Context, transfer domain.TransferDetail) (domain.Transfer, error) {
// // Original transfer logic from TransferToWallet, but now guaranteed to be approved
// senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value)
// if err != nil {
// return domain.Transfer{}, err
// }
// receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID.Value)
// if err != nil {
// return domain.Transfer{}, err
// }
// // Deduct from sender
// if senderWallet.Balance < transfer.Amount {
// return domain.Transfer{}, ErrBalanceInsufficient
// }
// err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, senderWallet.Balance-transfer.Amount)
// if err != nil {
// return domain.Transfer{}, err
// }
// // Add to receiver
// err = s.walletStore.UpdateBalance(ctx, transfer.ReceiverWalletID.Value, receiverWallet.Balance+transfer.Amount)
// if err != nil {
// return domain.Transfer{}, err
// }
// // Mark transfer as completed
// if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, "completed"); err != nil {
// return domain.Transfer{}, err
// }
// // Send notifications
// go s.SendTransferNotification(ctx, senderWallet, receiverWallet,
// domain.RoleFromString(transfer.SenderWalletID.Value),
// domain.RoleFromString(transfer.ReceiverWalletID.Value),
// transfer.Amount)
// return transfer, nil
// }
// func (s *Service) RejectTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
// // Update approval record
// if err := s.approvalStore.UpdateApprovalStatus(ctx, approval.TransferID, "rejected", approval.Comments); err != nil {
// return err
// }
// // Update transfer status
// if err := s.transferStore.UpdateTransferStatus(ctx, approval.TransferID, "rejected"); err != nil {
// return err
// }
// // Notify the initiator
// go s.notifyInitiator(ctx, approval.TransferID, approval.Comments)
// return nil
// }
// func (s *Service) GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error) {
// return s.approvalStore.GetPendingApprovals(ctx)
// }
// func (s *Service) GetTransferApprovalHistory(ctx context.Context, transferID int64) ([]domain.TransactionApproval, error) {
// return s.approvalStore.GetApprovalsByTransfer(ctx, transferID)
// }
// func (s *Service) notifyApprovers(ctx context.Context, transferID int64) {
// // Get approvers (could be from a config or role-based)
// approvers, err := s.userStore.GetUsersByRole(ctx, "approver")
// if err != nil {
// s.logger.Error("failed to get approvers", zap.Error(err))
// return
// }
// transfer, err := s.transferStore.GetTransferByID(ctx, transferID)
// if err != nil {
// s.logger.Error("failed to get transfer for notification", zap.Error(err))
// return
// }
// for _, approver := range approvers {
// notification := &domain.Notification{
// RecipientID: approver.ID,
// Type: domain.NOTIFICATION_TYPE_APPROVAL_REQUIRED,
// Level: domain.NotificationLevelWarning,
// Reciever: domain.NotificationRecieverSideAdmin,
// DeliveryChannel: domain.DeliveryChannelInApp,
// Payload: domain.NotificationPayload{
// Headline: "Transfer Approval Required",
// Message: fmt.Sprintf("Transfer #%d requires your approval", transfer.ID),
// },
// Priority: 1,
// Metadata: []byte(fmt.Sprintf(`{
// "transfer_id": %d,
// "amount": %d,
// "notification_type": "approval_request"
// }`, transfer.ID, transfer.Amount)),
// }
// if err := s.notificationStore.SendNotification(ctx, notification); err != nil {
// s.logger.Error("failed to send approval notification",
// zap.Int64("approver_id", approver.ID),
// zap.Error(err))
// }
// }
// }
// func (s *Service) notifyInitiator(ctx context.Context, transferID int64, comments string) {
// transfer, err := s.transferStore.GetTransferByID(ctx, transferID)
// if err != nil {
// s.logger.Error("failed to get transfer for notification", zap.Error(err))
// return
// }
// // Determine who initiated the transfer (could be cashier or user)
// recipientID := transfer.CashierID.Value
// if !transfer.CashierID.Valid {
// // If no cashier, assume user initiated
// wallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value)
// if err != nil {
// s.logger.Error("failed to get wallet for notification", zap.Error(err))
// return
// }
// recipientID = wallet.UserID
// }
// notification := &domain.Notification{
// RecipientID: recipientID,
// Type: domain.NOTIFICATION_TYPE_TRANSFER_REJECTED,
// Level: domain.NotificationLevelWarning,
// Reciever: domain.NotificationRecieverSideCustomer,
// DeliveryChannel: domain.DeliveryChannelInApp,
// Payload: domain.NotificationPayload{
// Headline: "Transfer Rejected",
// Message: fmt.Sprintf("Your transfer #%d was rejected. Comments: %s", transfer.ID, comments),
// },
// Priority: 2,
// Metadata: []byte(fmt.Sprintf(`{
// "transfer_id": %d,
// "notification_type": "transfer_rejected"
// }`, transfer.ID)),
// }
// if err := s.notificationStore.SendNotification(ctx, notification); err != nil {
// s.logger.Error("failed to send rejection notification",
// zap.Int64("recipient_id", recipientID),
// zap.Error(err))
// }
// }

View File

@ -1,8 +1,11 @@
package handlers package handlers
import ( import (
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"go.uber.org/zap"
) )
// @Summary Create a new bank // @Summary Create a new bank
@ -120,16 +123,63 @@ func (h *Handler) DeleteBank(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent) return c.SendStatus(fiber.StatusNoContent)
} }
// @Summary List all banks // @Summary List all banks with pagination and filtering
// @Tags Institutions - Banks // @Tags Institutions - Banks
// @Produce json // @Produce json
// @Success 200 {array} domain.Bank // @Param country_id query integer false "Filter by country ID"
// @Param is_active query boolean false "Filter by active status"
// @Param search query string false "Search term for bank name or code"
// @Param page query integer false "Page number" default(1)
// @Param page_size query integer false "Items per page" default(50) maximum(100)
// @Success 200 {object} domain.InstResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/banks [get] // @Router /api/v1/banks [get]
func (h *Handler) ListBanks(c *fiber.Ctx) error { func (h *Handler) ListBanks(c *fiber.Ctx) error {
banks, err := h.instSvc.List(c.Context()) // Parse query parameters
countryID, _ := strconv.Atoi(c.Query("country_id"))
var countryIDPtr *int
if c.Query("country_id") != "" {
countryIDPtr = &countryID
}
isActive, _ := strconv.ParseBool(c.Query("is_active"))
var isActivePtr *bool
if c.Query("is_active") != "" {
isActivePtr = &isActive
}
var searchTermPtr *string
if searchTerm := c.Query("search"); searchTerm != "" {
searchTermPtr = &searchTerm
}
page, _ := strconv.Atoi(c.Query("page", "1"))
pageSize, _ := strconv.Atoi(c.Query("page_size", "50"))
banks, pagination, err := h.instSvc.List(
c.Context(),
countryIDPtr,
isActivePtr,
searchTermPtr,
page,
pageSize,
)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) h.mongoLoggerSvc.Error("failed to list banks",
zap.Error(err),
zap.Any("query_params", c.Queries()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve banks",
Error: err.Error(),
})
} }
return c.JSON(banks)
return c.Status(fiber.StatusOK).JSON(domain.InstResponse{
Message: "Banks retrieved successfully",
Status: "success",
Data: banks,
Pagination: pagination,
})
} }

View File

@ -12,24 +12,87 @@ import (
) )
// GetLogsHandler godoc // GetLogsHandler godoc
// @Summary Retrieve latest application logs // @Summary Retrieve application logs with filtering and pagination
// @Description Fetches the 100 most recent application logs from MongoDB // @Description Fetches application logs from MongoDB with pagination, level filtering, and search
// @Tags Logs // @Tags Logs
// @Produce json // @Produce json
// @Success 200 {array} domain.LogEntry "List of application logs" // @Param level query string false "Filter logs by level (debug, info, warn, error, dpanic, panic, fatal)"
// @Param search query string false "Search term to match against message or fields"
// @Param page query int false "Page number for pagination (default: 1)" default(1)
// @Param limit query int false "Number of items per page (default: 50, max: 100)" default(50)
// @Success 200 {object} domain.LogResponse "Paginated list of application logs"
// @Failure 400 {object} domain.ErrorResponse "Invalid request parameters"
// @Failure 500 {object} domain.ErrorResponse "Internal server error" // @Failure 500 {object} domain.ErrorResponse "Internal server error"
// @Router /api/v1/logs [get] // @Router /api/v1/logs [get]
func GetLogsHandler(appCtx context.Context) fiber.Handler { func GetLogsHandler(appCtx context.Context) fiber.Handler {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error {
// Get query parameters
levelFilter := c.Query("level")
searchTerm := c.Query("search")
page := c.QueryInt("page", 1)
limit := c.QueryInt("limit", 50)
// Validate pagination parameters
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 50
}
// Calculate skip value for pagination
skip := (page - 1) * limit
client, err := mongo.Connect(appCtx, options.Client().ApplyURI(os.Getenv("MONGODB_URL"))) client, err := mongo.Connect(appCtx, options.Client().ApplyURI(os.Getenv("MONGODB_URL")))
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error())
} }
defer client.Disconnect(appCtx)
collection := client.Database("logdb").Collection("applogs") collection := client.Database("logdb").Collection("applogs")
filter := bson.M{}
opts := options.Find().SetSort(bson.D{{Key: "timestamp", Value: -1}}).SetLimit(100)
// Build filter
filter := bson.M{}
// Add level filter if specified
if levelFilter != "" {
validLevels := map[string]bool{
"debug": true,
"info": true,
"warn": true,
"error": true,
"dpanic": true,
"panic": true,
"fatal": true,
}
if !validLevels[levelFilter] {
return fiber.NewError(fiber.StatusBadRequest, "Invalid log level specified")
}
filter["level"] = levelFilter
}
// Add search filter if specified
if searchTerm != "" {
filter["$or"] = []bson.M{
{"message": bson.M{"$regex": searchTerm, "$options": "i"}},
{"fields": bson.M{"$elemMatch": bson.M{"$regex": searchTerm, "$options": "i"}}},
}
}
// Find options with pagination and sorting
opts := options.Find().
SetSort(bson.D{{Key: "timestamp", Value: -1}}).
SetSkip(int64(skip)).
SetLimit(int64(limit))
// Get total count for pagination metadata
total, err := collection.CountDocuments(appCtx, filter)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to count logs: "+err.Error())
}
// Find logs
cursor, err := collection.Find(appCtx, filter, opts) cursor, err := collection.Find(appCtx, filter, opts)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch logs: "+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch logs: "+err.Error())
@ -41,6 +104,24 @@ func GetLogsHandler(appCtx context.Context) fiber.Handler {
return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error())
} }
return c.JSON(logs) // Calculate pagination metadata
totalPages := int(total) / limit
if int(total)%limit != 0 {
totalPages++
}
// Prepare response
response := domain.LogResponse{
Message: "Logs fetched successfully",
Data: logs,
Pagination: domain.Pagination{
Total: int(total),
TotalPages: totalPages,
CurrentPage: page,
Limit: limit,
},
}
return c.JSON(response)
} }
} }

View File

@ -3,13 +3,16 @@ package handlers
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"os" "os"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"go.uber.org/zap"
) )
// GetDashboardReport returns a comprehensive dashboard report // GetDashboardReport returns a comprehensive dashboard report
@ -185,18 +188,45 @@ func (h *Handler) DownloadReportFile(c *fiber.Ctx) error {
// ListReportFiles godoc // ListReportFiles godoc
// @Summary List available report CSV files // @Summary List available report CSV files
// @Description Returns a list of all generated report CSV files available for download // @Description Returns a paginated list of generated report CSV files with search capability
// @Tags Reports // @Tags Reports
// @Produce json // @Produce json
// @Success 200 {object} domain.Response{data=[]string} "List of CSV report filenames" // @Param search query string false "Search term to filter filenames"
// @Param page query int false "Page number (default: 1)" default(1)
// @Param limit query int false "Items per page (default: 20, max: 100)" default(20)
// @Success 200 {object} domain.PaginatedFileResponse "Paginated list of CSV report filenames"
// @Failure 400 {object} domain.ErrorResponse "Invalid pagination parameters"
// @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 := "reports"
searchTerm := c.Query("search")
page := c.QueryInt("page", 1)
limit := c.QueryInt("limit", 20)
// Validate pagination parameters
if page < 1 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid page number",
Error: "Page must be greater than 0",
})
}
if limit < 1 || limit > 100 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid limit value",
Error: "Limit must be between 1 and 100",
})
}
// Create the reports directory if it doesn't exist // Create the reports directory if it doesn't exist
if _, err := os.Stat(reportDir); os.IsNotExist(err) { if _, err := os.Stat(reportDir); os.IsNotExist(err) {
if err := os.MkdirAll(reportDir, os.ModePerm); err != nil { if err := os.MkdirAll(reportDir, os.ModePerm); err != nil {
h.mongoLoggerSvc.Error("failed to create report directory",
zap.Int64("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to create report directory", Message: "Failed to create report directory",
Error: err.Error(), Error: err.Error(),
@ -206,23 +236,74 @@ func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
files, err := os.ReadDir(reportDir) files, err := os.ReadDir(reportDir)
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("failed to read report directory",
zap.Int64("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to read report directory", Message: "Failed to read report directory",
Error: err.Error(), Error: err.Error(),
}) })
} }
var reportFiles []string var allFiles []string
for _, file := range files { for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".csv") { if !file.IsDir() && strings.HasSuffix(file.Name(), ".csv") {
reportFiles = append(reportFiles, file.Name()) // Apply search filter if provided
if searchTerm == "" || strings.Contains(strings.ToLower(file.Name()), strings.ToLower(searchTerm)) {
allFiles = append(allFiles, file.Name())
}
} }
} }
return c.Status(fiber.StatusOK).JSON(domain.Response{ // Sort files by name (descending to show newest first)
StatusCode: 200, sort.Slice(allFiles, func(i, j int) bool {
Message: "Report files retrieved successfully", return allFiles[i] > allFiles[j]
Data: reportFiles, })
// Calculate pagination values
total := len(allFiles)
startIdx := (page - 1) * limit
endIdx := startIdx + limit
// Adjust end index if it exceeds the slice length
if endIdx > total {
endIdx = total
}
// Handle case where start index is beyond available items
if startIdx >= total {
return c.Status(fiber.StatusOK).JSON(domain.PaginatedFileResponse{
Response: domain.Response{
StatusCode: fiber.StatusOK,
Message: "No files found for the requested page",
Success: true, Success: true,
},
Data: []string{},
Pagination: domain.Pagination{
Total: total,
TotalPages: int(math.Ceil(float64(total) / float64(limit))),
CurrentPage: page,
Limit: limit,
},
})
}
paginatedFiles := allFiles[startIdx:endIdx]
return c.Status(fiber.StatusOK).JSON(domain.PaginatedFileResponse{
Response: domain.Response{
StatusCode: fiber.StatusOK,
Message: "Report files retrieved successfully",
Success: true,
},
Data: paginatedFiles,
Pagination: domain.Pagination{
Total: total,
TotalPages: int(math.Ceil(float64(total) / float64(limit))),
CurrentPage: page,
Limit: limit,
},
}) })
} }

View File

@ -18,7 +18,7 @@ import (
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param createBet body domain.ShopBetReq true "create bet" // @Param createBet body domain.ShopBetReq true "create bet"
// @Success 200 {object} TransactionRes // @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /shop/bet [post] // @Router /shop/bet [post]
@ -57,7 +57,7 @@ func (h *Handler) CreateShopBet(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param createBet body domain.CashoutReq true "cashout bet" // @Param createBet body domain.CashoutReq true "cashout bet"
// @Success 200 {object} TransactionRes // @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /shop/bet/{id} [get] // @Router /shop/bet/{id} [get]
@ -91,7 +91,7 @@ func (h *Handler) GetShopBetByBetID(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param cashoutBet body domain.CashoutReq true "cashout bet" // @Param cashoutBet body domain.CashoutReq true "cashout bet"
// @Success 200 {object} TransactionRes // @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /shop/bet/{id}/cashout [post] // @Router /shop/bet/{id}/cashout [post]
@ -139,7 +139,7 @@ func (h *Handler) CashoutBet(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param cashoutBet body domain.CashoutReq true "cashout bet" // @Param cashoutBet body domain.CashoutReq true "cashout bet"
// @Success 200 {object} TransactionRes // @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /shop/cashout [post] // @Router /shop/cashout [post]
@ -185,7 +185,7 @@ func (h *Handler) CashoutByCashoutID(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param createBet body domain.CashoutReq true "cashout bet" // @Param createBet body domain.CashoutReq true "cashout bet"
// @Success 200 {object} TransactionRes // @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /shop/cashout/{id} [get] // @Router /shop/cashout/{id} [get]
@ -262,7 +262,7 @@ func (h *Handler) DepositForCustomer(c *fiber.Ctx) error {
// @Tags transaction // @Tags transaction
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {array} TransactionRes // @Success 200 {array} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /shop/transaction [get] // @Router /shop/transaction [get]
@ -337,7 +337,7 @@ func (h *Handler) GetAllTransactions(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path int true "Transaction ID" // @Param id path int true "Transaction ID"
// @Success 200 {object} TransactionRes // @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /shop/transaction/{id} [get] // @Router /shop/transaction/{id} [get]
@ -366,7 +366,7 @@ func (h *Handler) GetTransactionByID(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path int true "Transaction ID" // @Param id path int true "Transaction ID"
// @Success 200 {object} TransactionRes // @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /shop/transaction/{id}/bet [get] // @Router /shop/transaction/{id}/bet [get]

View File

@ -1,9 +1,13 @@
package handlers package handlers
import ( import (
"encoding/json"
"errors"
"fmt"
"strconv" "strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
"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"
) )
@ -106,66 +110,243 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error {
} }
func (h *Handler) HandleBet(c *fiber.Ctx) error { func (h *Handler) HandleBet(c *fiber.Ctx) error {
var req domain.PopOKBetRequest // Read the raw body to avoid parsing issues
if err := c.BodyParser(&req); err != nil { body := c.Body()
if len(body) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid bet request", Message: "Empty request body",
Error: "Request body cannot be empty",
})
}
// Try to identify the provider based on the request structure
provider, err := identifyBetProvider(body)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Unrecognized request format",
Error: err.Error(), Error: err.Error(),
}) })
// return fiber.NewError(fiber.StatusBadRequest, "Invalid bet request")
} }
resp, _ := h.virtualGameSvc.ProcessBet(c.Context(), &req) switch provider {
// if err != nil { case "veli":
// code := fiber.StatusInternalServerError var req domain.BetRequest
// // if err.Error() == "invalid token" { if err := json.Unmarshal(body, &req); err != nil {
// // code = fiber.StatusUnauthorized return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
// // } else if err.Error() == "insufficient balance" { Message: "Invalid Veli bet request",
// // code = fiber.StatusBadRequest Error: err.Error(),
// // } })
// return fiber.NewError(code, err.Error())
// }
return response.WriteJSON(c, fiber.StatusOK, "Bet processed", resp, nil)
} }
res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req)
if err != nil {
if errors.Is(err, veli.ErrDuplicateTransaction) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
Message: "Duplicate transaction",
Error: "DUPLICATE_TRANSACTION",
})
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Veli bet processing failed",
Error: err.Error(),
})
}
return c.JSON(res)
case "popok":
var req domain.PopOKBetRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid PopOK bet request",
Error: err.Error(),
})
}
resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req)
if err != nil {
code := fiber.StatusInternalServerError
switch err.Error() {
case "invalid token":
code = fiber.StatusUnauthorized
case "insufficient balance":
code = fiber.StatusBadRequest
}
return c.Status(code).JSON(domain.ErrorResponse{
Message: "PopOK bet processing failed",
Error: err.Error(),
})
}
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",
})
}
}
// identifyProvider examines the request body to determine the provider
// WinHandler godoc
// @Summary Handle win callback (Veli or PopOK)
// @Description Processes win callbacks from either Veli or PopOK providers by auto-detecting the format
// @Tags Wins
// @Accept json
// @Produce json
// @Success 200 {object} interface{} "Win processing result"
// @Failure 400 {object} domain.ErrorResponse "Invalid request format"
// @Failure 401 {object} domain.ErrorResponse "Authentication failed"
// @Failure 409 {object} domain.ErrorResponse "Duplicate transaction"
// @Failure 500 {object} domain.ErrorResponse "Internal server error"
// @Router /api/v1/win [post]
func (h *Handler) HandleWin(c *fiber.Ctx) error { func (h *Handler) HandleWin(c *fiber.Ctx) error {
var req domain.PopOKWinRequest // Read the raw body to avoid parsing issues
if err := c.BodyParser(&req); err != nil { body := c.Body()
return fiber.NewError(fiber.StatusBadRequest, "Invalid win request") if len(body) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Empty request body",
Error: "Request body cannot be empty",
})
} }
resp, _ := h.virtualGameSvc.ProcessWin(c.Context(), &req) // Try to identify the provider based on the request structure
// if err != nil { provider, err := identifyWinProvider(body)
// code := fiber.StatusInternalServerError if err != nil {
// if err.Error() == "invalid token" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
// code = fiber.StatusUnauthorized Message: "Unrecognized request format",
// } Error: err.Error(),
// return fiber.NewError(code, err.Error()) })
// } }
return response.WriteJSON(c, fiber.StatusOK, "Win processed", resp, nil) switch provider {
case "veli":
var req domain.WinRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Veli win request",
Error: err.Error(),
})
}
res, err := h.veliVirtualGameSvc.ProcessWin(c.Context(), req)
if err != nil {
if errors.Is(err, veli.ErrDuplicateTransaction) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
Message: "Duplicate transaction",
Error: "DUPLICATE_TRANSACTION",
})
}
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Veli win processing failed",
Error: err.Error(),
})
}
return c.JSON(res)
case "popok":
var req domain.PopOKWinRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid PopOK win request",
Error: err.Error(),
})
}
resp, err := h.virtualGameSvc.ProcessWin(c.Context(), &req)
if err != nil {
code := fiber.StatusInternalServerError
if err.Error() == "invalid token" {
code = fiber.StatusUnauthorized
}
return c.Status(code).JSON(domain.ErrorResponse{
Message: "PopOK win processing failed",
Error: err.Error(),
})
}
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",
})
}
} }
func (h *Handler) HandleCancel(c *fiber.Ctx) error { func (h *Handler) HandleCancel(c *fiber.Ctx) error {
var req domain.PopOKCancelRequest body := c.Body()
if err := c.BodyParser(&req); err != nil { if len(body) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid cancel request") return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Empty request body",
Error: "Request body cannot be empty",
})
} }
resp, _ := h.virtualGameSvc.ProcessCancel(c.Context(), &req) provider, err := identifyCancelProvider(body)
// if err != nil { if err != nil {
// code := fiber.StatusInternalServerError return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
// switch err.Error() { Message: "Unrecognized request format",
// case "invalid token": Error: err.Error(),
// code = fiber.StatusUnauthorized })
// case "original bet not found", "invalid original transaction": }
// code = fiber.StatusBadRequest
// }
// return fiber.NewError(code, err.Error())
// }
return response.WriteJSON(c, fiber.StatusOK, "Cancel processed", resp, nil) switch provider {
case "veli":
var req domain.CancelRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Veli cancel request",
Error: err.Error(),
})
}
res, err := h.veliVirtualGameSvc.ProcessCancel(c.Context(), req)
if err != nil {
if errors.Is(err, veli.ErrDuplicateTransaction) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
Message: "Duplicate transaction",
Error: "DUPLICATE_TRANSACTION",
})
}
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Veli cancel processing failed",
Error: err.Error(),
})
}
return c.JSON(res)
case "popok":
var req domain.PopOKCancelRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid PopOK cancel request",
Error: err.Error(),
})
}
resp, err := h.virtualGameSvc.ProcessCancel(c.Context(), &req)
if err != nil {
code := fiber.StatusInternalServerError
switch err.Error() {
case "invalid token":
code = fiber.StatusUnauthorized
case "original bet not found", "invalid original transaction":
code = fiber.StatusBadRequest
}
return c.Status(code).JSON(domain.ErrorResponse{
Message: "PopOK cancel processing failed",
Error: err.Error(),
})
}
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",
})
}
} }
// GetGameList godoc // GetGameList godoc
@ -176,14 +357,14 @@ func (h *Handler) HandleCancel(c *fiber.Ctx) error {
// @Produce json // @Produce json
// @Param currency query string false "Currency (e.g. USD, ETB)" default(USD) // @Param currency query string false "Currency (e.g. USD, ETB)" default(USD)
// @Success 200 {array} domain.PopOKGame // @Success 200 {array} domain.PopOKGame
// @Failure 502 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /popok/games [get] // @Router /popok/games [get]
func (h *Handler) GetGameList(c *fiber.Ctx) error { func (h *Handler) GetGameList(c *fiber.Ctx) error {
currency := c.Query("currency", "ETB") // fallback default currency := c.Query("currency", "ETB") // fallback default
games, err := h.virtualGameSvc.ListGames(c.Context(), currency) games, err := h.virtualGameSvc.ListGames(c.Context(), currency)
if err != nil { if err != nil {
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Falied to fetch games", Message: "Falied to fetch games",
Error: err.Error(), Error: err.Error(),
}) })
@ -331,3 +512,86 @@ 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) {
// Check for Veli signature fields
var veliCheck struct {
TransactionID string `json:"transaction_id"`
GameID string `json:"game_id"`
}
if json.Unmarshal(body, &veliCheck) == nil {
if veliCheck.TransactionID != "" && veliCheck.GameID != "" {
return "veli", nil
}
}
// Check for PopOK signature fields
var popokCheck struct {
Token string `json:"token"`
PlayerID string `json:"player_id"`
BetAmount float64 `json:"bet_amount"`
}
if json.Unmarshal(body, &popokCheck) == nil {
if popokCheck.Token != "" && popokCheck.PlayerID != "" {
return "popok", nil
}
}
return "", fmt.Errorf("could not identify provider from request structure")
}
// identifyWinProvider examines the request body to determine the provider for win callbacks
func identifyWinProvider(body []byte) (string, error) {
// Check for Veli signature fields
var veliCheck struct {
TransactionID string `json:"transaction_id"`
WinAmount float64 `json:"win_amount"`
}
if json.Unmarshal(body, &veliCheck) == nil {
if veliCheck.TransactionID != "" && veliCheck.WinAmount > 0 {
return "veli", nil
}
}
// Check for PopOK signature fields
var popokCheck struct {
Token string `json:"token"`
PlayerID string `json:"player_id"`
WinAmount float64 `json:"win_amount"`
}
if json.Unmarshal(body, &popokCheck) == nil {
if popokCheck.Token != "" && popokCheck.PlayerID != "" {
return "popok", nil
}
}
return "", fmt.Errorf("could not identify provider from request structure")
}
func identifyCancelProvider(body []byte) (string, error) {
// Check for Veli cancel signature
var veliCheck struct {
TransactionID string `json:"transaction_id"`
OriginalTxID string `json:"original_transaction_id"`
CancelReason string `json:"cancel_reason"`
}
if json.Unmarshal(body, &veliCheck) == nil {
if veliCheck.TransactionID != "" && veliCheck.OriginalTxID != "" {
return "veli", nil
}
}
// Check for PopOK cancel signature
var popokCheck struct {
Token string `json:"token"`
PlayerID string `json:"player_id"`
OriginalTxID string `json:"original_transaction_id"`
}
if json.Unmarshal(body, &popokCheck) == nil {
if popokCheck.Token != "" && popokCheck.PlayerID != "" && popokCheck.OriginalTxID != "" {
return "popok", nil
}
}
return "", fmt.Errorf("could not identify cancel provider from request structure")
}

112
test.html Normal file
View File

@ -0,0 +1,112 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en-US"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en-US"> <![endif]-->
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]-->
<head>
<title>Attention Required! | Cloudflare</title>
<meta charset="UTF-8" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="stylesheet" id="cf_styles-css" href="/cdn-cgi/styles/cf.errors.css" />
<!--[if lt IE 9]>
<link rel="stylesheet" id='cf_styles-ie-css' href="/cdn-cgi/styles/cf.errors.ie.css" />
<![endif]-->
<style>body{margin:0;padding:0}</style>
<script>
if (!navigator.cookieEnabled) {
window.addEventListener('DOMContentLoaded', function () {
var cookieEl = document.getElementById('cookie-alert');
cookieEl.style.display = 'block';
});
}
</script>
</head>
<body>
<div id="cf-wrapper">
<div class="cf-alert cf-alert-error cf-cookie-error" id="cookie-alert" data-translate="enable_cookies">
Please enable cookies.
</div>
<div id="cf-error-details" class="cf-error-details-wrapper">
<div class="cf-wrapper cf-header cf-error-overview">
<h1 data-translate="block_headline">Sorry, you have been blocked</h1>
<h2 class="cf-subheadline">
<span data-translate="unable_to_access">You are unable to access</span> pokgaming.com
</h2>
</div>
<div class="cf-section cf-highlight">
<div class="cf-wrapper">
<div class="cf-screenshot-container cf-screenshot-full">
<span class="cf-no-screenshot error"></span>
</div>
</div>
</div>
<div class="cf-section cf-wrapper">
<div class="cf-columns two">
<div class="cf-column">
<h2 data-translate="blocked_why_headline">Why have I been blocked?</h2>
<p data-translate="blocked_why_detail">
This website is using a security service to protect itself from online attacks.
The action you just performed triggered the security solution.
There are several actions that could trigger this block including submitting a certain word or phrase,
a SQL command or malformed data.
</p>
</div>
<div class="cf-column">
<h2 data-translate="blocked_resolve_headline">What can I do to resolve this?</h2>
<p data-translate="blocked_resolve_detail">
You can email the site owner to let them know you were blocked.
Please include what you were doing when this page came up and the Cloudflare Ray ID
found at the bottom of this page.
</p>
</div>
</div>
</div>
<div class="cf-error-footer cf-wrapper w-240 lg:w-full py-10 sm:py-4 sm:px-8 mx-auto text-center sm:text-left border-solid border-0 border-t border-gray-300">
<p class="text-13">
<span class="cf-footer-item sm:block sm:mb-1">
Cloudflare Ray ID: <strong class="font-semibold">9584c5e88deb3c8f</strong>
</span>
<span class="cf-footer-separator sm:hidden">&bull;</span>
<span id="cf-footer-item-ip" class="cf-footer-item hidden sm:block sm:mb-1">
Your IP:
<button type="button" id="cf-footer-ip-reveal" class="cf-footer-ip-reveal-btn">Click to reveal</button>
<span class="hidden" id="cf-footer-ip">195.201.117.22</span>
</span>
<span class="cf-footer-separator sm:hidden">&bull;</span>
<span class="cf-footer-item sm:block sm:mb-1">
<span>Performance &amp; security by</span>
<a rel="noopener noreferrer" href="https://www.cloudflare.com/5xx-error-landing" id="brand_link" target="_blank">Cloudflare</a>
</span>
</p>
<script>
(function(){
function d(){
var b = document.getElementById("cf-footer-item-ip"),
c = document.getElementById("cf-footer-ip-reveal");
if (b && "classList" in b) {
b.classList.remove("hidden");
c.addEventListener("click", function() {
c.classList.add("hidden");
document.getElementById("cf-footer-ip").classList.remove("hidden");
});
}
}
document.addEventListener && document.addEventListener("DOMContentLoaded", d);
})();
</script>
</div>
</div>
</div>
<script>
window._cf_translation = {};
</script>
</body>
</html>