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)
|
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
|
||||||
|
|
||||||
var notificatioStore notificationservice.NotificationStore
|
var notificatioStore notificationservice.NotificationStore
|
||||||
|
// var userStore user.UserStore
|
||||||
|
|
||||||
walletSvc := wallet.NewService(
|
walletSvc := wallet.NewService(
|
||||||
wallet.WalletStore(store),
|
wallet.WalletStore(store),
|
||||||
wallet.TransferStore(store),
|
wallet.TransferStore(store),
|
||||||
notificatioStore,
|
notificatioStore,
|
||||||
|
// userStore,
|
||||||
notificationSvc,
|
notificationSvc,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,33 @@ WHERE (
|
||||||
AND (
|
AND (
|
||||||
is_active = sqlc.narg('is_active')
|
is_active = sqlc.narg('is_active')
|
||||||
OR sqlc.narg('is_active') IS NULL
|
OR sqlc.narg('is_active') IS NULL
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
name ILIKE '%' || sqlc.narg('search_term') || '%'
|
||||||
|
OR sqlc.narg('search_term') IS NULL
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
code ILIKE '%' || sqlc.narg('search_term') || '%'
|
||||||
|
OR sqlc.narg('search_term') IS NULL
|
||||||
|
)
|
||||||
|
ORDER BY name ASC
|
||||||
|
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||||
|
|
||||||
|
-- name: CountBanks :one
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM banks
|
||||||
|
WHERE (
|
||||||
|
country_id = $1
|
||||||
|
OR $1 IS NULL
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
is_active = $2
|
||||||
|
OR $2 IS NULL
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
name ILIKE '%' || $3 || '%'
|
||||||
|
OR code ILIKE '%' || $3 || '%'
|
||||||
|
OR $3 IS NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- name: UpdateBank :one
|
-- name: UpdateBank :one
|
||||||
|
|
|
||||||
|
|
@ -66,3 +66,9 @@ SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id,
|
||||||
FROM branches
|
FROM branches
|
||||||
WHERE wallet_id = $1
|
WHERE wallet_id = $1
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- -- name: GetCustomerByWalletID :one
|
||||||
|
-- SELECT id, first_name, last_name, email, phone_number,email_verified,phone_verified,company_id,suspended
|
||||||
|
-- FROM users
|
||||||
|
-- WHERE wallet_id = $1
|
||||||
|
-- LIMIT 1;
|
||||||
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"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CountBanks = `-- name: CountBanks :one
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM banks
|
||||||
|
WHERE (
|
||||||
|
country_id = $1
|
||||||
|
OR $1 IS NULL
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
is_active = $2
|
||||||
|
OR $2 IS NULL
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
name ILIKE '%' || $3 || '%'
|
||||||
|
OR code ILIKE '%' || $3 || '%'
|
||||||
|
OR $3 IS NULL
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountBanksParams struct {
|
||||||
|
CountryID int32 `json:"country_id"`
|
||||||
|
IsActive int32 `json:"is_active"`
|
||||||
|
Column3 pgtype.Text `json:"column_3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountBanks(ctx context.Context, arg CountBanksParams) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountBanks, arg.CountryID, arg.IsActive, arg.Column3)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
const CreateBank = `-- name: CreateBank :one
|
const CreateBank = `-- name: CreateBank :one
|
||||||
INSERT INTO banks (
|
INSERT INTO banks (
|
||||||
slug,
|
slug,
|
||||||
|
|
@ -106,15 +137,34 @@ WHERE (
|
||||||
is_active = $2
|
is_active = $2
|
||||||
OR $2 IS NULL
|
OR $2 IS NULL
|
||||||
)
|
)
|
||||||
|
AND (
|
||||||
|
name ILIKE '%' || $3 || '%'
|
||||||
|
OR $3 IS NULL
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
code ILIKE '%' || $3 || '%'
|
||||||
|
OR $3 IS NULL
|
||||||
|
)
|
||||||
|
ORDER BY name ASC
|
||||||
|
LIMIT $5 OFFSET $4
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetAllBanksParams struct {
|
type GetAllBanksParams struct {
|
||||||
CountryID pgtype.Int4 `json:"country_id"`
|
CountryID pgtype.Int4 `json:"country_id"`
|
||||||
IsActive pgtype.Int4 `json:"is_active"`
|
IsActive pgtype.Int4 `json:"is_active"`
|
||||||
|
SearchTerm pgtype.Text `json:"search_term"`
|
||||||
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetAllBanks(ctx context.Context, arg GetAllBanksParams) ([]Bank, error) {
|
func (q *Queries) GetAllBanks(ctx context.Context, arg GetAllBanksParams) ([]Bank, error) {
|
||||||
rows, err := q.db.Query(ctx, GetAllBanks, arg.CountryID, arg.IsActive)
|
rows, err := q.db.Query(ctx, GetAllBanks,
|
||||||
|
arg.CountryID,
|
||||||
|
arg.IsActive,
|
||||||
|
arg.SearchTerm,
|
||||||
|
arg.Offset,
|
||||||
|
arg.Limit,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -78,7 +78,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-resty/resty/v2 v2.16.5
|
// github.com/go-resty/resty/v2 v2.16.5
|
||||||
github.com/twilio/twilio-go v1.26.3
|
github.com/twilio/twilio-go v1.26.3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -87,6 +87,6 @@ require (
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.10.0 // indirect
|
github.com/redis/go-redis/v9 v9.10.0 // direct
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
@ -9,6 +9,9 @@ var (
|
||||||
ErrInsufficientBalance = errors.New("insufficient balance")
|
ErrInsufficientBalance = errors.New("insufficient balance")
|
||||||
ErrInvalidWithdrawalAmount = errors.New("invalid withdrawal amount")
|
ErrInvalidWithdrawalAmount = errors.New("invalid withdrawal amount")
|
||||||
ErrWithdrawalNotFound = errors.New("withdrawal not found")
|
ErrWithdrawalNotFound = errors.New("withdrawal not found")
|
||||||
|
ErrPaymentNotFound = errors.New("payment not found")
|
||||||
|
ErrPaymentAlreadyExists = errors.New("payment with this reference already exists")
|
||||||
|
ErrInvalidPaymentAmount = errors.New("invalid payment amount")
|
||||||
)
|
)
|
||||||
|
|
||||||
type PaymentStatus string
|
type PaymentStatus string
|
||||||
|
|
|
||||||
|
|
@ -79,5 +79,12 @@ func CalculateWinnings(amount Currency, totalOdds float32) Currency {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Pagination struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
func PtrFloat64(v float64) *float64 { return &v }
|
func PtrFloat64(v float64) *float64 { return &v }
|
||||||
func PtrInt64(v int64) *int64 { return &v }
|
func PtrInt64(v int64) *int64 { return &v }
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,10 @@ type Bank struct {
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
BankLogo string `json:"bank_logo"` // URL or base64
|
BankLogo string `json:"bank_logo"` // URL or base64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InstResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data interface{} `json:"data"` // Changed to interface{} for flexibility
|
||||||
|
Pagination *Pagination `json:"pagination,omitempty"` // Made pointer and optional
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,11 @@ type LogEntry struct {
|
||||||
Service string `json:"service" bson:"service"`
|
Service string `json:"service" bson:"service"`
|
||||||
Env string `json:"env" bson:"env"`
|
Env string `json:"env" bson:"env"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogResponse struct {
|
||||||
|
Message string `json:"message" bson:"message"`
|
||||||
|
Data []LogEntry `json:"data"`
|
||||||
|
Pagination Pagination `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,9 @@ const (
|
||||||
NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success"
|
NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success"
|
||||||
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
|
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
|
||||||
NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
|
NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
|
||||||
|
NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected"
|
||||||
|
NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required"
|
||||||
|
|
||||||
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin"
|
|
||||||
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
||||||
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
||||||
NotificationRecieverSideCashier NotificationRecieverSide = "cashier"
|
NotificationRecieverSideCashier NotificationRecieverSide = "cashier"
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ const (
|
||||||
ReportMonthly ReportFrequency = "monthly"
|
ReportMonthly ReportFrequency = "monthly"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PaginatedFileResponse struct {
|
||||||
|
Response `json:",inline"`
|
||||||
|
Data []string `json:"data"`
|
||||||
|
Pagination Pagination `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
||||||
type ReportRequest struct {
|
type ReportRequest struct {
|
||||||
Frequency ReportFrequency
|
Frequency ReportFrequency
|
||||||
StartDate time.Time
|
StartDate time.Time
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,30 @@ const (
|
||||||
TRANSFER_OTHER PaymentMethod = "other"
|
TRANSFER_OTHER PaymentMethod = "other"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TransactionApproval struct {
|
||||||
|
ID int64
|
||||||
|
TransferID int64
|
||||||
|
ApprovedBy int64 // User ID of approver
|
||||||
|
Status string // "pending", "approved", "rejected"
|
||||||
|
Comments string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApprovalActionApprove ApprovalAction = "approve"
|
||||||
|
ApprovalActionReject ApprovalAction = "reject"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalRequest struct {
|
||||||
|
TransferID int64
|
||||||
|
Action ApprovalAction
|
||||||
|
Comments string
|
||||||
|
ApproverID int64
|
||||||
|
}
|
||||||
|
|
||||||
// Info for the payment providers
|
// Info for the payment providers
|
||||||
type PaymentDetails struct {
|
type PaymentDetails struct {
|
||||||
ReferenceNumber ValidString
|
ReferenceNumber ValidString
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,14 @@ import (
|
||||||
type BankRepository interface {
|
type BankRepository interface {
|
||||||
CreateBank(ctx context.Context, bank *domain.Bank) error
|
CreateBank(ctx context.Context, bank *domain.Bank) error
|
||||||
GetBankByID(ctx context.Context, id int) (*domain.Bank, error)
|
GetBankByID(ctx context.Context, id int) (*domain.Bank, error)
|
||||||
GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error)
|
GetAllBanks(
|
||||||
|
ctx context.Context,
|
||||||
|
countryID *int,
|
||||||
|
isActive *bool,
|
||||||
|
searchTerm *string,
|
||||||
|
page int,
|
||||||
|
pageSize int,
|
||||||
|
) ([]domain.Bank, int64, error)
|
||||||
UpdateBank(ctx context.Context, bank *domain.Bank) error
|
UpdateBank(ctx context.Context, bank *domain.Bank) error
|
||||||
DeleteBank(ctx context.Context, id int) error
|
DeleteBank(ctx context.Context, id int) error
|
||||||
}
|
}
|
||||||
|
|
@ -63,28 +70,72 @@ func (r *BankRepo) GetBankByID(ctx context.Context, id int) (*domain.Bank, error
|
||||||
return mapDBBankToDomain(&dbBank), nil
|
return mapDBBankToDomain(&dbBank), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *BankRepo) GetAllBanks(ctx context.Context, countryID *int, isActive *int) ([]domain.Bank, error) {
|
func (r *BankRepo) GetAllBanks(
|
||||||
|
ctx context.Context,
|
||||||
|
countryID *int,
|
||||||
|
isActive *bool,
|
||||||
|
searchTerm *string,
|
||||||
|
page int,
|
||||||
|
pageSize int,
|
||||||
|
) ([]domain.Bank, int64, error) {
|
||||||
|
// Set default pagination values if not provided
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 50
|
||||||
|
}
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
params := dbgen.GetAllBanksParams{
|
params := dbgen.GetAllBanksParams{
|
||||||
CountryID: pgtype.Int4{},
|
CountryID: pgtype.Int4{},
|
||||||
IsActive: pgtype.Int4{},
|
IsActive: pgtype.Int4{},
|
||||||
|
SearchTerm: pgtype.Text{},
|
||||||
|
Limit: pgtype.Int4{Int32: int32(pageSize), Valid: true},
|
||||||
|
Offset: pgtype.Int4{Int32: int32(offset), Valid: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
if countryID != nil {
|
if countryID != nil {
|
||||||
params.CountryID = pgtype.Int4{Int32: int32(*countryID), Valid: true}
|
params.CountryID = pgtype.Int4{Int32: int32(*countryID), Valid: true}
|
||||||
}
|
}
|
||||||
if isActive != nil {
|
if isActive != nil {
|
||||||
params.IsActive = pgtype.Int4{Int32: int32(*isActive), Valid: true}
|
var activeInt int32
|
||||||
|
if *isActive {
|
||||||
|
activeInt = 1
|
||||||
|
} else {
|
||||||
|
activeInt = 0
|
||||||
|
}
|
||||||
|
params.IsActive = pgtype.Int4{Int32: activeInt, Valid: true}
|
||||||
|
}
|
||||||
|
if searchTerm != nil {
|
||||||
|
params.SearchTerm = pgtype.Text{String: *searchTerm, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get paginated results
|
||||||
dbBanks, err := r.store.queries.GetAllBanks(ctx, params)
|
dbBanks, err := r.store.queries.GetAllBanks(ctx, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count for pagination
|
||||||
|
var countCountryID int32
|
||||||
|
if params.CountryID.Valid {
|
||||||
|
countCountryID = params.CountryID.Int32
|
||||||
|
}
|
||||||
|
total, err := r.store.queries.CountBanks(ctx, dbgen.CountBanksParams{
|
||||||
|
CountryID: countCountryID,
|
||||||
|
IsActive: params.IsActive.Int32,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
banks := make([]domain.Bank, len(dbBanks))
|
banks := make([]domain.Bank, len(dbBanks))
|
||||||
for i, b := range dbBanks {
|
for i, b := range dbBanks {
|
||||||
banks[i] = *mapDBBankToDomain(&b)
|
banks[i] = *mapDBBankToDomain(&b)
|
||||||
}
|
}
|
||||||
return banks, nil
|
|
||||||
|
return banks, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *BankRepo) UpdateBank(ctx context.Context, bank *domain.Bank) error {
|
func (r *BankRepo) UpdateBank(ctx context.Context, bank *domain.Bank) error {
|
||||||
|
|
|
||||||
|
|
@ -159,3 +159,7 @@ func (s *Store) UpdateTransferStatus(ctx context.Context, id int64, status strin
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,6 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
ErrPaymentNotFound = errors.New("payment not found")
|
|
||||||
ErrPaymentAlreadyExists = errors.New("payment with this reference already exists")
|
|
||||||
ErrInvalidPaymentAmount = errors.New("invalid payment amount")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
transferStore wallet.TransferStore
|
transferStore wallet.TransferStore
|
||||||
walletStore wallet.Service
|
walletStore wallet.Service
|
||||||
|
|
@ -49,7 +43,7 @@ func NewService(
|
||||||
func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
|
func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount domain.Currency) (string, error) {
|
||||||
// Validate amount
|
// Validate amount
|
||||||
if amount <= 0 {
|
if amount <= 0 {
|
||||||
return "", ErrInvalidPaymentAmount
|
return "", domain.ErrInvalidPaymentAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user details
|
// Get user details
|
||||||
|
|
@ -136,12 +130,6 @@ func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req doma
|
||||||
return nil, domain.ErrInvalidWithdrawalAmount
|
return nil, domain.ErrInvalidWithdrawalAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user details
|
|
||||||
// user, err := s.userStore.GetUserByID(ctx, userID)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, fmt.Errorf("failed to get user: %w", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Get user's wallet
|
// Get user's wallet
|
||||||
wallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
|
wallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -300,7 +288,7 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
|
||||||
// Find payment by reference
|
// Find payment by reference
|
||||||
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference)
|
payment, err := s.transferStore.GetTransferByReference(ctx, transfer.Reference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrPaymentNotFound
|
return domain.ErrPaymentNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if payment.Verified {
|
if payment.Verified {
|
||||||
|
|
@ -330,7 +318,7 @@ func (s *Service) HandleVerifyDepositWebhook(ctx context.Context, transfer domai
|
||||||
ReferenceNumber: domain.ValidString{
|
ReferenceNumber: domain.ValidString{
|
||||||
Value: transfer.Reference,
|
Value: transfer.Reference,
|
||||||
},
|
},
|
||||||
}, fmt.Sprintf("Added %v to wallet using Chapa")); err != nil {
|
}, fmt.Sprintf("Added %v to wallet using Chapa", payment.Amount)); err != nil {
|
||||||
return fmt.Errorf("failed to credit user wallet: %w", err)
|
return fmt.Errorf("failed to credit user wallet: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -342,7 +330,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
|
||||||
// Find payment by reference
|
// Find payment by reference
|
||||||
transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference)
|
transfer, err := s.transferStore.GetTransferByReference(ctx, payment.Reference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrPaymentNotFound
|
return domain.ErrPaymentNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if transfer.Verified {
|
if transfer.Verified {
|
||||||
|
|
@ -368,7 +356,7 @@ func (s *Service) HandleVerifyWithdrawWebhook(ctx context.Context, payment domai
|
||||||
} else {
|
} else {
|
||||||
_, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{},
|
_, err := s.walletStore.AddToWallet(ctx, transfer.SenderWalletID.Value, transfer.Amount, domain.ValidInt64{},
|
||||||
domain.TRANSFER_DIRECT, domain.PaymentDetails{},
|
domain.TRANSFER_DIRECT, domain.PaymentDetails{},
|
||||||
fmt.Sprintf("Added %v to wallet by system because chapa withdraw is unsuccessful"))
|
fmt.Sprintf("Added %v to wallet by system because chapa withdraw is unsuccessful", transfer.Amount))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to credit user wallet: %w", err)
|
return fmt.Errorf("failed to credit user wallet: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,36 @@ func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||||
return s.repo.DeleteBank(ctx, int(id))
|
return s.repo.DeleteBank(ctx, int(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) List(ctx context.Context) ([]*domain.Bank, error) {
|
func (s *Service) List(
|
||||||
banks, err := s.repo.GetAllBanks(ctx, nil, nil)
|
ctx context.Context,
|
||||||
|
countryID *int,
|
||||||
|
isActive *bool,
|
||||||
|
searchTerm *string,
|
||||||
|
page int,
|
||||||
|
pageSize int,
|
||||||
|
) ([]*domain.Bank, *domain.Pagination, error) {
|
||||||
|
banks, total, err := s.repo.GetAllBanks(ctx, countryID, isActive, searchTerm, page, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]*domain.Bank, len(banks))
|
result := make([]*domain.Bank, len(banks))
|
||||||
for i := range banks {
|
for i := range banks {
|
||||||
result[i] = &banks[i]
|
result[i] = &banks[i]
|
||||||
}
|
}
|
||||||
return result, nil
|
|
||||||
|
// Calculate pagination metadata
|
||||||
|
totalPages := int(total) / pageSize
|
||||||
|
if int(total)%pageSize != 0 {
|
||||||
|
totalPages++
|
||||||
|
}
|
||||||
|
|
||||||
|
pagination := &domain.Pagination{
|
||||||
|
Total: int(total),
|
||||||
|
TotalPages: totalPages,
|
||||||
|
CurrentPage: page,
|
||||||
|
Limit: pageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, pagination, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -465,6 +465,11 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
|
||||||
return fmt.Errorf("fetch data: %w", err)
|
return fmt.Errorf("fetch data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the reports directory exists
|
||||||
|
if err := os.MkdirAll("reports", os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("creating reports directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
filePath := fmt.Sprintf("reports/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04"))
|
filePath := fmt.Sprintf("reports/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04"))
|
||||||
file, err := os.Create(filePath)
|
file, err := os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -476,9 +481,13 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
|
||||||
defer writer.Flush()
|
defer writer.Flush()
|
||||||
|
|
||||||
// Summary section
|
// Summary section
|
||||||
writer.Write([]string{"Sports Betting Reports (Periodic)"})
|
if err := writer.Write([]string{"Sports Betting Reports (Periodic)"}); err != nil {
|
||||||
writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"})
|
return fmt.Errorf("write header: %w", err)
|
||||||
writer.Write([]string{
|
}
|
||||||
|
if err := writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"}); err != nil {
|
||||||
|
return fmt.Errorf("write header row: %w", err)
|
||||||
|
}
|
||||||
|
if err := writer.Write([]string{
|
||||||
period,
|
period,
|
||||||
fmt.Sprintf("%d", data.TotalBets),
|
fmt.Sprintf("%d", data.TotalBets),
|
||||||
fmt.Sprintf("%.2f", data.TotalCashIn),
|
fmt.Sprintf("%.2f", data.TotalCashIn),
|
||||||
|
|
@ -487,40 +496,50 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
|
||||||
fmt.Sprintf("%.2f", data.Deposits),
|
fmt.Sprintf("%.2f", data.Deposits),
|
||||||
fmt.Sprintf("%.2f", data.Withdrawals),
|
fmt.Sprintf("%.2f", data.Withdrawals),
|
||||||
fmt.Sprintf("%d", data.TotalTickets),
|
fmt.Sprintf("%d", data.TotalTickets),
|
||||||
})
|
}); err != nil {
|
||||||
|
return fmt.Errorf("write summary row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
writer.Write([]string{}) // Empty line for spacing
|
writer.Write([]string{}) // Empty line
|
||||||
|
|
||||||
// Virtual Game Summary section
|
// Virtual Game Summary section
|
||||||
writer.Write([]string{"Virtual Game Reports (Periodic)"})
|
writer.Write([]string{"Virtual Game Reports (Periodic)"})
|
||||||
writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"})
|
writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"})
|
||||||
for _, row := range data.VirtualGameStats {
|
for _, row := range data.VirtualGameStats {
|
||||||
writer.Write([]string{
|
if err := writer.Write([]string{
|
||||||
row.GameName,
|
row.GameName,
|
||||||
fmt.Sprintf("%d", row.NumBets),
|
fmt.Sprintf("%d", row.NumBets),
|
||||||
fmt.Sprintf("%.2f", row.TotalTransaction),
|
fmt.Sprintf("%.2f", row.TotalTransaction),
|
||||||
})
|
}); err != nil {
|
||||||
|
return fmt.Errorf("write virtual game row: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.Write([]string{}) // Empty line
|
writer.Write([]string{}) // Empty line
|
||||||
|
|
||||||
|
// Company Reports
|
||||||
writer.Write([]string{"Company Reports (Periodic)"})
|
writer.Write([]string{"Company Reports (Periodic)"})
|
||||||
writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
|
writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
|
||||||
for _, cr := range data.CompanyReports {
|
for _, cr := range data.CompanyReports {
|
||||||
writer.Write([]string{
|
if err := writer.Write([]string{
|
||||||
fmt.Sprintf("%d", cr.CompanyID),
|
fmt.Sprintf("%d", cr.CompanyID),
|
||||||
cr.CompanyName,
|
cr.CompanyName,
|
||||||
fmt.Sprintf("%d", cr.TotalBets),
|
fmt.Sprintf("%d", cr.TotalBets),
|
||||||
fmt.Sprintf("%.2f", cr.TotalCashIn),
|
fmt.Sprintf("%.2f", cr.TotalCashIn),
|
||||||
fmt.Sprintf("%.2f", cr.TotalCashOut),
|
fmt.Sprintf("%.2f", cr.TotalCashOut),
|
||||||
fmt.Sprintf("%.2f", cr.TotalCashBacks),
|
fmt.Sprintf("%.2f", cr.TotalCashBacks),
|
||||||
})
|
}); err != nil {
|
||||||
|
return fmt.Errorf("write company row: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.Write([]string{}) // Empty line
|
writer.Write([]string{}) // Empty line
|
||||||
|
|
||||||
|
// Branch Reports
|
||||||
writer.Write([]string{"Branch Reports (Periodic)"})
|
writer.Write([]string{"Branch Reports (Periodic)"})
|
||||||
writer.Write([]string{"Branch ID", "Branch Name", "Company ID", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
|
writer.Write([]string{"Branch ID", "Branch Name", "Company ID", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
|
||||||
for _, br := range data.BranchReports {
|
for _, br := range data.BranchReports {
|
||||||
writer.Write([]string{
|
if err := writer.Write([]string{
|
||||||
fmt.Sprintf("%d", br.BranchID),
|
fmt.Sprintf("%d", br.BranchID),
|
||||||
br.BranchName,
|
br.BranchName,
|
||||||
fmt.Sprintf("%d", br.CompanyID),
|
fmt.Sprintf("%d", br.CompanyID),
|
||||||
|
|
@ -528,9 +547,12 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
|
||||||
fmt.Sprintf("%.2f", br.TotalCashIn),
|
fmt.Sprintf("%.2f", br.TotalCashIn),
|
||||||
fmt.Sprintf("%.2f", br.TotalCashOut),
|
fmt.Sprintf("%.2f", br.TotalCashOut),
|
||||||
fmt.Sprintf("%.2f", br.TotalCashBacks),
|
fmt.Sprintf("%.2f", br.TotalCashBacks),
|
||||||
})
|
}); err != nil {
|
||||||
|
return fmt.Errorf("write branch row: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Total Summary
|
||||||
var totalBets int64
|
var totalBets int64
|
||||||
var totalCashIn, totalCashOut, totalCashBacks float64
|
var totalCashIn, totalCashOut, totalCashBacks float64
|
||||||
for _, cr := range data.CompanyReports {
|
for _, cr := range data.CompanyReports {
|
||||||
|
|
@ -540,19 +562,22 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
|
||||||
totalCashBacks += cr.TotalCashBacks
|
totalCashBacks += cr.TotalCashBacks
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.Write([]string{})
|
writer.Write([]string{}) // Empty line
|
||||||
writer.Write([]string{"Total Summary"})
|
writer.Write([]string{"Total Summary"})
|
||||||
writer.Write([]string{"Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
|
writer.Write([]string{"Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
|
||||||
writer.Write([]string{
|
if err := writer.Write([]string{
|
||||||
fmt.Sprintf("%d", totalBets),
|
fmt.Sprintf("%d", totalBets),
|
||||||
fmt.Sprintf("%.2f", totalCashIn),
|
fmt.Sprintf("%.2f", totalCashIn),
|
||||||
fmt.Sprintf("%.2f", totalCashOut),
|
fmt.Sprintf("%.2f", totalCashOut),
|
||||||
fmt.Sprintf("%.2f", totalCashBacks),
|
fmt.Sprintf("%.2f", totalCashBacks),
|
||||||
})
|
}); err != nil {
|
||||||
|
return fmt.Errorf("write total summary row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) {
|
func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) {
|
||||||
from, to := getTimeRange(period)
|
from, to := getTimeRange(period)
|
||||||
// companyID := int64(0)
|
// companyID := int64(0)
|
||||||
|
|
|
||||||
|
|
@ -41,14 +41,15 @@ func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if outcome.OddHeader == "Over" {
|
switch outcome.OddHeader {
|
||||||
|
case "Over":
|
||||||
if totalGoals > threshold {
|
if totalGoals > threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
} else if totalGoals == threshold {
|
} else if totalGoals == threshold {
|
||||||
return domain.OUTCOME_STATUS_VOID, nil
|
return domain.OUTCOME_STATUS_VOID, nil
|
||||||
}
|
}
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if outcome.OddHeader == "Under" {
|
case "Under":
|
||||||
if totalGoals < threshold {
|
if totalGoals < threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
} else if totalGoals == threshold {
|
} else if totalGoals == threshold {
|
||||||
|
|
@ -91,46 +92,48 @@ func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.Outcom
|
||||||
case domain.OUTCOME_STATUS_PENDING:
|
case domain.OUTCOME_STATUS_PENDING:
|
||||||
return secondOutcome, nil
|
return secondOutcome, nil
|
||||||
case domain.OUTCOME_STATUS_WIN:
|
case domain.OUTCOME_STATUS_WIN:
|
||||||
if secondOutcome == domain.OUTCOME_STATUS_WIN {
|
switch secondOutcome {
|
||||||
|
case domain.OUTCOME_STATUS_WIN:
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
} else if secondOutcome == domain.OUTCOME_STATUS_LOSS {
|
case domain.OUTCOME_STATUS_LOSS:
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if secondOutcome == domain.OUTCOME_STATUS_HALF {
|
case domain.OUTCOME_STATUS_HALF:
|
||||||
return domain.OUTCOME_STATUS_VOID, nil
|
return domain.OUTCOME_STATUS_VOID, nil
|
||||||
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
|
case domain.OUTCOME_STATUS_VOID:
|
||||||
return domain.OUTCOME_STATUS_HALF, nil
|
return domain.OUTCOME_STATUS_HALF, nil
|
||||||
} else {
|
default:
|
||||||
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
||||||
}
|
}
|
||||||
case domain.OUTCOME_STATUS_LOSS:
|
case domain.OUTCOME_STATUS_LOSS:
|
||||||
if secondOutcome == domain.OUTCOME_STATUS_LOSS ||
|
switch secondOutcome {
|
||||||
secondOutcome == domain.OUTCOME_STATUS_WIN ||
|
case domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF:
|
||||||
secondOutcome == domain.OUTCOME_STATUS_HALF {
|
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
|
case domain.OUTCOME_STATUS_VOID:
|
||||||
return domain.OUTCOME_STATUS_VOID, nil
|
return domain.OUTCOME_STATUS_VOID, nil
|
||||||
} else {
|
default:
|
||||||
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
||||||
}
|
}
|
||||||
case domain.OUTCOME_STATUS_VOID:
|
case domain.OUTCOME_STATUS_VOID:
|
||||||
if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_LOSS {
|
switch secondOutcome {
|
||||||
|
case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS:
|
||||||
return domain.OUTCOME_STATUS_VOID, nil
|
return domain.OUTCOME_STATUS_VOID, nil
|
||||||
} else if secondOutcome == domain.OUTCOME_STATUS_VOID || secondOutcome == domain.OUTCOME_STATUS_HALF {
|
case domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_HALF:
|
||||||
return domain.OUTCOME_STATUS_VOID, nil
|
return domain.OUTCOME_STATUS_VOID, nil
|
||||||
} else {
|
default:
|
||||||
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
||||||
}
|
}
|
||||||
case domain.OUTCOME_STATUS_HALF:
|
case domain.OUTCOME_STATUS_HALF:
|
||||||
if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_HALF {
|
switch secondOutcome {
|
||||||
|
case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF:
|
||||||
return domain.OUTCOME_STATUS_VOID, nil
|
return domain.OUTCOME_STATUS_VOID, nil
|
||||||
} else if secondOutcome == domain.OUTCOME_STATUS_LOSS {
|
case domain.OUTCOME_STATUS_LOSS:
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
|
case domain.OUTCOME_STATUS_VOID:
|
||||||
return domain.OUTCOME_STATUS_VOID, nil
|
return domain.OUTCOME_STATUS_VOID, nil
|
||||||
} else {
|
default:
|
||||||
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
|
||||||
}
|
}
|
||||||
|
|
@ -168,11 +171,12 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i
|
||||||
}
|
}
|
||||||
adjustedHomeScore := float64(score.Home)
|
adjustedHomeScore := float64(score.Home)
|
||||||
adjustedAwayScore := float64(score.Away)
|
adjustedAwayScore := float64(score.Away)
|
||||||
if outcome.OddHeader == "1" { // Home team
|
switch outcome.OddHeader {
|
||||||
|
case "1": // Home team
|
||||||
adjustedHomeScore += handicap
|
adjustedHomeScore += handicap
|
||||||
} else if outcome.OddHeader == "2" { // Away team
|
case "2": // Away team
|
||||||
adjustedAwayScore += handicap
|
adjustedAwayScore += handicap
|
||||||
} else {
|
default:
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,7 +248,8 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int })
|
||||||
}
|
}
|
||||||
|
|
||||||
oddHeader := strings.TrimSpace(outcome.OddHeader)
|
oddHeader := strings.TrimSpace(outcome.OddHeader)
|
||||||
if oddHeader == "Over" {
|
switch oddHeader {
|
||||||
|
case "Over":
|
||||||
if totalGoals > threshold {
|
if totalGoals > threshold {
|
||||||
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -261,7 +266,7 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.OUTCOME_STATUS_ERROR, err
|
return domain.OUTCOME_STATUS_ERROR, err
|
||||||
}
|
}
|
||||||
} else if oddHeader == "Under" {
|
case "Under":
|
||||||
if totalGoals < threshold {
|
if totalGoals < threshold {
|
||||||
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
|
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -280,7 +285,7 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int })
|
||||||
return domain.OUTCOME_STATUS_ERROR, err
|
return domain.OUTCOME_STATUS_ERROR, err
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
default:
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: '%s'", oddHeader)
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: '%s'", oddHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,31 +326,33 @@ func evaluateTeamOddEven(outcome domain.BetOutcome, score struct{ Home, Away int
|
||||||
|
|
||||||
switch outcome.OddHeader {
|
switch outcome.OddHeader {
|
||||||
case "1":
|
case "1":
|
||||||
if outcome.OddHandicap == "Odd" {
|
switch outcome.OddHandicap {
|
||||||
|
case "Odd":
|
||||||
if score.Home%2 == 1 {
|
if score.Home%2 == 1 {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
}
|
}
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if outcome.OddHandicap == "Even" {
|
case "Even":
|
||||||
if score.Home%2 == 0 {
|
if score.Home%2 == 0 {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
}
|
}
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else {
|
default:
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
|
||||||
}
|
}
|
||||||
case "2":
|
case "2":
|
||||||
if outcome.OddHandicap == "Odd" {
|
switch outcome.OddHandicap {
|
||||||
|
case "Odd":
|
||||||
if score.Away%2 == 1 {
|
if score.Away%2 == 1 {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
}
|
}
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if outcome.OddHandicap == "Even" {
|
case "Even":
|
||||||
if score.Away%2 == 0 {
|
if score.Away%2 == 0 {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
}
|
}
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else {
|
default:
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
@ -480,17 +487,18 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away
|
||||||
// Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet
|
// Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet
|
||||||
totalScore := float64(score.Home + score.Away)
|
totalScore := float64(score.Home + score.Away)
|
||||||
|
|
||||||
if overUnderStr[0] == "O" {
|
switch overUnderStr[0] {
|
||||||
|
case "O":
|
||||||
if totalScore > threshold {
|
if totalScore > threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
}
|
}
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if overUnderStr[0] == "U" {
|
case "U":
|
||||||
if totalScore < threshold {
|
if totalScore < threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
}
|
}
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if overUnderStr[0] == "E" {
|
case "E":
|
||||||
if totalScore == threshold {
|
if totalScore == threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
}
|
}
|
||||||
|
|
@ -633,11 +641,12 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away
|
||||||
|
|
||||||
scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1]
|
scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1]
|
||||||
var isScorePoints bool
|
var isScorePoints bool
|
||||||
if scoreCheckSplit == "Yes" {
|
switch scoreCheckSplit {
|
||||||
|
case "Yes":
|
||||||
isScorePoints = true
|
isScorePoints = true
|
||||||
} else if scoreCheckSplit == "No" {
|
case "No":
|
||||||
isScorePoints = false
|
isScorePoints = false
|
||||||
} else {
|
default:
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -853,15 +862,16 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa
|
||||||
}
|
}
|
||||||
total := float64(score.Home + score.Away)
|
total := float64(score.Home + score.Away)
|
||||||
overUnder := nameSplit[len(nameSplit)-2]
|
overUnder := nameSplit[len(nameSplit)-2]
|
||||||
if overUnder == "Over" {
|
switch overUnder {
|
||||||
|
case "Over":
|
||||||
if total < threshold {
|
if total < threshold {
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
}
|
}
|
||||||
} else if overUnder == "Under" {
|
case "Under":
|
||||||
if total > threshold {
|
if total > threshold {
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName)
|
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, re
|
||||||
return fmt.Errorf("Outcome has not expired yet")
|
return fmt.Errorf("Outcome has not expired yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
parseResult, err := s.parseResult(ctx, resultRes, outcome, sportID)
|
parseResult, err := s.parseResult(resultRes, outcome, sportID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
|
s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err)
|
||||||
|
|
@ -595,7 +595,7 @@ func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json.
|
||||||
// Get results for outcome
|
// Get results for outcome
|
||||||
for i, outcome := range outcomes {
|
for i, outcome := range outcomes {
|
||||||
// Parse the result based on sport type
|
// Parse the result based on sport type
|
||||||
parsedResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID)
|
parsedResult, err := s.parseResult(result.Results[0], outcome, sportID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to parse result for outcome", "event_id", outcome.EventID, "error", err)
|
s.logger.Error("Failed to parse result for outcome", "event_id", outcome.EventID, "error", err)
|
||||||
return json.RawMessage{}, nil, fmt.Errorf("failed to parse result for outcome %d: %w", i, err)
|
return json.RawMessage{}, nil, fmt.Errorf("failed to parse result for outcome %d: %w", i, err)
|
||||||
|
|
@ -643,7 +643,7 @@ func (s *Service) fetchResult(ctx context.Context, eventID int64) (domain.BaseRe
|
||||||
return resultResp, nil
|
return resultResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) parseResult(ctx context.Context, resultResp json.RawMessage, outcome domain.BetOutcome, sportID int64) (domain.CreateResult, error) {
|
func (s *Service) parseResult(resultResp json.RawMessage, outcome domain.BetOutcome, sportID int64) (domain.CreateResult, error) {
|
||||||
|
|
||||||
var result domain.CreateResult
|
var result domain.CreateResult
|
||||||
var err error
|
var err error
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,12 @@ func EvaluateNFLSpread(outcome domain.BetOutcome, score struct{ Home, Away int }
|
||||||
adjustedHomeScore := float64(score.Home)
|
adjustedHomeScore := float64(score.Home)
|
||||||
adjustedAwayScore := float64(score.Away)
|
adjustedAwayScore := float64(score.Away)
|
||||||
|
|
||||||
if outcome.OddHeader == "1" {
|
switch outcome.OddHeader {
|
||||||
|
case "1":
|
||||||
adjustedHomeScore += handicap
|
adjustedHomeScore += handicap
|
||||||
} else if outcome.OddHeader == "2" {
|
case "2":
|
||||||
adjustedAwayScore += handicap
|
adjustedAwayScore += handicap
|
||||||
} else {
|
default:
|
||||||
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,14 +64,15 @@ func EvaluateNFLTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away
|
||||||
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if outcome.OddHeader == "Over" {
|
switch outcome.OddHeader {
|
||||||
|
case "Over":
|
||||||
if totalPoints > threshold {
|
if totalPoints > threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
} else if totalPoints == threshold {
|
} else if totalPoints == threshold {
|
||||||
return domain.OUTCOME_STATUS_VOID, nil
|
return domain.OUTCOME_STATUS_VOID, nil
|
||||||
}
|
}
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if outcome.OddHeader == "Under" {
|
case "Under":
|
||||||
if totalPoints < threshold {
|
if totalPoints < threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
} else if totalPoints == threshold {
|
} else if totalPoints == threshold {
|
||||||
|
|
@ -109,11 +111,12 @@ func EvaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int
|
||||||
adjustedHomeScore := float64(score.Home)
|
adjustedHomeScore := float64(score.Home)
|
||||||
adjustedAwayScore := float64(score.Away)
|
adjustedAwayScore := float64(score.Away)
|
||||||
|
|
||||||
if outcome.OddHeader == "1" {
|
switch outcome.OddHeader {
|
||||||
|
case "1":
|
||||||
adjustedHomeScore += handicap
|
adjustedHomeScore += handicap
|
||||||
} else if outcome.OddHeader == "2" {
|
case "2":
|
||||||
adjustedAwayScore += handicap
|
adjustedAwayScore += handicap
|
||||||
} else {
|
default:
|
||||||
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,14 +142,15 @@ func EvaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Awa
|
||||||
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if outcome.OddHeader == "Over" {
|
switch outcome.OddHeader {
|
||||||
|
case "Over":
|
||||||
if totalPoints > threshold {
|
if totalPoints > threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
} else if totalPoints == threshold {
|
} else if totalPoints == threshold {
|
||||||
return domain.OUTCOME_STATUS_VOID, nil
|
return domain.OUTCOME_STATUS_VOID, nil
|
||||||
}
|
}
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if outcome.OddHeader == "Under" {
|
case "Under":
|
||||||
if totalPoints < threshold {
|
if totalPoints < threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
} else if totalPoints == threshold {
|
} else if totalPoints == threshold {
|
||||||
|
|
@ -185,11 +189,12 @@ func EvaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away
|
||||||
adjustedHomeScore := float64(score.Home)
|
adjustedHomeScore := float64(score.Home)
|
||||||
adjustedAwayScore := float64(score.Away)
|
adjustedAwayScore := float64(score.Away)
|
||||||
|
|
||||||
if outcome.OddHeader == "1" {
|
switch outcome.OddHeader {
|
||||||
|
case "1":
|
||||||
adjustedHomeScore += handicap
|
adjustedHomeScore += handicap
|
||||||
} else if outcome.OddHeader == "2" {
|
case "2":
|
||||||
adjustedAwayScore += handicap
|
adjustedAwayScore += handicap
|
||||||
} else {
|
default:
|
||||||
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,14 +220,15 @@ func EvaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Aw
|
||||||
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if outcome.OddHeader == "Over" {
|
switch outcome.OddHeader {
|
||||||
|
case "Over":
|
||||||
if totalRuns > threshold {
|
if totalRuns > threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
} else if totalRuns == threshold {
|
} else if totalRuns == threshold {
|
||||||
return domain.OUTCOME_STATUS_VOID, nil
|
return domain.OUTCOME_STATUS_VOID, nil
|
||||||
}
|
}
|
||||||
return domain.OUTCOME_STATUS_LOSS, nil
|
return domain.OUTCOME_STATUS_LOSS, nil
|
||||||
} else if outcome.OddHeader == "Under" {
|
case "Under":
|
||||||
if totalRuns < threshold {
|
if totalRuns < threshold {
|
||||||
return domain.OUTCOME_STATUS_WIN, nil
|
return domain.OUTCOME_STATUS_WIN, nil
|
||||||
} else if totalRuns == threshold {
|
} else if totalRuns == threshold {
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ func (s *Service) SendTwilioSMSOTP(ctx context.Context, receiverPhone, message s
|
||||||
|
|
||||||
_, err := client.Api.CreateMessage(params)
|
_, err := client.Api.CreateMessage(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error sending SMS message: %s" + err.Error())
|
return fmt.Errorf("%s", "Error sending SMS message: %s" + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,16 @@ type TransferStore interface {
|
||||||
GetTransferByID(ctx context.Context, id int64) (domain.TransferDetail, error)
|
GetTransferByID(ctx context.Context, id int64) (domain.TransferDetail, error)
|
||||||
UpdateTransferVerification(ctx context.Context, id int64, verified bool) error
|
UpdateTransferVerification(ctx context.Context, id int64, verified bool) error
|
||||||
UpdateTransferStatus(ctx context.Context, id int64, status string) error
|
UpdateTransferStatus(ctx context.Context, id int64, status string) error
|
||||||
|
// InitiateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error)
|
||||||
|
// ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error
|
||||||
|
// RejectTransfer(ctx context.Context, approval domain.ApprovalRequest) error
|
||||||
|
// GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error)
|
||||||
|
// GetTransferApprovalHistory(ctx context.Context, transferID int64) ([]domain.TransactionApproval, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalStore interface {
|
||||||
|
CreateApproval(ctx context.Context, approval domain.TransactionApproval) error
|
||||||
|
UpdateApprovalStatus(ctx context.Context, approvalID int64, status string, comments string) error
|
||||||
|
GetApprovalsByTransfer(ctx context.Context, transferID int64) ([]domain.TransactionApproval, error)
|
||||||
|
GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,24 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
// approvalStore ApprovalStore
|
||||||
walletStore WalletStore
|
walletStore WalletStore
|
||||||
transferStore TransferStore
|
transferStore TransferStore
|
||||||
notificationStore notificationservice.NotificationStore
|
notificationStore notificationservice.NotificationStore
|
||||||
notificationSvc *notificationservice.Service
|
notificationSvc *notificationservice.Service
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
// userStore user.UserStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger) *Service {
|
func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
walletStore: walletStore,
|
walletStore: walletStore,
|
||||||
transferStore: transferStore,
|
transferStore: transferStore,
|
||||||
|
// approvalStore: approvalStore,
|
||||||
notificationStore: notificationStore,
|
notificationStore: notificationStore,
|
||||||
notificationSvc: notificationSvc,
|
notificationSvc: notificationSvc,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
// userStore: userStore,
|
||||||
|
// userStore users
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -172,3 +172,214 @@ func (s *Service) SendTransferNotification(ctx context.Context, senderWallet dom
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////Transaction Make-Cheker/////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// func (s *Service) InitiateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
|
||||||
|
// // Set transfer as unverified by default
|
||||||
|
// transfer.Verified = false
|
||||||
|
|
||||||
|
// // Create the transfer record
|
||||||
|
// newTransfer, err := s.transferStore.CreateTransfer(ctx, transfer)
|
||||||
|
// if err != nil {
|
||||||
|
// return domain.Transfer{}, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Create approval record
|
||||||
|
// approval := domain.TransactionApproval{
|
||||||
|
// TransferID: newTransfer.ID,
|
||||||
|
// Status: "pending",
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := s.approvalStore.CreateApproval(ctx, approval); err != nil {
|
||||||
|
// return domain.Transfer{}, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Notify approvers
|
||||||
|
// go s.notifyApprovers(ctx, newTransfer.ID)
|
||||||
|
|
||||||
|
// return newTransfer, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *Service) ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
|
||||||
|
// // Get the transfer
|
||||||
|
// transfer, err := s.transferStore.GetTransferByID(ctx, approval.TransferID)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Only allow approval of pending transfers
|
||||||
|
// if transfer.Status != "pending" {
|
||||||
|
// return errors.New("only pending transfers can be approved")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Update approval record
|
||||||
|
// if err := s.approvalStore.UpdateApprovalStatus(ctx, approval.TransferID, "approved", approval.Comments); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Execute the actual transfer
|
||||||
|
// if transfer.Type == domain.WALLET {
|
||||||
|
// _, err = s.executeWalletTransfer(ctx, transfer)
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // For other transfer types, implement similar execution logic
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *Service) executeWalletTransfer(ctx context.Context, transfer domain.TransferDetail) (domain.Transfer, error) {
|
||||||
|
// // Original transfer logic from TransferToWallet, but now guaranteed to be approved
|
||||||
|
// senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value)
|
||||||
|
// if err != nil {
|
||||||
|
// return domain.Transfer{}, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID.Value)
|
||||||
|
// if err != nil {
|
||||||
|
// return domain.Transfer{}, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Deduct from sender
|
||||||
|
// if senderWallet.Balance < transfer.Amount {
|
||||||
|
// return domain.Transfer{}, ErrBalanceInsufficient
|
||||||
|
// }
|
||||||
|
|
||||||
|
// err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, senderWallet.Balance-transfer.Amount)
|
||||||
|
// if err != nil {
|
||||||
|
// return domain.Transfer{}, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Add to receiver
|
||||||
|
// err = s.walletStore.UpdateBalance(ctx, transfer.ReceiverWalletID.Value, receiverWallet.Balance+transfer.Amount)
|
||||||
|
// if err != nil {
|
||||||
|
// return domain.Transfer{}, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Mark transfer as completed
|
||||||
|
// if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, "completed"); err != nil {
|
||||||
|
// return domain.Transfer{}, err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Send notifications
|
||||||
|
// go s.SendTransferNotification(ctx, senderWallet, receiverWallet,
|
||||||
|
// domain.RoleFromString(transfer.SenderWalletID.Value),
|
||||||
|
// domain.RoleFromString(transfer.ReceiverWalletID.Value),
|
||||||
|
// transfer.Amount)
|
||||||
|
|
||||||
|
// return transfer, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *Service) RejectTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
|
||||||
|
// // Update approval record
|
||||||
|
// if err := s.approvalStore.UpdateApprovalStatus(ctx, approval.TransferID, "rejected", approval.Comments); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Update transfer status
|
||||||
|
// if err := s.transferStore.UpdateTransferStatus(ctx, approval.TransferID, "rejected"); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Notify the initiator
|
||||||
|
// go s.notifyInitiator(ctx, approval.TransferID, approval.Comments)
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *Service) GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error) {
|
||||||
|
// return s.approvalStore.GetPendingApprovals(ctx)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *Service) GetTransferApprovalHistory(ctx context.Context, transferID int64) ([]domain.TransactionApproval, error) {
|
||||||
|
// return s.approvalStore.GetApprovalsByTransfer(ctx, transferID)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *Service) notifyApprovers(ctx context.Context, transferID int64) {
|
||||||
|
// // Get approvers (could be from a config or role-based)
|
||||||
|
// approvers, err := s.userStore.GetUsersByRole(ctx, "approver")
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get approvers", zap.Error(err))
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// transfer, err := s.transferStore.GetTransferByID(ctx, transferID)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get transfer for notification", zap.Error(err))
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for _, approver := range approvers {
|
||||||
|
// notification := &domain.Notification{
|
||||||
|
// RecipientID: approver.ID,
|
||||||
|
// Type: domain.NOTIFICATION_TYPE_APPROVAL_REQUIRED,
|
||||||
|
// Level: domain.NotificationLevelWarning,
|
||||||
|
// Reciever: domain.NotificationRecieverSideAdmin,
|
||||||
|
// DeliveryChannel: domain.DeliveryChannelInApp,
|
||||||
|
// Payload: domain.NotificationPayload{
|
||||||
|
// Headline: "Transfer Approval Required",
|
||||||
|
// Message: fmt.Sprintf("Transfer #%d requires your approval", transfer.ID),
|
||||||
|
// },
|
||||||
|
// Priority: 1,
|
||||||
|
// Metadata: []byte(fmt.Sprintf(`{
|
||||||
|
// "transfer_id": %d,
|
||||||
|
// "amount": %d,
|
||||||
|
// "notification_type": "approval_request"
|
||||||
|
// }`, transfer.ID, transfer.Amount)),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := s.notificationStore.SendNotification(ctx, notification); err != nil {
|
||||||
|
// s.logger.Error("failed to send approval notification",
|
||||||
|
// zap.Int64("approver_id", approver.ID),
|
||||||
|
// zap.Error(err))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *Service) notifyInitiator(ctx context.Context, transferID int64, comments string) {
|
||||||
|
// transfer, err := s.transferStore.GetTransferByID(ctx, transferID)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get transfer for notification", zap.Error(err))
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Determine who initiated the transfer (could be cashier or user)
|
||||||
|
// recipientID := transfer.CashierID.Value
|
||||||
|
// if !transfer.CashierID.Valid {
|
||||||
|
// // If no cashier, assume user initiated
|
||||||
|
// wallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("failed to get wallet for notification", zap.Error(err))
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// recipientID = wallet.UserID
|
||||||
|
// }
|
||||||
|
|
||||||
|
// notification := &domain.Notification{
|
||||||
|
// RecipientID: recipientID,
|
||||||
|
// Type: domain.NOTIFICATION_TYPE_TRANSFER_REJECTED,
|
||||||
|
// Level: domain.NotificationLevelWarning,
|
||||||
|
// Reciever: domain.NotificationRecieverSideCustomer,
|
||||||
|
// DeliveryChannel: domain.DeliveryChannelInApp,
|
||||||
|
// Payload: domain.NotificationPayload{
|
||||||
|
// Headline: "Transfer Rejected",
|
||||||
|
// Message: fmt.Sprintf("Your transfer #%d was rejected. Comments: %s", transfer.ID, comments),
|
||||||
|
// },
|
||||||
|
// Priority: 2,
|
||||||
|
// Metadata: []byte(fmt.Sprintf(`{
|
||||||
|
// "transfer_id": %d,
|
||||||
|
// "notification_type": "transfer_rejected"
|
||||||
|
// }`, transfer.ID)),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := s.notificationStore.SendNotification(ctx, notification); err != nil {
|
||||||
|
// s.logger.Error("failed to send rejection notification",
|
||||||
|
// zap.Int64("recipient_id", recipientID),
|
||||||
|
// zap.Error(err))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Summary Create a new bank
|
// @Summary Create a new bank
|
||||||
|
|
@ -120,16 +123,63 @@ func (h *Handler) DeleteBank(c *fiber.Ctx) error {
|
||||||
return c.SendStatus(fiber.StatusNoContent)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary List all banks
|
// @Summary List all banks with pagination and filtering
|
||||||
// @Tags Institutions - Banks
|
// @Tags Institutions - Banks
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} domain.Bank
|
// @Param country_id query integer false "Filter by country ID"
|
||||||
|
// @Param is_active query boolean false "Filter by active status"
|
||||||
|
// @Param search query string false "Search term for bank name or code"
|
||||||
|
// @Param page query integer false "Page number" default(1)
|
||||||
|
// @Param page_size query integer false "Items per page" default(50) maximum(100)
|
||||||
|
// @Success 200 {object} domain.InstResponse
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
// @Router /api/v1/banks [get]
|
// @Router /api/v1/banks [get]
|
||||||
func (h *Handler) ListBanks(c *fiber.Ctx) error {
|
func (h *Handler) ListBanks(c *fiber.Ctx) error {
|
||||||
banks, err := h.instSvc.List(c.Context())
|
// Parse query parameters
|
||||||
|
countryID, _ := strconv.Atoi(c.Query("country_id"))
|
||||||
|
var countryIDPtr *int
|
||||||
|
if c.Query("country_id") != "" {
|
||||||
|
countryIDPtr = &countryID
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive, _ := strconv.ParseBool(c.Query("is_active"))
|
||||||
|
var isActivePtr *bool
|
||||||
|
if c.Query("is_active") != "" {
|
||||||
|
isActivePtr = &isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchTermPtr *string
|
||||||
|
if searchTerm := c.Query("search"); searchTerm != "" {
|
||||||
|
searchTermPtr = &searchTerm
|
||||||
|
}
|
||||||
|
|
||||||
|
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.Query("page_size", "50"))
|
||||||
|
|
||||||
|
banks, pagination, err := h.instSvc.List(
|
||||||
|
c.Context(),
|
||||||
|
countryIDPtr,
|
||||||
|
isActivePtr,
|
||||||
|
searchTermPtr,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
h.mongoLoggerSvc.Error("failed to list banks",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Any("query_params", c.Queries()),
|
||||||
|
)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to retrieve banks",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return c.JSON(banks)
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(domain.InstResponse{
|
||||||
|
Message: "Banks retrieved successfully",
|
||||||
|
Status: "success",
|
||||||
|
Data: banks,
|
||||||
|
Pagination: pagination,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,24 +12,87 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetLogsHandler godoc
|
// GetLogsHandler godoc
|
||||||
// @Summary Retrieve latest application logs
|
// @Summary Retrieve application logs with filtering and pagination
|
||||||
// @Description Fetches the 100 most recent application logs from MongoDB
|
// @Description Fetches application logs from MongoDB with pagination, level filtering, and search
|
||||||
// @Tags Logs
|
// @Tags Logs
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} domain.LogEntry "List of application logs"
|
// @Param level query string false "Filter logs by level (debug, info, warn, error, dpanic, panic, fatal)"
|
||||||
|
// @Param search query string false "Search term to match against message or fields"
|
||||||
|
// @Param page query int false "Page number for pagination (default: 1)" default(1)
|
||||||
|
// @Param limit query int false "Number of items per page (default: 50, max: 100)" default(50)
|
||||||
|
// @Success 200 {object} domain.LogResponse "Paginated list of application logs"
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse "Invalid request parameters"
|
||||||
// @Failure 500 {object} domain.ErrorResponse "Internal server error"
|
// @Failure 500 {object} domain.ErrorResponse "Internal server error"
|
||||||
// @Router /api/v1/logs [get]
|
// @Router /api/v1/logs [get]
|
||||||
func GetLogsHandler(appCtx context.Context) fiber.Handler {
|
func GetLogsHandler(appCtx context.Context) fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
|
// Get query parameters
|
||||||
|
levelFilter := c.Query("level")
|
||||||
|
searchTerm := c.Query("search")
|
||||||
|
page := c.QueryInt("page", 1)
|
||||||
|
limit := c.QueryInt("limit", 50)
|
||||||
|
|
||||||
|
// Validate pagination parameters
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit < 1 || limit > 100 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate skip value for pagination
|
||||||
|
skip := (page - 1) * limit
|
||||||
|
|
||||||
client, err := mongo.Connect(appCtx, options.Client().ApplyURI(os.Getenv("MONGODB_URL")))
|
client, err := mongo.Connect(appCtx, options.Client().ApplyURI(os.Getenv("MONGODB_URL")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error())
|
return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error())
|
||||||
}
|
}
|
||||||
|
defer client.Disconnect(appCtx)
|
||||||
|
|
||||||
collection := client.Database("logdb").Collection("applogs")
|
collection := client.Database("logdb").Collection("applogs")
|
||||||
filter := bson.M{}
|
|
||||||
opts := options.Find().SetSort(bson.D{{Key: "timestamp", Value: -1}}).SetLimit(100)
|
|
||||||
|
|
||||||
|
// Build filter
|
||||||
|
filter := bson.M{}
|
||||||
|
|
||||||
|
// Add level filter if specified
|
||||||
|
if levelFilter != "" {
|
||||||
|
validLevels := map[string]bool{
|
||||||
|
"debug": true,
|
||||||
|
"info": true,
|
||||||
|
"warn": true,
|
||||||
|
"error": true,
|
||||||
|
"dpanic": true,
|
||||||
|
"panic": true,
|
||||||
|
"fatal": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validLevels[levelFilter] {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid log level specified")
|
||||||
|
}
|
||||||
|
filter["level"] = levelFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add search filter if specified
|
||||||
|
if searchTerm != "" {
|
||||||
|
filter["$or"] = []bson.M{
|
||||||
|
{"message": bson.M{"$regex": searchTerm, "$options": "i"}},
|
||||||
|
{"fields": bson.M{"$elemMatch": bson.M{"$regex": searchTerm, "$options": "i"}}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find options with pagination and sorting
|
||||||
|
opts := options.Find().
|
||||||
|
SetSort(bson.D{{Key: "timestamp", Value: -1}}).
|
||||||
|
SetSkip(int64(skip)).
|
||||||
|
SetLimit(int64(limit))
|
||||||
|
|
||||||
|
// Get total count for pagination metadata
|
||||||
|
total, err := collection.CountDocuments(appCtx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to count logs: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find logs
|
||||||
cursor, err := collection.Find(appCtx, filter, opts)
|
cursor, err := collection.Find(appCtx, filter, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch logs: "+err.Error())
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch logs: "+err.Error())
|
||||||
|
|
@ -41,6 +104,24 @@ func GetLogsHandler(appCtx context.Context) fiber.Handler {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error())
|
return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(logs)
|
// Calculate pagination metadata
|
||||||
|
totalPages := int(total) / limit
|
||||||
|
if int(total)%limit != 0 {
|
||||||
|
totalPages++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare response
|
||||||
|
response := domain.LogResponse{
|
||||||
|
Message: "Logs fetched successfully",
|
||||||
|
Data: logs,
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Total: int(total),
|
||||||
|
TotalPages: totalPages,
|
||||||
|
CurrentPage: page,
|
||||||
|
Limit: limit,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,16 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetDashboardReport returns a comprehensive dashboard report
|
// GetDashboardReport returns a comprehensive dashboard report
|
||||||
|
|
@ -185,18 +188,45 @@ func (h *Handler) DownloadReportFile(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// ListReportFiles godoc
|
// ListReportFiles godoc
|
||||||
// @Summary List available report CSV files
|
// @Summary List available report CSV files
|
||||||
// @Description Returns a list of all generated report CSV files available for download
|
// @Description Returns a paginated list of generated report CSV files with search capability
|
||||||
// @Tags Reports
|
// @Tags Reports
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} domain.Response{data=[]string} "List of CSV report filenames"
|
// @Param search query string false "Search term to filter filenames"
|
||||||
|
// @Param page query int false "Page number (default: 1)" default(1)
|
||||||
|
// @Param limit query int false "Items per page (default: 20, max: 100)" default(20)
|
||||||
|
// @Success 200 {object} domain.PaginatedFileResponse "Paginated list of CSV report filenames"
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse "Invalid pagination parameters"
|
||||||
// @Failure 500 {object} domain.ErrorResponse "Failed to read report directory"
|
// @Failure 500 {object} domain.ErrorResponse "Failed to read report directory"
|
||||||
// @Router /api/v1/report-files/list [get]
|
// @Router /api/v1/report-files/list [get]
|
||||||
func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
|
func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
|
||||||
reportDir := "reports"
|
reportDir := "reports"
|
||||||
|
searchTerm := c.Query("search")
|
||||||
|
page := c.QueryInt("page", 1)
|
||||||
|
limit := c.QueryInt("limit", 20)
|
||||||
|
|
||||||
|
// Validate pagination parameters
|
||||||
|
if page < 1 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid page number",
|
||||||
|
Error: "Page must be greater than 0",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit < 1 || limit > 100 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid limit value",
|
||||||
|
Error: "Limit must be between 1 and 100",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Create the reports directory if it doesn't exist
|
// Create the reports directory if it doesn't exist
|
||||||
if _, err := os.Stat(reportDir); os.IsNotExist(err) {
|
if _, err := os.Stat(reportDir); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(reportDir, os.ModePerm); err != nil {
|
if err := os.MkdirAll(reportDir, os.ModePerm); err != nil {
|
||||||
|
h.mongoLoggerSvc.Error("failed to create report directory",
|
||||||
|
zap.Int64("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to create report directory",
|
Message: "Failed to create report directory",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
|
|
@ -206,23 +236,74 @@ func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
|
||||||
|
|
||||||
files, err := os.ReadDir(reportDir)
|
files, err := os.ReadDir(reportDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
h.mongoLoggerSvc.Error("failed to read report directory",
|
||||||
|
zap.Int64("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to read report directory",
|
Message: "Failed to read report directory",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var reportFiles []string
|
var allFiles []string
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if !file.IsDir() && strings.HasSuffix(file.Name(), ".csv") {
|
if !file.IsDir() && strings.HasSuffix(file.Name(), ".csv") {
|
||||||
reportFiles = append(reportFiles, file.Name())
|
// Apply search filter if provided
|
||||||
|
if searchTerm == "" || strings.Contains(strings.ToLower(file.Name()), strings.ToLower(searchTerm)) {
|
||||||
|
allFiles = append(allFiles, file.Name())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
// Sort files by name (descending to show newest first)
|
||||||
StatusCode: 200,
|
sort.Slice(allFiles, func(i, j int) bool {
|
||||||
Message: "Report files retrieved successfully",
|
return allFiles[i] > allFiles[j]
|
||||||
Data: reportFiles,
|
})
|
||||||
|
|
||||||
|
// Calculate pagination values
|
||||||
|
total := len(allFiles)
|
||||||
|
startIdx := (page - 1) * limit
|
||||||
|
endIdx := startIdx + limit
|
||||||
|
|
||||||
|
// Adjust end index if it exceeds the slice length
|
||||||
|
if endIdx > total {
|
||||||
|
endIdx = total
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle case where start index is beyond available items
|
||||||
|
if startIdx >= total {
|
||||||
|
return c.Status(fiber.StatusOK).JSON(domain.PaginatedFileResponse{
|
||||||
|
Response: domain.Response{
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
Message: "No files found for the requested page",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
},
|
||||||
|
Data: []string{},
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Total: total,
|
||||||
|
TotalPages: int(math.Ceil(float64(total) / float64(limit))),
|
||||||
|
CurrentPage: page,
|
||||||
|
Limit: limit,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
paginatedFiles := allFiles[startIdx:endIdx]
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(domain.PaginatedFileResponse{
|
||||||
|
Response: domain.Response{
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
Message: "Report files retrieved successfully",
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
Data: paginatedFiles,
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
Total: total,
|
||||||
|
TotalPages: int(math.Ceil(float64(total) / float64(limit))),
|
||||||
|
CurrentPage: page,
|
||||||
|
Limit: limit,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import (
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param createBet body domain.ShopBetReq true "create bet"
|
// @Param createBet body domain.ShopBetReq true "create bet"
|
||||||
// @Success 200 {object} TransactionRes
|
// @Success 200 {object} domain.ShopTransactionRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /shop/bet [post]
|
// @Router /shop/bet [post]
|
||||||
|
|
@ -57,7 +57,7 @@ func (h *Handler) CreateShopBet(c *fiber.Ctx) error {
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param createBet body domain.CashoutReq true "cashout bet"
|
// @Param createBet body domain.CashoutReq true "cashout bet"
|
||||||
// @Success 200 {object} TransactionRes
|
// @Success 200 {object} domain.ShopTransactionRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /shop/bet/{id} [get]
|
// @Router /shop/bet/{id} [get]
|
||||||
|
|
@ -91,7 +91,7 @@ func (h *Handler) GetShopBetByBetID(c *fiber.Ctx) error {
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param cashoutBet body domain.CashoutReq true "cashout bet"
|
// @Param cashoutBet body domain.CashoutReq true "cashout bet"
|
||||||
// @Success 200 {object} TransactionRes
|
// @Success 200 {object} domain.ShopTransactionRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /shop/bet/{id}/cashout [post]
|
// @Router /shop/bet/{id}/cashout [post]
|
||||||
|
|
@ -139,7 +139,7 @@ func (h *Handler) CashoutBet(c *fiber.Ctx) error {
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param cashoutBet body domain.CashoutReq true "cashout bet"
|
// @Param cashoutBet body domain.CashoutReq true "cashout bet"
|
||||||
// @Success 200 {object} TransactionRes
|
// @Success 200 {object} domain.ShopTransactionRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /shop/cashout [post]
|
// @Router /shop/cashout [post]
|
||||||
|
|
@ -185,7 +185,7 @@ func (h *Handler) CashoutByCashoutID(c *fiber.Ctx) error {
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param createBet body domain.CashoutReq true "cashout bet"
|
// @Param createBet body domain.CashoutReq true "cashout bet"
|
||||||
// @Success 200 {object} TransactionRes
|
// @Success 200 {object} domain.ShopTransactionRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /shop/cashout/{id} [get]
|
// @Router /shop/cashout/{id} [get]
|
||||||
|
|
@ -262,7 +262,7 @@ func (h *Handler) DepositForCustomer(c *fiber.Ctx) error {
|
||||||
// @Tags transaction
|
// @Tags transaction
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {array} TransactionRes
|
// @Success 200 {array} domain.ShopTransactionRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /shop/transaction [get]
|
// @Router /shop/transaction [get]
|
||||||
|
|
@ -337,7 +337,7 @@ func (h *Handler) GetAllTransactions(c *fiber.Ctx) error {
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path int true "Transaction ID"
|
// @Param id path int true "Transaction ID"
|
||||||
// @Success 200 {object} TransactionRes
|
// @Success 200 {object} domain.ShopTransactionRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /shop/transaction/{id} [get]
|
// @Router /shop/transaction/{id} [get]
|
||||||
|
|
@ -366,7 +366,7 @@ func (h *Handler) GetTransactionByID(c *fiber.Ctx) error {
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path int true "Transaction ID"
|
// @Param id path int true "Transaction ID"
|
||||||
// @Success 200 {object} TransactionRes
|
// @Success 200 {object} domain.ShopTransactionRes
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /shop/transaction/{id}/bet [get]
|
// @Router /shop/transaction/{id}/bet [get]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
@ -106,66 +110,243 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) HandleBet(c *fiber.Ctx) error {
|
func (h *Handler) HandleBet(c *fiber.Ctx) error {
|
||||||
var req domain.PopOKBetRequest
|
// Read the raw body to avoid parsing issues
|
||||||
if err := c.BodyParser(&req); err != nil {
|
body := c.Body()
|
||||||
|
if len(body) == 0 {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid bet request",
|
Message: "Empty request body",
|
||||||
|
Error: "Request body cannot be empty",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to identify the provider based on the request structure
|
||||||
|
provider, err := identifyBetProvider(body)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Unrecognized request format",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
// return fiber.NewError(fiber.StatusBadRequest, "Invalid bet request")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := h.virtualGameSvc.ProcessBet(c.Context(), &req)
|
switch provider {
|
||||||
// if err != nil {
|
case "veli":
|
||||||
// code := fiber.StatusInternalServerError
|
var req domain.BetRequest
|
||||||
// // if err.Error() == "invalid token" {
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
// // code = fiber.StatusUnauthorized
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
// // } else if err.Error() == "insufficient balance" {
|
Message: "Invalid Veli bet request",
|
||||||
// // code = fiber.StatusBadRequest
|
Error: err.Error(),
|
||||||
// // }
|
})
|
||||||
// return fiber.NewError(code, err.Error())
|
|
||||||
// }
|
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Bet processed", resp, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, veli.ErrDuplicateTransaction) {
|
||||||
|
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Duplicate transaction",
|
||||||
|
Error: "DUPLICATE_TRANSACTION",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Veli bet processing failed",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(res)
|
||||||
|
|
||||||
|
case "popok":
|
||||||
|
var req domain.PopOKBetRequest
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid PopOK bet request",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
code := fiber.StatusInternalServerError
|
||||||
|
switch err.Error() {
|
||||||
|
case "invalid token":
|
||||||
|
code = fiber.StatusUnauthorized
|
||||||
|
case "insufficient balance":
|
||||||
|
code = fiber.StatusBadRequest
|
||||||
|
}
|
||||||
|
return c.Status(code).JSON(domain.ErrorResponse{
|
||||||
|
Message: "PopOK bet processing failed",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(resp)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Unsupported provider",
|
||||||
|
Error: "Request format doesn't match any supported provider",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// identifyProvider examines the request body to determine the provider
|
||||||
|
|
||||||
|
// WinHandler godoc
|
||||||
|
// @Summary Handle win callback (Veli or PopOK)
|
||||||
|
// @Description Processes win callbacks from either Veli or PopOK providers by auto-detecting the format
|
||||||
|
// @Tags Wins
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} interface{} "Win processing result"
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse "Invalid request format"
|
||||||
|
// @Failure 401 {object} domain.ErrorResponse "Authentication failed"
|
||||||
|
// @Failure 409 {object} domain.ErrorResponse "Duplicate transaction"
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/win [post]
|
||||||
func (h *Handler) HandleWin(c *fiber.Ctx) error {
|
func (h *Handler) HandleWin(c *fiber.Ctx) error {
|
||||||
var req domain.PopOKWinRequest
|
// Read the raw body to avoid parsing issues
|
||||||
if err := c.BodyParser(&req); err != nil {
|
body := c.Body()
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid win request")
|
if len(body) == 0 {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Empty request body",
|
||||||
|
Error: "Request body cannot be empty",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := h.virtualGameSvc.ProcessWin(c.Context(), &req)
|
// Try to identify the provider based on the request structure
|
||||||
// if err != nil {
|
provider, err := identifyWinProvider(body)
|
||||||
// code := fiber.StatusInternalServerError
|
if err != nil {
|
||||||
// if err.Error() == "invalid token" {
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
// code = fiber.StatusUnauthorized
|
Message: "Unrecognized request format",
|
||||||
// }
|
Error: err.Error(),
|
||||||
// return fiber.NewError(code, err.Error())
|
})
|
||||||
// }
|
}
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Win processed", resp, nil)
|
switch provider {
|
||||||
|
case "veli":
|
||||||
|
var req domain.WinRequest
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid Veli win request",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.veliVirtualGameSvc.ProcessWin(c.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, veli.ErrDuplicateTransaction) {
|
||||||
|
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Duplicate transaction",
|
||||||
|
Error: "DUPLICATE_TRANSACTION",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Veli win processing failed",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(res)
|
||||||
|
|
||||||
|
case "popok":
|
||||||
|
var req domain.PopOKWinRequest
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid PopOK win request",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.virtualGameSvc.ProcessWin(c.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
code := fiber.StatusInternalServerError
|
||||||
|
if err.Error() == "invalid token" {
|
||||||
|
code = fiber.StatusUnauthorized
|
||||||
|
}
|
||||||
|
return c.Status(code).JSON(domain.ErrorResponse{
|
||||||
|
Message: "PopOK win processing failed",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(resp)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Unsupported provider",
|
||||||
|
Error: "Request format doesn't match any supported provider",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) HandleCancel(c *fiber.Ctx) error {
|
func (h *Handler) HandleCancel(c *fiber.Ctx) error {
|
||||||
var req domain.PopOKCancelRequest
|
body := c.Body()
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if len(body) == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid cancel request")
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Empty request body",
|
||||||
|
Error: "Request body cannot be empty",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, _ := h.virtualGameSvc.ProcessCancel(c.Context(), &req)
|
provider, err := identifyCancelProvider(body)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// code := fiber.StatusInternalServerError
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
// switch err.Error() {
|
Message: "Unrecognized request format",
|
||||||
// case "invalid token":
|
Error: err.Error(),
|
||||||
// code = fiber.StatusUnauthorized
|
})
|
||||||
// case "original bet not found", "invalid original transaction":
|
}
|
||||||
// code = fiber.StatusBadRequest
|
|
||||||
// }
|
|
||||||
// return fiber.NewError(code, err.Error())
|
|
||||||
// }
|
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Cancel processed", resp, nil)
|
switch provider {
|
||||||
|
case "veli":
|
||||||
|
var req domain.CancelRequest
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid Veli cancel request",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.veliVirtualGameSvc.ProcessCancel(c.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, veli.ErrDuplicateTransaction) {
|
||||||
|
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Duplicate transaction",
|
||||||
|
Error: "DUPLICATE_TRANSACTION",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Veli cancel processing failed",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(res)
|
||||||
|
|
||||||
|
case "popok":
|
||||||
|
var req domain.PopOKCancelRequest
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid PopOK cancel request",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.virtualGameSvc.ProcessCancel(c.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
code := fiber.StatusInternalServerError
|
||||||
|
switch err.Error() {
|
||||||
|
case "invalid token":
|
||||||
|
code = fiber.StatusUnauthorized
|
||||||
|
case "original bet not found", "invalid original transaction":
|
||||||
|
code = fiber.StatusBadRequest
|
||||||
|
}
|
||||||
|
return c.Status(code).JSON(domain.ErrorResponse{
|
||||||
|
Message: "PopOK cancel processing failed",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(resp)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Unsupported provider",
|
||||||
|
Error: "Request format doesn't match any supported provider",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGameList godoc
|
// GetGameList godoc
|
||||||
|
|
@ -176,14 +357,14 @@ func (h *Handler) HandleCancel(c *fiber.Ctx) error {
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param currency query string false "Currency (e.g. USD, ETB)" default(USD)
|
// @Param currency query string false "Currency (e.g. USD, ETB)" default(USD)
|
||||||
// @Success 200 {array} domain.PopOKGame
|
// @Success 200 {array} domain.PopOKGame
|
||||||
// @Failure 502 {object} domain.ErrorResponse
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
// @Router /popok/games [get]
|
// @Router /popok/games [get]
|
||||||
func (h *Handler) GetGameList(c *fiber.Ctx) error {
|
func (h *Handler) GetGameList(c *fiber.Ctx) error {
|
||||||
currency := c.Query("currency", "ETB") // fallback default
|
currency := c.Query("currency", "ETB") // fallback default
|
||||||
|
|
||||||
games, err := h.virtualGameSvc.ListGames(c.Context(), currency)
|
games, err := h.virtualGameSvc.ListGames(c.Context(), currency)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Falied to fetch games",
|
Message: "Falied to fetch games",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
|
|
@ -331,3 +512,86 @@ func (h *Handler) ListFavorites(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
return c.Status(fiber.StatusOK).JSON(games)
|
return c.Status(fiber.StatusOK).JSON(games)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func identifyBetProvider(body []byte) (string, error) {
|
||||||
|
// Check for Veli signature fields
|
||||||
|
var veliCheck struct {
|
||||||
|
TransactionID string `json:"transaction_id"`
|
||||||
|
GameID string `json:"game_id"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(body, &veliCheck) == nil {
|
||||||
|
if veliCheck.TransactionID != "" && veliCheck.GameID != "" {
|
||||||
|
return "veli", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for PopOK signature fields
|
||||||
|
var popokCheck struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
PlayerID string `json:"player_id"`
|
||||||
|
BetAmount float64 `json:"bet_amount"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(body, &popokCheck) == nil {
|
||||||
|
if popokCheck.Token != "" && popokCheck.PlayerID != "" {
|
||||||
|
return "popok", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("could not identify provider from request structure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// identifyWinProvider examines the request body to determine the provider for win callbacks
|
||||||
|
func identifyWinProvider(body []byte) (string, error) {
|
||||||
|
// Check for Veli signature fields
|
||||||
|
var veliCheck struct {
|
||||||
|
TransactionID string `json:"transaction_id"`
|
||||||
|
WinAmount float64 `json:"win_amount"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(body, &veliCheck) == nil {
|
||||||
|
if veliCheck.TransactionID != "" && veliCheck.WinAmount > 0 {
|
||||||
|
return "veli", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for PopOK signature fields
|
||||||
|
var popokCheck struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
PlayerID string `json:"player_id"`
|
||||||
|
WinAmount float64 `json:"win_amount"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(body, &popokCheck) == nil {
|
||||||
|
if popokCheck.Token != "" && popokCheck.PlayerID != "" {
|
||||||
|
return "popok", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("could not identify provider from request structure")
|
||||||
|
}
|
||||||
|
|
||||||
|
func identifyCancelProvider(body []byte) (string, error) {
|
||||||
|
// Check for Veli cancel signature
|
||||||
|
var veliCheck struct {
|
||||||
|
TransactionID string `json:"transaction_id"`
|
||||||
|
OriginalTxID string `json:"original_transaction_id"`
|
||||||
|
CancelReason string `json:"cancel_reason"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(body, &veliCheck) == nil {
|
||||||
|
if veliCheck.TransactionID != "" && veliCheck.OriginalTxID != "" {
|
||||||
|
return "veli", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for PopOK cancel signature
|
||||||
|
var popokCheck struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
PlayerID string `json:"player_id"`
|
||||||
|
OriginalTxID string `json:"original_transaction_id"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(body, &popokCheck) == nil {
|
||||||
|
if popokCheck.Token != "" && popokCheck.PlayerID != "" && popokCheck.OriginalTxID != "" {
|
||||||
|
return "popok", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("could not identify cancel provider from request structure")
|
||||||
|
}
|
||||||
|
|
|
||||||
112
test.html
Normal file
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