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)
var notificatioStore notificationservice.NotificationStore
// var userStore user.UserStore
walletSvc := wallet.NewService(
wallet.WalletStore(store),
wallet.TransferStore(store),
notificatioStore,
// userStore,
notificationSvc,
logger,
)

View File

@ -35,6 +35,33 @@ WHERE (
AND (
is_active = sqlc.narg('is_active')
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

View File

@ -66,3 +66,9 @@ SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id,
FROM branches
WHERE wallet_id = $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"
)
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
INSERT INTO banks (
slug,
@ -106,15 +137,34 @@ WHERE (
is_active = $2
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 {
CountryID pgtype.Int4 `json:"country_id"`
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) {
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 {
return nil, err
}

4
go.mod
View File

@ -78,7 +78,7 @@ 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
)
@ -87,6 +87,6 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/redis/go-redis/v9 v9.10.0 // indirect
github.com/redis/go-redis/v9 v9.10.0 // direct
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")
ErrInvalidWithdrawalAmount = errors.New("invalid withdrawal amount")
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

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 PtrInt64(v int64) *int64 { return &v }

View File

@ -19,3 +19,10 @@ type Bank struct {
Currency string `json:"currency"`
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"`
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_ADMIN_ALERT NotificationType = "admin_alert"
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"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
NotificationRecieverSideCashier NotificationRecieverSide = "cashier"

View File

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

View File

@ -25,6 +25,30 @@ const (
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
type PaymentDetails struct {
ReferenceNumber ValidString

View File

@ -13,7 +13,14 @@ import (
type BankRepository interface {
CreateBank(ctx context.Context, bank *domain.Bank) error
GetBankByID(ctx context.Context, id int) (*domain.Bank, error)
GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error)
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
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
}
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{
CountryID: 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 {
params.CountryID = pgtype.Int4{Int32: int32(*countryID), Valid: true}
}
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)
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))
for i, b := range dbBanks {
banks[i] = *mapDBBankToDomain(&b)
}
return banks, nil
return banks, total, nil
}
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
}
func ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
return nil
}

View File

@ -14,12 +14,6 @@ import (
"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 {
transferStore wallet.TransferStore
walletStore wallet.Service
@ -49,7 +43,7 @@ func NewService(
func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
// Validate amount
if amount <= 0 {
return "", ErrInvalidPaymentAmount
return "", domain.ErrInvalidPaymentAmount
}
// Get user details
@ -136,12 +130,6 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
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
wallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
if err != nil {
@ -300,7 +288,7 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
// Find payment by reference
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference)
if err != nil {
return ErrPaymentNotFound
return domain.ErrPaymentNotFound
}
if payment.Verified {
@ -330,7 +318,7 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
ReferenceNumber: domain.ValidString{
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)
}
}
@ -342,7 +330,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
// Find payment by reference
transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference)
if err != nil {
return ErrPaymentNotFound
return domain.ErrPaymentNotFound
}
if transfer.Verified {
@ -368,7 +356,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
} else {
_, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{},
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 {
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))
}
func (s *Service) List(ctx context.Context) ([]*domain.Bank, error) {
banks, err := s.repo.GetAllBanks(ctx, nil, nil)
func (s *Service) List(
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 {
return nil, err
return nil, nil, err
}
result := make([]*domain.Bank, len(banks))
for i := range banks {
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)
}
// 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"))
file, err := os.Create(filePath)
if err != nil {
@ -476,9 +481,13 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
defer writer.Flush()
// Summary section
writer.Write([]string{"Sports Betting Reports (Periodic)"})
writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"})
writer.Write([]string{
if err := writer.Write([]string{"Sports Betting Reports (Periodic)"}); err != nil {
return fmt.Errorf("write header: %w", err)
}
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,
fmt.Sprintf("%d", data.TotalBets),
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.Withdrawals),
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
writer.Write([]string{"Virtual Game Reports (Periodic)"})
writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"})
for _, row := range data.VirtualGameStats {
writer.Write([]string{
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 {
writer.Write([]string{
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"})
for _, br := range data.BranchReports {
writer.Write([]string{
if err := writer.Write([]string{
fmt.Sprintf("%d", br.BranchID),
br.BranchName,
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.TotalCashOut),
fmt.Sprintf("%.2f", br.TotalCashBacks),
})
}); err != nil {
return fmt.Errorf("write branch row: %w", err)
}
}
// Total Summary
var totalBets int64
var totalCashIn, totalCashOut, totalCashBacks float64
for _, cr := range data.CompanyReports {
@ -540,19 +562,22 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
totalCashBacks += cr.TotalCashBacks
}
writer.Write([]string{})
writer.Write([]string{}) // Empty line
writer.Write([]string{"Total Summary"})
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("%.2f", totalCashIn),
fmt.Sprintf("%.2f", totalCashOut),
fmt.Sprintf("%.2f", totalCashBacks),
})
}); err != nil {
return fmt.Errorf("write total summary row: %w", err)
}
return nil
}
func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) {
from, to := getTimeRange(period)
// 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)
}
if outcome.OddHeader == "Over" {
switch outcome.OddHeader {
case "Over":
if totalGoals > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalGoals == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" {
case "Under":
if totalGoals < threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalGoals == threshold {
@ -91,46 +92,48 @@ func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.Outcom
case domain.OUTCOME_STATUS_PENDING:
return secondOutcome, nil
case domain.OUTCOME_STATUS_WIN:
if secondOutcome == domain.OUTCOME_STATUS_WIN {
switch secondOutcome {
case domain.OUTCOME_STATUS_WIN:
return domain.OUTCOME_STATUS_WIN, nil
} else if secondOutcome == domain.OUTCOME_STATUS_LOSS {
case domain.OUTCOME_STATUS_LOSS:
return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_HALF {
case domain.OUTCOME_STATUS_HALF:
return domain.OUTCOME_STATUS_VOID, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
case domain.OUTCOME_STATUS_VOID:
return domain.OUTCOME_STATUS_HALF, nil
} else {
default:
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
}
case domain.OUTCOME_STATUS_LOSS:
if secondOutcome == domain.OUTCOME_STATUS_LOSS ||
secondOutcome == domain.OUTCOME_STATUS_WIN ||
secondOutcome == domain.OUTCOME_STATUS_HALF {
switch secondOutcome {
case domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF:
return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
case domain.OUTCOME_STATUS_VOID:
return domain.OUTCOME_STATUS_VOID, nil
} else {
default:
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
}
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
} 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
} else {
default:
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
}
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
} else if secondOutcome == domain.OUTCOME_STATUS_LOSS {
case domain.OUTCOME_STATUS_LOSS:
return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
case domain.OUTCOME_STATUS_VOID:
return domain.OUTCOME_STATUS_VOID, nil
} else {
default:
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
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)
adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" { // Home team
switch outcome.OddHeader {
case "1": // Home team
adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" { // Away team
case "2": // Away team
adjustedAwayScore += handicap
} else {
default:
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)
if oddHeader == "Over" {
switch oddHeader {
case "Over":
if totalGoals > threshold {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil {
@ -261,7 +266,7 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int })
if err != nil {
return domain.OUTCOME_STATUS_ERROR, err
}
} else if oddHeader == "Under" {
case "Under":
if totalGoals < threshold {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil {
@ -280,7 +285,7 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int })
return domain.OUTCOME_STATUS_ERROR, err
}
} else {
default:
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 {
case "1":
if outcome.OddHandicap == "Odd" {
switch outcome.OddHandicap {
case "Odd":
if score.Home%2 == 1 {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHandicap == "Even" {
case "Even":
if score.Home%2 == 0 {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else {
default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
}
case "2":
if outcome.OddHandicap == "Odd" {
switch outcome.OddHandicap {
case "Odd":
if score.Away%2 == 1 {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHandicap == "Even" {
case "Even":
if score.Away%2 == 0 {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else {
default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
}
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
totalScore := float64(score.Home + score.Away)
if overUnderStr[0] == "O" {
switch overUnderStr[0] {
case "O":
if totalScore > threshold {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if overUnderStr[0] == "U" {
case "U":
if totalScore < threshold {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if overUnderStr[0] == "E" {
case "E":
if totalScore == threshold {
return domain.OUTCOME_STATUS_WIN, nil
}
@ -633,11 +641,12 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away
scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1]
var isScorePoints bool
if scoreCheckSplit == "Yes" {
switch scoreCheckSplit {
case "Yes":
isScorePoints = true
} else if scoreCheckSplit == "No" {
case "No":
isScorePoints = false
} else {
default:
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)
overUnder := nameSplit[len(nameSplit)-2]
if overUnder == "Over" {
switch overUnder {
case "Over":
if total < threshold {
return domain.OUTCOME_STATUS_LOSS, nil
}
} else if overUnder == "Under" {
case "Under":
if total > threshold {
return domain.OUTCOME_STATUS_LOSS, nil
}
} else {
default:
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")
}
parseResult, err := s.parseResult(ctx, resultRes, outcome, sportID)
parseResult, err := s.parseResult(resultRes, outcome, sportID)
if err != nil {
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
for i, outcome := range outcomes {
// 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 {
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)
@ -643,7 +643,7 @@ func (s *Service) fetchResult(ctx context.Context, eventID int64) (domain.BaseRe
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 err error

View File

@ -34,11 +34,12 @@ func EvaluateNFLSpread(outcome domain.BetOutcome, score struct{ Home, Away int }
adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" {
switch outcome.OddHeader {
case "1":
adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" {
case "2":
adjustedAwayScore += handicap
} else {
default:
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)
}
if outcome.OddHeader == "Over" {
switch outcome.OddHeader {
case "Over":
if totalPoints > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" {
case "Under":
if totalPoints < threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold {
@ -109,11 +111,12 @@ func EvaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int
adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" {
switch outcome.OddHeader {
case "1":
adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" {
case "2":
adjustedAwayScore += handicap
} else {
default:
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)
}
if outcome.OddHeader == "Over" {
switch outcome.OddHeader {
case "Over":
if totalPoints > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" {
case "Under":
if totalPoints < threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold {
@ -185,11 +189,12 @@ func EvaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away
adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" {
switch outcome.OddHeader {
case "1":
adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" {
case "2":
adjustedAwayScore += handicap
} else {
default:
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)
}
if outcome.OddHeader == "Over" {
switch outcome.OddHeader {
case "Over":
if totalRuns > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalRuns == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" {
case "Under":
if totalRuns < threshold {
return domain.OUTCOME_STATUS_WIN, nil
} 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)
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

View File

@ -32,4 +32,16 @@ type TransferStore interface {
GetTransferByID(ctx context.Context, id int64) (domain.TransferDetail, error)
UpdateTransferVerification(ctx context.Context, id int64, verified bool) 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 {
// approvalStore ApprovalStore
walletStore WalletStore
transferStore TransferStore
notificationStore notificationservice.NotificationStore
notificationSvc *notificationservice.Service
logger *slog.Logger
// userStore user.UserStore
}
func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger) *Service {
return &Service{
walletStore: walletStore,
transferStore: transferStore,
// approvalStore: approvalStore,
notificationStore: notificationStore,
notificationSvc: notificationSvc,
logger: logger,
// userStore: userStore,
// userStore users
}
}

View File

@ -172,3 +172,214 @@ func (s *Service) SendTransferNotification(ctx context.Context, senderWallet dom
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
import (
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
// @Summary Create a new bank
@ -120,16 +123,63 @@ func (h *Handler) DeleteBank(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
// @Summary List all banks
// @Summary List all banks with pagination and filtering
// @Tags Institutions - Banks
// @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
// @Router /api/v1/banks [get]
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 {
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
// @Summary Retrieve latest application logs
// @Description Fetches the 100 most recent application logs from MongoDB
// @Summary Retrieve application logs with filtering and pagination
// @Description Fetches application logs from MongoDB with pagination, level filtering, and search
// @Tags Logs
// @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"
// @Router /api/v1/logs [get]
func GetLogsHandler(appCtx context.Context) fiber.Handler {
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")))
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error())
}
defer client.Disconnect(appCtx)
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)
if err != nil {
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 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 (
"context"
"fmt"
"math"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
// GetDashboardReport returns a comprehensive dashboard report
@ -185,18 +188,45 @@ func (h *Handler) DownloadReportFile(c *fiber.Ctx) error {
// ListReportFiles godoc
// @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
// @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"
// @Router /api/v1/report-files/list [get]
func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
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
if _, err := os.Stat(reportDir); os.IsNotExist(err) {
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{
Message: "Failed to create report directory",
Error: err.Error(),
@ -206,23 +236,74 @@ func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
files, err := os.ReadDir(reportDir)
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{
Message: "Failed to read report directory",
Error: err.Error(),
})
}
var reportFiles []string
var allFiles []string
for _, file := range files {
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{
StatusCode: 200,
Message: "Report files retrieved successfully",
Data: reportFiles,
// Sort files by name (descending to show newest first)
sort.Slice(allFiles, func(i, j int) bool {
return allFiles[i] > allFiles[j]
})
// 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,
},
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
// @Produce json
// @Param createBet body domain.ShopBetReq true "create bet"
// @Success 200 {object} TransactionRes
// @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /shop/bet [post]
@ -57,7 +57,7 @@ func (h *Handler) CreateShopBet(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param createBet body domain.CashoutReq true "cashout bet"
// @Success 200 {object} TransactionRes
// @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /shop/bet/{id} [get]
@ -91,7 +91,7 @@ func (h *Handler) GetShopBetByBetID(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param cashoutBet body domain.CashoutReq true "cashout bet"
// @Success 200 {object} TransactionRes
// @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /shop/bet/{id}/cashout [post]
@ -139,7 +139,7 @@ func (h *Handler) CashoutBet(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param cashoutBet body domain.CashoutReq true "cashout bet"
// @Success 200 {object} TransactionRes
// @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /shop/cashout [post]
@ -185,7 +185,7 @@ func (h *Handler) CashoutByCashoutID(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param createBet body domain.CashoutReq true "cashout bet"
// @Success 200 {object} TransactionRes
// @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /shop/cashout/{id} [get]
@ -262,7 +262,7 @@ func (h *Handler) DepositForCustomer(c *fiber.Ctx) error {
// @Tags transaction
// @Accept json
// @Produce json
// @Success 200 {array} TransactionRes
// @Success 200 {array} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /shop/transaction [get]
@ -337,7 +337,7 @@ func (h *Handler) GetAllTransactions(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param id path int true "Transaction ID"
// @Success 200 {object} TransactionRes
// @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /shop/transaction/{id} [get]
@ -366,7 +366,7 @@ func (h *Handler) GetTransactionByID(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param id path int true "Transaction ID"
// @Success 200 {object} TransactionRes
// @Success 200 {object} domain.ShopTransactionRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /shop/transaction/{id}/bet [get]

View File

@ -1,9 +1,13 @@
package handlers
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"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/gofiber/fiber/v2"
)
@ -106,66 +110,243 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error {
}
func (h *Handler) HandleBet(c *fiber.Ctx) error {
var req domain.PopOKBetRequest
if err := c.BodyParser(&req); err != nil {
// Read the raw body to avoid parsing issues
body := c.Body()
if len(body) == 0 {
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(),
})
// return fiber.NewError(fiber.StatusBadRequest, "Invalid bet request")
}
resp, _ := h.virtualGameSvc.ProcessBet(c.Context(), &req)
// if err != nil {
// code := fiber.StatusInternalServerError
// // if err.Error() == "invalid token" {
// // code = fiber.StatusUnauthorized
// // } else if err.Error() == "insufficient balance" {
// // code = fiber.StatusBadRequest
// // }
// return fiber.NewError(code, err.Error())
// }
return response.WriteJSON(c, fiber.StatusOK, "Bet processed", resp, nil)
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 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 {
var req domain.PopOKWinRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid win request")
// Read the raw body to avoid parsing issues
body := c.Body()
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)
// if err != nil {
// code := fiber.StatusInternalServerError
// if err.Error() == "invalid token" {
// code = fiber.StatusUnauthorized
// }
// return fiber.NewError(code, err.Error())
// }
// Try to identify the provider based on the request structure
provider, err := identifyWinProvider(body)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Unrecognized request format",
Error: 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 {
var req domain.PopOKCancelRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid cancel request")
body := c.Body()
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.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 fiber.NewError(code, err.Error())
// }
provider, err := identifyCancelProvider(body)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Unrecognized request format",
Error: 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
@ -176,14 +357,14 @@ func (h *Handler) HandleCancel(c *fiber.Ctx) error {
// @Produce json
// @Param currency query string false "Currency (e.g. USD, ETB)" default(USD)
// @Success 200 {array} domain.PopOKGame
// @Failure 502 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /popok/games [get]
func (h *Handler) GetGameList(c *fiber.Ctx) error {
currency := c.Query("currency", "ETB") // fallback default
games, err := h.virtualGameSvc.ListGames(c.Context(), currency)
if err != nil {
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Falied to fetch games",
Error: err.Error(),
})
@ -331,3 +512,86 @@ func (h *Handler) ListFavorites(c *fiber.Ctx) error {
}
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>