transaction maker-checker fixes
This commit is contained in:
parent
d5bfe98900
commit
2b9302b10b
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
1730
docs/docs.go
1730
docs/docs.go
File diff suppressed because it is too large
Load Diff
1730
docs/swagger.json
1730
docs/swagger.json
File diff suppressed because it is too large
Load Diff
1173
docs/swagger.yaml
1173
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -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
4
go.mod
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
package domain
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
112
test.html
Normal 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">•</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">•</span>
|
||||
<span class="cf-footer-item sm:block sm:mb-1">
|
||||
<span>Performance & 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>
|
||||
Loading…
Reference in New Issue
Block a user