fix: transfer not showing online bet issue

This commit is contained in:
Samuel Tariku 2025-06-16 16:24:42 +03:00
parent 49527cbf2a
commit 1557a3141b
40 changed files with 585 additions and 315 deletions

8
.env
View File

@ -1,8 +1,8 @@
# REPORT_EXPORT_PATH="C:\\ProgramData\\FortuneBet\\exported_reports" #prod env # REPORT_EXPORT_PATH="C:\\ProgramData\\FortuneBet\\exported_reports" #prod env
REPORT_EXPORT_PATH ="./exported_reports" #dev env REPORT_EXPORT_PATH ="./exported_reports" #dev env
RESEND_SENDER_EMAIL=email RESEND_SENDER_EMAIL=customer@fortunebets.net
RESEND_API_KEY=123 RESEND_API_KEY=re_GSTRa9Pp_JkRWBpST9MvaCVULJF8ybGKE
ENV=development ENV=development
PORT=8080 PORT=8080
@ -11,8 +11,8 @@ REFRESH_EXPIRY=2592000
JWT_KEY=mysecretkey JWT_KEY=mysecretkey
ACCESS_EXPIRY=600 ACCESS_EXPIRY=600
LOG_LEVEL=debug LOG_LEVEL=debug
AFRO_SMS_API_KEY=1 AFRO_SMS_API_KEY=eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGlmaWVyIjoiQlR5ZDFIYmJFYXZ6YUo3dzZGell1RUlieGozSElJeTYiLCJleHAiOjE4OTYwMTM5MTksImlhdCI6MTczODI0NzUxOSwianRpIjoiOWIyNTJkNWQtODcxOC00NGYzLWIzMDQtMGYxOTRhY2NiNTU3In0.XPw8s6mCx1Tp1CfxGmjFRROmdkVnghnqfmsniB-Ze8I
AFRO_SMS_SENDER_NAME= AFRO_SMS_SENDER_NAME=FortuneBets
AFRO_SMS_RECEIVER_PHONE_NUMBER= AFRO_SMS_RECEIVER_PHONE_NUMBER=
BET365_TOKEN=158046-hesJDP2Cay2M5G BET365_TOKEN=158046-hesJDP2Cay2M5G

17
db.sql Normal file
View File

@ -0,0 +1,17 @@
psql (16.8)
Type "help" for help.
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=#
gh=# 
gh=#

View File

@ -125,7 +125,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
amount BIGINT NOT NULL, amount BIGINT NOT NULL,
type VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL,
receiver_wallet_id BIGINT NOT NULL, receiver_wallet_id BIGINT,
sender_wallet_id BIGINT, sender_wallet_id BIGINT,
cashier_id BIGINT, cashier_id BIGINT,
verified BOOLEAN NOT NULL DEFAULT false, verified BOOLEAN NOT NULL DEFAULT false,
@ -263,7 +263,6 @@ FROM companies
JOIN wallets ON wallets.id = companies.wallet_id JOIN wallets ON wallets.id = companies.wallet_id
JOIN users ON users.id = companies.admin_id; JOIN users ON users.id = companies.admin_id;
; ;
CREATE VIEW branch_details AS CREATE VIEW branch_details AS
SELECT branches.*, SELECT branches.*,
CONCAT(users.first_name, ' ', users.last_name) AS manager_name, CONCAT(users.first_name, ' ', users.last_name) AS manager_name,
@ -290,40 +289,39 @@ FROM tickets
LEFT JOIN ticket_outcomes ON tickets.id = ticket_outcomes.ticket_id LEFT JOIN ticket_outcomes ON tickets.id = ticket_outcomes.ticket_id
GROUP BY tickets.id; GROUP BY tickets.id;
-- Foreign Keys -- Foreign Keys
ALTER TABLE users ALTER TABLE users
ADD CONSTRAINT unique_email UNIQUE (email), ADD CONSTRAINT unique_email UNIQUE (email),
ADD CONSTRAINT unique_phone_number UNIQUE (phone_number); ADD CONSTRAINT unique_phone_number UNIQUE (phone_number);
ALTER TABLE refresh_tokens ALTER TABLE refresh_tokens
ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id); ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE bets ALTER TABLE bets
ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id);
ALTER TABLE wallets ALTER TABLE wallets
ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id); ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE customer_wallets ALTER TABLE customer_wallets
ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id), ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id),
ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_customer_wallets_static_wallet FOREIGN KEY (static_wallet_id) REFERENCES wallets(id); ADD CONSTRAINT fk_customer_wallets_static_wallet FOREIGN KEY (static_wallet_id) REFERENCES wallets(id);
ALTER TABLE wallet_transfer ALTER TABLE wallet_transfer
ADD CONSTRAINT fk_wallet_transfer_receiver_wallet FOREIGN KEY (receiver_wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_wallet_transfer_receiver_wallet FOREIGN KEY (receiver_wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_wallet_transfer_sender_wallet FOREIGN KEY (sender_wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_wallet_transfer_sender_wallet FOREIGN KEY (sender_wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_wallet_transfer_cashier FOREIGN KEY (cashier_id) REFERENCES users(id); ADD CONSTRAINT fk_wallet_transfer_cashier FOREIGN KEY (cashier_id) REFERENCES users(id);
ALTER TABLE transactions ALTER TABLE transactions
ADD CONSTRAINT fk_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches(id), ADD CONSTRAINT fk_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches(id),
ADD CONSTRAINT fk_transactions_cashiers FOREIGN KEY (cashier_id) REFERENCES users(id), ADD CONSTRAINT fk_transactions_cashiers FOREIGN KEY (cashier_id) REFERENCES users(id),
ADD CONSTRAINT fk_transactions_bets FOREIGN KEY (bet_id) REFERENCES bets(id); ADD CONSTRAINT fk_transactions_bets FOREIGN KEY (bet_id) REFERENCES bets(id);
ALTER TABLE branches ALTER TABLE branches
ADD CONSTRAINT fk_branches_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id), ADD CONSTRAINT fk_branches_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id),
ADD CONSTRAINT fk_branches_manager FOREIGN KEY (branch_manager_id) REFERENCES users(id); ADD CONSTRAINT fk_branches_manager FOREIGN KEY (branch_manager_id) REFERENCES users(id);
ALTER TABLE branch_operations ALTER TABLE branch_operations
ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id) ON DELETE CASCADE, ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_operations_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; ADD CONSTRAINT fk_branch_operations_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE branch_cashiers ALTER TABLE branch_cashiers
ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE companies ALTER TABLE companies
ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id),
ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE; ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE;
----------------------------------------------seed data------------------------------------------------------------- ----------------------------------------------seed data-------------------------------------------------------------
-------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION-------------------------------------------------

View File

@ -1,28 +1,28 @@
CREATE TABLE virtual_games ( -- CREATE TABLE virtual_games (
id BIGSERIAL PRIMARY KEY, -- id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL, -- name VARCHAR(255) NOT NULL,
provider VARCHAR(100) NOT NULL, -- provider VARCHAR(100) NOT NULL,
category VARCHAR(100) NOT NULL, -- category VARCHAR(100) NOT NULL,
min_bet DECIMAL(15,2) NOT NULL, -- min_bet DECIMAL(15,2) NOT NULL,
max_bet DECIMAL(15,2) NOT NULL, -- max_bet DECIMAL(15,2) NOT NULL,
volatility VARCHAR(50) NOT NULL, -- volatility VARCHAR(50) NOT NULL,
rtp DECIMAL(5,2) NOT NULL, -- rtp DECIMAL(5,2) NOT NULL,
is_featured BOOLEAN DEFAULT false, -- is_featured BOOLEAN DEFAULT false,
popularity_score INTEGER DEFAULT 0, -- popularity_score INTEGER DEFAULT 0,
thumbnail_url TEXT, -- thumbnail_url TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, -- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
); -- );
CREATE TABLE user_game_interactions ( CREATE TABLE user_game_interactions (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id), user_id BIGINT NOT NULL REFERENCES users(id),
game_id BIGINT NOT NULL REFERENCES virtual_games(id), game_id BIGINT NOT NULL REFERENCES virtual_games(id),
interaction_type VARCHAR(50) NOT NULL, -- 'view', 'play', 'bet', 'favorite' interaction_type VARCHAR(50) NOT NULL,
amount DECIMAL(15,2), -- 'view', 'play', 'bet', 'favorite'
amount DECIMAL(15, 2),
duration_seconds INTEGER, duration_seconds INTEGER,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX idx_user_game_interactions_user ON user_game_interactions(user_id); CREATE INDEX idx_user_game_interactions_user ON user_game_interactions(user_id);
CREATE INDEX idx_user_game_interactions_game ON user_game_interactions(game_id); CREATE INDEX idx_user_game_interactions_game ON user_game_interactions(game_id);

View File

@ -48,16 +48,20 @@ VALUES (
SELECT * SELECT *
FROM bet_with_outcomes FROM bet_with_outcomes
wHERE ( wHERE (
branch_id = $1 branch_id = sqlc.narg('branch_id')
OR $1 IS NULL OR sqlc.narg('branch_id') IS NULL
) )
AND ( AND (
company_id = $2 company_id = sqlc.narg('company_id')
OR $2 IS NULL OR sqlc.narg('company_id') IS NULL
) )
AND ( AND (
user_id = $3 user_id = sqlc.narg('user_id')
OR $3 IS NULL OR sqlc.narg('user_id') IS NULL
)
AND (
is_shop_bet = sqlc.narg('is_shop_bet')
OR sqlc.narg('is_shop_bet') IS NULL
); );
-- name: GetBetByID :one -- name: GetBetByID :one
SELECT * SELECT *

View File

@ -23,7 +23,15 @@ VALUES ($1, $2)
RETURNING *; RETURNING *;
-- name: GetAllBranches :many -- name: GetAllBranches :many
SELECT * SELECT *
FROM branch_details; FROM branch_details
WHERE (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
AND (
is_active = sqlc.narg('is_active')
OR sqlc.narg('is_active') IS NULL
);
-- name: GetBranchByID :one -- name: GetBranchByID :one
SELECT * SELECT *
FROM branch_details FROM branch_details

View File

@ -125,16 +125,26 @@ wHERE (
user_id = $3 user_id = $3
OR $3 IS NULL OR $3 IS NULL
) )
AND (
is_shop_bet = $4
OR $4 IS NULL
)
` `
type GetAllBetsParams struct { type GetAllBetsParams struct {
BranchID pgtype.Int8 `json:"branch_id"` BranchID pgtype.Int8 `json:"branch_id"`
CompanyID pgtype.Int8 `json:"company_id"` CompanyID pgtype.Int8 `json:"company_id"`
UserID pgtype.Int8 `json:"user_id"` UserID pgtype.Int8 `json:"user_id"`
IsShopBet pgtype.Bool `json:"is_shop_bet"`
} }
func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWithOutcome, error) { func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWithOutcome, error) {
rows, err := q.db.Query(ctx, GetAllBets, arg.BranchID, arg.CompanyID, arg.UserID) rows, err := q.db.Query(ctx, GetAllBets,
arg.BranchID,
arg.CompanyID,
arg.UserID,
arg.IsShopBet,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -157,10 +157,23 @@ func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOpe
const GetAllBranches = `-- name: GetAllBranches :many const GetAllBranches = `-- name: GetAllBranches :many
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
FROM branch_details FROM branch_details
WHERE (
company_id = $1
OR $1 IS NULL
)
AND (
is_active = $2
OR $2 IS NULL
)
` `
func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) { type GetAllBranchesParams struct {
rows, err := q.db.Query(ctx, GetAllBranches) CompanyID pgtype.Int8 `json:"company_id"`
IsActive pgtype.Bool `json:"is_active"`
}
func (q *Queries) GetAllBranches(ctx context.Context, arg GetAllBranchesParams) ([]BranchDetail, error) {
rows, err := q.db.Query(ctx, GetAllBranches, arg.CompanyID, arg.IsActive)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -421,12 +421,13 @@ type VirtualGame struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Provider string `json:"provider"` Provider string `json:"provider"`
Category string `json:"category"` Category pgtype.Text `json:"category"`
MinBet pgtype.Numeric `json:"min_bet"` MinBet pgtype.Numeric `json:"min_bet"`
MaxBet pgtype.Numeric `json:"max_bet"` MaxBet pgtype.Numeric `json:"max_bet"`
Volatility string `json:"volatility"` Volatility pgtype.Text `json:"volatility"`
IsActive bool `json:"is_active"`
Rtp pgtype.Numeric `json:"rtp"` Rtp pgtype.Numeric `json:"rtp"`
IsFeatured pgtype.Bool `json:"is_featured"` IsFeatured bool `json:"is_featured"`
PopularityScore pgtype.Int4 `json:"popularity_score"` PopularityScore pgtype.Int4 `json:"popularity_score"`
ThumbnailUrl pgtype.Text `json:"thumbnail_url"` ThumbnailUrl pgtype.Text `json:"thumbnail_url"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
@ -483,7 +484,7 @@ type WalletTransfer struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
Type string `json:"type"` Type string `json:"type"`
ReceiverWalletID int64 `json:"receiver_wallet_id"` ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"`
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
CashierID pgtype.Int8 `json:"cashier_id"` CashierID pgtype.Int8 `json:"cashier_id"`
Verified bool `json:"verified"` Verified bool `json:"verified"`

View File

@ -29,7 +29,7 @@ RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, ve
type CreateTransferParams struct { type CreateTransferParams struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount"`
Type string `json:"type"` Type string `json:"type"`
ReceiverWalletID int64 `json:"receiver_wallet_id"` ReceiverWalletID pgtype.Int8 `json:"receiver_wallet_id"`
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
CashierID pgtype.Int8 `json:"cashier_id"` CashierID pgtype.Int8 `json:"cashier_id"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
@ -159,7 +159,7 @@ WHERE receiver_wallet_id = $1
OR sender_wallet_id = $1 OR sender_wallet_id = $1
` `
func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID int64) ([]WalletTransfer, error) { func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgtype.Int8) ([]WalletTransfer, error) {
rows, err := q.db.Query(ctx, GetTransfersByWallet, receiverWalletID) rows, err := q.db.Query(ctx, GetTransfersByWallet, receiverWalletID)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -60,6 +60,7 @@ type BetFilter struct {
BranchID ValidInt64 // Can Be Nullable BranchID ValidInt64 // Can Be Nullable
CompanyID ValidInt64 // Can Be Nullable CompanyID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable UserID ValidInt64 // Can Be Nullable
IsShopBet ValidBool
} }
type GetBet struct { type GetBet struct {
@ -173,4 +174,3 @@ func ConvertBet(bet GetBet) BetRes {
CreatedAt: bet.CreatedAt, CreatedAt: bet.CreatedAt,
} }
} }

View File

@ -11,6 +11,11 @@ type Branch struct {
IsSelfOwned bool IsSelfOwned bool
} }
type BranchFilter struct {
CompanyID ValidInt64
IsSuspended ValidBool
}
type BranchDetail struct { type BranchDetail struct {
ID int64 ID int64
Name string Name string

View File

@ -14,18 +14,20 @@ type NotificationDeliveryStatus string
type DeliveryChannel string type DeliveryChannel string
const ( const (
NotificationTypeCashOutSuccess NotificationType = "cash_out_success" NotificationTypeCashOutSuccess NotificationType = "cash_out_success"
NotificationTypeDepositSuccess NotificationType = "deposit_success" NotificationTypeDepositSuccess NotificationType = "deposit_success"
NotificationTypeBetPlaced NotificationType = "bet_placed" NotificationTypeWithdrawSuccess NotificationType = "withdraw_success"
NotificationTypeDailyReport NotificationType = "daily_report" NotificationTypeBetPlaced NotificationType = "bet_placed"
NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" NotificationTypeDailyReport NotificationType = "daily_report"
NotificationTypeBetOverload NotificationType = "bet_overload" NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet"
NotificationTypeSignUpWelcome NotificationType = "signup_welcome" NotificationTypeBetOverload NotificationType = "bet_overload"
NotificationTypeOTPSent NotificationType = "otp_sent" NotificationTypeSignUpWelcome NotificationType = "signup_welcome"
NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold" NotificationTypeOTPSent NotificationType = "otp_sent"
NOTIFICATION_TYPE_TRANSFER NotificationType = "transfer_failed" NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold"
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed"
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin" NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success"
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin"
NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer" NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
@ -57,9 +59,9 @@ const (
) )
type NotificationPayload struct { type NotificationPayload struct {
Headline string `json:"headline"` Headline string `json:"headline"`
Message string `json:"message"` Message string `json:"message"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
} }
type Notification struct { type Notification struct {
@ -91,3 +93,18 @@ func FromJSON(data []byte) (*Notification, error) {
} }
return &n, nil return &n, nil
} }
func ReceiverFromRole(role Role) NotificationRecieverSide {
if role == RoleAdmin {
return NotificationRecieverSideAdmin
} else if role == RoleCashier {
return NotificationRecieverSideCashier
} else if role == RoleBranchManager {
return NotificationRecieverSideBranchManager
} else if role == RoleCustomer {
return NotificationRecieverSideCustomer
} else {
return ""
}
}

View File

@ -12,7 +12,10 @@ const (
type PaymentMethod string type PaymentMethod string
// Info on why the wallet was modified
// If its internal system modification then, its always direct
const ( const (
TRANSFER_DIRECT PaymentMethod = "direct"
TRANSFER_CASH PaymentMethod = "cash" TRANSFER_CASH PaymentMethod = "cash"
TRANSFER_BANK PaymentMethod = "bank" TRANSFER_BANK PaymentMethod = "bank"
TRANSFER_CHAPA PaymentMethod = "chapa" TRANSFER_CHAPA PaymentMethod = "chapa"
@ -22,16 +25,21 @@ const (
TRANSFER_OTHER PaymentMethod = "other" TRANSFER_OTHER PaymentMethod = "other"
) )
// There is always a receiving wallet id // Info for the payment providers
// There is a sender wallet id only if wallet transfer type type PaymentDetails struct {
ReferenceNumber ValidString
BankNumber ValidString
}
// A Transfer is logged for every modification of ALL wallets and wallet types
type Transfer struct { type Transfer struct {
ID int64 ID int64
Amount Currency Amount Currency
Verified bool Verified bool
Type TransferType Type TransferType
PaymentMethod PaymentMethod PaymentMethod PaymentMethod
ReceiverWalletID int64 ReceiverWalletID ValidInt64
SenderWalletID int64 SenderWalletID ValidInt64
ReferenceNumber string ReferenceNumber string
CashierID ValidInt64 CashierID ValidInt64
CreatedAt time.Time CreatedAt time.Time
@ -42,8 +50,8 @@ type CreateTransfer struct {
Amount Currency Amount Currency
Verified bool Verified bool
ReferenceNumber string ReferenceNumber string
ReceiverWalletID int64 ReceiverWalletID ValidInt64
SenderWalletID int64 SenderWalletID ValidInt64
CashierID ValidInt64 CashierID ValidInt64
Type TransferType Type TransferType
PaymentMethod PaymentMethod PaymentMethod PaymentMethod

View File

@ -57,3 +57,11 @@ type CreateCustomerWallet struct {
RegularWalletID int64 RegularWalletID int64
StaticWalletID int64 StaticWalletID int64
} }
type WalletType string
const (
CustomerWalletType WalletType = "customer_wallet"
BranchWalletType WalletType = "branch_wallet"
CompanyWalletType WalletType = "company_wallet"
)

View File

@ -10,7 +10,7 @@ import (
func InitLogger() (*zap.Logger, error) { func InitLogger() (*zap.Logger, error) {
mongoCore, err := NewMongoCore( mongoCore, err := NewMongoCore(
"mongodb://root:secret@mongo:27017/?authSource=admin", "mongodb://root:secret@localhost:27017/?authSource=admin",
"logdb", "logdb",
"applogs", "applogs",
zapcore.InfoLevel, zapcore.InfoLevel,

View File

@ -209,6 +209,10 @@ func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]doma
Int64: filter.UserID.Value, Int64: filter.UserID.Value,
Valid: filter.UserID.Valid, Valid: filter.UserID.Valid,
}, },
IsShopBet: pgtype.Bool{
Bool: filter.IsShopBet.Value,
Valid: filter.IsShopBet.Valid,
},
}) })
if err != nil { if err != nil {
domain.MongoDBLogger.Error("failed to get all bets", domain.MongoDBLogger.Error("failed to get all bets",

View File

@ -128,8 +128,13 @@ func (s *Store) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]do
return branches, nil return branches, nil
} }
func (s *Store) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { func (s *Store) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) {
dbBranches, err := s.queries.GetAllBranches(ctx) dbBranches, err := s.queries.GetAllBranches(ctx, dbgen.GetAllBranchesParams{
CompanyID: pgtype.Int8{
Int64: filter.CompanyID.Value,
Valid: filter.CompanyID.Valid,
},
})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -10,12 +10,18 @@ import (
func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer { func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer {
return domain.Transfer{ return domain.Transfer{
ID: transfer.ID, ID: transfer.ID,
Amount: domain.Currency(transfer.Amount), Amount: domain.Currency(transfer.Amount),
Type: domain.TransferType(transfer.Type), Type: domain.TransferType(transfer.Type),
Verified: transfer.Verified, Verified: transfer.Verified,
ReceiverWalletID: transfer.ReceiverWalletID, ReceiverWalletID: domain.ValidInt64{
SenderWalletID: transfer.SenderWalletID.Int64, Value: transfer.ReceiverWalletID.Int64,
Valid: transfer.ReceiverWalletID.Valid,
},
SenderWalletID: domain.ValidInt64{
Value: transfer.SenderWalletID.Int64,
Valid: transfer.SenderWalletID.Valid,
},
CashierID: domain.ValidInt64{ CashierID: domain.ValidInt64{
Value: transfer.CashierID.Int64, Value: transfer.CashierID.Int64,
Valid: transfer.CashierID.Valid, Valid: transfer.CashierID.Valid,
@ -26,12 +32,15 @@ func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer {
func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferParams { func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferParams {
return dbgen.CreateTransferParams{ return dbgen.CreateTransferParams{
Amount: int64(transfer.Amount), Amount: int64(transfer.Amount),
Type: string(transfer.Type), Type: string(transfer.Type),
ReceiverWalletID: transfer.ReceiverWalletID, ReceiverWalletID: pgtype.Int8{
Int64: transfer.ReceiverWalletID.Value,
Valid: transfer.ReceiverWalletID.Valid,
},
SenderWalletID: pgtype.Int8{ SenderWalletID: pgtype.Int8{
Int64: transfer.SenderWalletID, Int64: transfer.SenderWalletID.Value,
Valid: true, Valid: transfer.SenderWalletID.Valid,
}, },
CashierID: pgtype.Int8{ CashierID: pgtype.Int8{
Int64: transfer.CashierID.Value, Int64: transfer.CashierID.Value,
@ -62,7 +71,10 @@ func (s *Store) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error)
return result, nil return result, nil
} }
func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) { func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) {
transfers, err := s.queries.GetTransfersByWallet(ctx, walletID) transfers, err := s.queries.GetTransfersByWallet(ctx, pgtype.Int8{
Int64: walletID,
Valid: true,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -241,7 +241,11 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
} }
deductedAmount := req.Amount / 10 deductedAmount := req.Amount / 10
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount)) _, err = s.walletSvc.DeductFromWallet(ctx,
branch.WalletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{
Value: userID,
Valid: true,
}, domain.TRANSFER_DIRECT)
if err != nil { if err != nil {
s.mongoLogger.Error("failed to deduct from wallet", s.mongoLogger.Error("failed to deduct from wallet",
zap.Int64("wallet_id", branch.WalletID), zap.Int64("wallet_id", branch.WalletID),
@ -274,7 +278,10 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
} }
deductedAmount := req.Amount / 10 deductedAmount := req.Amount / 10
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount)) _, err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{
Value: userID,
Valid: true,
}, domain.TRANSFER_DIRECT)
if err != nil { if err != nil {
s.mongoLogger.Error("wallet deduction failed", s.mongoLogger.Error("wallet deduction failed",
zap.Int64("wallet_id", branch.WalletID), zap.Int64("wallet_id", branch.WalletID),
@ -300,7 +307,8 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
} }
userWallet := wallets[0] userWallet := wallets[0]
err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID, domain.ToCurrency(req.Amount)) _, err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID,
domain.ToCurrency(req.Amount), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT)
if err != nil { if err != nil {
s.mongoLogger.Error("wallet deduction failed for customer", s.mongoLogger.Error("wallet deduction failed for customer",
zap.Int64("wallet_id", userWallet.ID), zap.Int64("wallet_id", userWallet.ID),
@ -676,7 +684,8 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
amount = bet.Amount amount = bet.Amount
} }
err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount) _, err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
s.mongoLogger.Error("failed to add winnings to wallet", s.mongoLogger.Error("failed to add winnings to wallet",
zap.Int64("wallet_id", customerWallet.RegularID), zap.Int64("wallet_id", customerWallet.RegularID),

View File

@ -11,7 +11,7 @@ type BranchStore interface {
GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error)
GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error)
GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error)
GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error)
SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error) SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error)
UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error) UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error)
DeleteBranch(ctx context.Context, id int64) error DeleteBranch(ctx context.Context, id int64) error

View File

@ -1,4 +1,4 @@
package branch package branch
import ( import (
"context" "context"
@ -42,8 +42,8 @@ func (s *Service) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
func (s *Service) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) { func (s *Service) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) {
return s.branchStore.GetBranchOperations(ctx, branchID) return s.branchStore.GetBranchOperations(ctx, branchID)
} }
func (s *Service) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { func (s *Service) GetAllBranches(ctx context.Context, filter domain.BranchFilter) ([]domain.BranchDetail, error) {
return s.branchStore.GetAllBranches(ctx) return s.branchStore.GetAllBranches(ctx, filter)
} }
func (s *Service) GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) { func (s *Service) GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) {

View File

@ -82,8 +82,11 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
PaymentMethod: domain.TRANSFER_CHAPA, PaymentMethod: domain.TRANSFER_CHAPA,
ReferenceNumber: reference, ReferenceNumber: reference,
// ReceiverWalletID: 1, // ReceiverWalletID: 1,
SenderWalletID: senderWallet.ID, SenderWalletID: domain.ValidInt64{
Verified: false, Value: senderWallet.ID,
Valid: true,
},
Verified: false,
} }
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil { if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
@ -119,6 +122,11 @@ func (s *Service) VerifyDeposit(ctx context.Context, reference string) error {
return ErrPaymentNotFound return ErrPaymentNotFound
} }
// just making sure that the sender id is valid
if !payment.SenderWalletID.Valid {
return fmt.Errorf("sender wallet is invalid %v \n", payment.SenderWalletID)
}
// Skip if already completed // Skip if already completed
if payment.Verified { if payment.Verified {
return nil return nil
@ -137,7 +145,7 @@ func (s *Service) VerifyDeposit(ctx context.Context, reference string) error {
// If payment is completed, credit user's wallet // If payment is completed, credit user's wallet
if verification.Status == domain.PaymentStatusCompleted { if verification.Status == domain.PaymentStatusCompleted {
if err := s.walletStore.UpdateBalance(ctx, payment.SenderWalletID, payment.Amount); err != nil { if err := s.walletStore.UpdateBalance(ctx, payment.SenderWalletID.Value, payment.Amount); err != nil {
return fmt.Errorf("failed to credit user wallet: %w", err) return fmt.Errorf("failed to credit user wallet: %w", err)
} }
} }
@ -156,6 +164,11 @@ func (s *Service) ManualVerifyPayment(ctx context.Context, txRef string) (*domai
}, nil }, nil
} }
// just making sure that the sender id is valid
if !transfer.SenderWalletID.Valid {
return nil, fmt.Errorf("sender wallet id is invalid: %v \n", transfer.SenderWalletID)
}
// If not verified or not found, verify with Chapa // If not verified or not found, verify with Chapa
verification, err := s.chapaClient.VerifyPayment(ctx, txRef) verification, err := s.chapaClient.VerifyPayment(ctx, txRef)
if err != nil { if err != nil {
@ -170,7 +183,7 @@ func (s *Service) ManualVerifyPayment(ctx context.Context, txRef string) (*domai
} }
// Credit user's wallet // Credit user's wallet
err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID, transfer.Amount) err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, transfer.Amount)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to update wallet balance: %w", err) return nil, fmt.Errorf("failed to update wallet balance: %w", err)
} }

View File

@ -124,7 +124,7 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo
walletID := wallets[0].ID walletID := wallets[0].ID
currentBonus := float64(wallets[0].Balance) currentBonus := float64(wallets[0].Balance)
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBonus+referral.RewardAmount)*100))) _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+referral.RewardAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
s.logger.Error("Failed to add referral reward to wallet", "walletID", walletID, "referrerID", referrerID, "error", err) s.logger.Error("Failed to add referral reward to wallet", "walletID", walletID, "referrerID", referrerID, "error", err)
return err return err
@ -162,7 +162,7 @@ func (s *Service) ProcessDepositBonus(ctx context.Context, userPhone string, amo
walletID := wallets[0].ID walletID := wallets[0].ID
bonus := amount * (settings.CashbackPercentage / 100) bonus := amount * (settings.CashbackPercentage / 100)
currentBonus := float64(wallets[0].Balance) currentBonus := float64(wallets[0].Balance)
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBonus+bonus)*100))) _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+bonus)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
s.logger.Error("Failed to add deposit bonus to wallet", "walletID", walletID, "userID", userID, "bonus", bonus, "error", err) s.logger.Error("Failed to add deposit bonus to wallet", "walletID", walletID, "userID", userID, "bonus", bonus, "error", err)
return err return err
@ -216,7 +216,7 @@ func (s *Service) ProcessBetReferral(ctx context.Context, userPhone string, betA
walletID := wallets[0].ID walletID := wallets[0].ID
currentBalance := float64(wallets[0].Balance) currentBalance := float64(wallets[0].Balance)
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(int64((currentBalance+bonus)*100))) _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBalance+bonus)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
s.logger.Error("Failed to add bet referral bonus to wallet", "walletID", walletID, "referrerID", referrerID, "bonus", bonus, "error", err) s.logger.Error("Failed to add bet referral bonus to wallet", "walletID", walletID, "referrerID", referrerID, "bonus", bonus, "error", err)
return err return err

View File

@ -128,7 +128,7 @@ func (s *AleaPlayService) processTransaction(ctx context.Context, tx *domain.Vir
} }
tx.WalletID = wallets[0].ID tx.WalletID = wallets[0].ID
if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil { if _, err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
return fmt.Errorf("wallet update failed: %w", err) return fmt.Errorf("wallet update failed: %w", err)
} }

View File

@ -117,7 +117,7 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall
return errors.New("unknown transaction type") return errors.New("unknown transaction type")
} }
err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(amount)) _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.Currency(amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
s.logger.Error("Failed to update wallet", "walletID", walletID, "userID", session.UserID, "amount", amount, "error", err) s.logger.Error("Failed to update wallet", "walletID", walletID, "userID", session.UserID, "amount", amount, "error", err)
return err return err
@ -184,7 +184,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets") return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets")
} }
if err := s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents)); err != nil { if _, err := s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT); err != nil {
return nil, fmt.Errorf("insufficient balance") return nil, fmt.Errorf("insufficient balance")
} }
@ -245,7 +245,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
// 4. Credit to wallet // 4. Credit to wallet
if err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents)); err != nil { if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(amountCents), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
s.logger.Error("Failed to credit wallet", "userID", claims.UserID, "error", err) s.logger.Error("Failed to credit wallet", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("wallet credit failed") return nil, fmt.Errorf("wallet credit failed")
} }
@ -316,7 +316,7 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ
// 5. Refund the bet amount (absolute value since bet amount is negative) // 5. Refund the bet amount (absolute value since bet amount is negative)
refundAmount := -originalBet.Amount refundAmount := -originalBet.Amount
if err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount)); err != nil { if _, err := s.walletSvc.AddToWallet(ctx, claims.UserID, domain.Currency(refundAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
s.logger.Error("Failed to refund bet", "userID", claims.UserID, "error", err) s.logger.Error("Failed to refund bet", "userID", claims.UserID, "error", err)
return nil, fmt.Errorf("refund failed") return nil, fmt.Errorf("refund failed")
} }

View File

@ -150,7 +150,7 @@ func (s *VeliPlayService) processTransaction(ctx context.Context, tx *domain.Vir
} }
tx.WalletID = wallets[0].ID tx.WalletID = wallets[0].ID
if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil { if _, err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}); err != nil {
return fmt.Errorf("wallet update failed: %w", err) return fmt.Errorf("wallet update failed: %w", err)
} }

View File

@ -14,85 +14,7 @@ var (
) )
func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
senderWallet, err := s.walletStore.GetWalletByID(ctx, transfer.SenderWalletID) // This is just a transfer log when
receiverWallet, err := s.walletStore.GetWalletByID(ctx, transfer.ReceiverWalletID)
if err != nil {
return domain.Transfer{}, fmt.Errorf("failed to get sender wallet: %w", err)
}
// Check if wallet has sufficient balance
if senderWallet.Balance < transfer.Amount || senderWallet.Balance == 0 {
// Send notification to customer
customerNotification := &domain.Notification{
RecipientID: receiverWallet.UserID,
Type: domain.NOTIFICATION_TYPE_TRANSFER,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: "Service Temporarily Unavailable",
Message: "Our payment system is currently under maintenance. Please try again later.",
},
Priority: 2,
Metadata: []byte(fmt.Sprintf(`{
"transfer_amount": %d,
"current_balance": %d,
"wallet_id": %d,
"notification_type": "customer_facing"
}`, transfer.Amount, senderWallet.Balance, transfer.SenderWalletID)),
}
// Send notification to admin team
adminNotification := &domain.Notification{
RecipientID: senderWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT WARNING: System Running Out of Funds",
Message: fmt.Sprintf(
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f",
transfer.SenderWalletID,
float64(senderWallet.Balance)/100,
float64(transfer.Amount)/100,
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"required_amount": %d,
"notification_type": "admin_alert"
}`, transfer.SenderWalletID, senderWallet.Balance, transfer.Amount),
}
// Send both notifications
if err := s.notificationStore.SendNotification(ctx, customerNotification); err != nil {
s.logger.Error("failed to send customer notification",
"user_id", "",
"error", err)
}
// Get admin recipients and send to all
adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin)
if err != nil {
s.logger.Error("failed to get admin recipients", "error", err)
} else {
for _, adminID := range adminRecipients {
adminNotification.RecipientID = adminID
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
s.logger.Error("failed to send admin notification",
"admin_id", adminID,
"error", err)
}
}
}
return domain.Transfer{}, ErrInsufficientBalance
}
// Proceed with transfer if balance is sufficient
return s.transferStore.CreateTransfer(ctx, transfer) return s.transferStore.CreateTransfer(ctx, transfer)
} }
@ -116,43 +38,12 @@ func (s *Service) UpdateTransferVerification(ctx context.Context, id int64, veri
return s.transferStore.UpdateTransferVerification(ctx, id, verified) return s.transferStore.UpdateTransferVerification(ctx, id, verified)
} }
func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID)
if err != nil {
return domain.Transfer{}, err
}
// Add to receiver
senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID)
if err != nil {
return domain.Transfer{}, err
} else if senderWallet.Balance < transfer.Amount {
return domain.Transfer{}, ErrInsufficientBalance
}
err = s.walletStore.UpdateBalance(ctx, receiverWallet.ID, receiverWallet.Balance+transfer.Amount) func (s *Service) TransferToWallet(ctx context.Context,
if err != nil { senderID int64, receiverID int64,
return domain.Transfer{}, err amount domain.Currency, paymentMethod domain.PaymentMethod,
} cashierID domain.ValidInt64) (domain.Transfer, error) {
// Log the transfer so that if there is a mistake, it can be reverted
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
CashierID: transfer.CashierID,
ReceiverWalletID: receiverWallet.ID,
Amount: transfer.Amount,
Type: domain.DEPOSIT,
PaymentMethod: transfer.PaymentMethod,
Verified: true,
})
if err != nil {
return domain.Transfer{}, err
}
return newTransfer, nil
}
func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiverID int64, amount domain.Currency, paymentMethod domain.PaymentMethod, cashierID domain.ValidInt64) (domain.Transfer, error) {
senderWallet, err := s.GetWalletByID(ctx, senderID) senderWallet, err := s.GetWalletByID(ctx, senderID)
if err != nil { if err != nil {
@ -191,14 +82,20 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver
} }
// Log the transfer so that if there is a mistake, it can be reverted // Log the transfer so that if there is a mistake, it can be reverted
transfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ transfer, err := s.CreateTransfer(ctx, domain.CreateTransfer{
SenderWalletID: senderID, SenderWalletID: domain.ValidInt64{
CashierID: cashierID, Value: senderID,
ReceiverWalletID: receiverID, Valid: true,
Amount: amount, },
Type: domain.WALLET, ReceiverWalletID: domain.ValidInt64{
PaymentMethod: paymentMethod, Value: receiverID,
Verified: true, Valid: true,
},
CashierID: cashierID,
Amount: amount,
Type: domain.WALLET,
PaymentMethod: paymentMethod,
Verified: true,
}) })
if err != nil { if err != nil {
return domain.Transfer{}, err return domain.Transfer{}, err
@ -206,3 +103,66 @@ func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiver
return transfer, nil return transfer, nil
} }
func (s *Service) SendTransferNotification(ctx context.Context, senderWallet domain.Wallet, receiverWallet domain.Wallet,
senderRole domain.Role, receiverRole domain.Role, amount domain.Currency) error {
// Send notification to sender (this could be any role) that money was transferred
senderWalletReceiverSide := domain.ReceiverFromRole(senderRole)
senderNotify := &domain.Notification{
RecipientID: senderWallet.UserID,
Type: domain.NOTIFICATION_TYPE_TRANSFER_SUCCESS,
Level: domain.NotificationLevelSuccess,
Reciever: senderWalletReceiverSide,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: "Wallet has been deducted",
Message: fmt.Sprintf(`ETB %d has been transferred from your wallet`),
},
Priority: 2,
Metadata: []byte(fmt.Sprintf(`{
"transfer_amount": %d,
"current_balance": %d,
"wallet_id": %d,
"notification_type": "customer_facing"
}`, amount, senderWallet.Balance, senderWallet.ID)),
}
// Sender notifications
if err := s.notificationStore.SendNotification(ctx, senderNotify); err != nil {
s.logger.Error("failed to send sender notification",
"user_id", "",
"error", err)
return err
}
receiverWalletReceiverSide := domain.ReceiverFromRole(receiverRole)
receiverNotify := &domain.Notification{
RecipientID: receiverWallet.UserID,
Type: domain.NOTIFICATION_TYPE_TRANSFER_SUCCESS,
Level: domain.NotificationLevelSuccess,
Reciever: receiverWalletReceiverSide,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: "Wallet has been credited",
Message: fmt.Sprintf(`ETB %d has been transferred to your wallet`),
},
Priority: 2,
Metadata: []byte(fmt.Sprintf(`{
"transfer_amount": %d,
"current_balance": %d,
"wallet_id": %d,
"notification_type": "customer_facing"
}`, amount, receiverWallet.Balance, receiverWallet.ID)),
}
// Sender notifications
if err := s.notificationStore.SendNotification(ctx, receiverNotify); err != nil {
s.logger.Error("failed to send sender notification",
"user_id", "",
"error", err)
return err
}
return nil
}

View File

@ -3,6 +3,7 @@ package wallet
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
@ -68,28 +69,152 @@ func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Cu
return s.walletStore.UpdateBalance(ctx, id, balance) return s.walletStore.UpdateBalance(ctx, id, balance)
} }
func (s *Service) AddToWallet(ctx context.Context, id int64, amount domain.Currency) error { func (s *Service) AddToWallet(
ctx context.Context, id int64, amount domain.Currency, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, paymentDetails domain.PaymentDetails) (domain.Transfer, error) {
wallet, err := s.GetWalletByID(ctx, id) wallet, err := s.GetWalletByID(ctx, id)
if err != nil { if err != nil {
return err return domain.Transfer{}, err
} }
return s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount) err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount)
if err != nil {
return domain.Transfer{}, err
}
// Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
Amount: amount,
Verified: true,
ReceiverWalletID: domain.ValidInt64{
Value: wallet.ID,
Valid: true,
},
CashierID: cashierID,
PaymentMethod: paymentMethod,
Type: domain.DEPOSIT,
ReferenceNumber: paymentDetails.ReferenceNumber.Value,
})
return newTransfer, err
} }
func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency) error { func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency, walletType domain.WalletType, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod) (domain.Transfer, error) {
wallet, err := s.GetWalletByID(ctx, id) wallet, err := s.GetWalletByID(ctx, id)
if err != nil { if err != nil {
return err return domain.Transfer{}, err
} }
if wallet.Balance < amount { if wallet.Balance < amount {
return ErrBalanceInsufficient // Send Wallet low to admin
if walletType == domain.CompanyWalletType || walletType == domain.BranchWalletType {
s.SendAdminWalletLowNotification(ctx, wallet, amount)
}
return domain.Transfer{}, ErrBalanceInsufficient
} }
return s.walletStore.UpdateBalance(ctx, id, wallet.Balance-amount) err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance-amount)
if err != nil {
return domain.Transfer{}, nil
}
// Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
Amount: amount,
Verified: true,
SenderWalletID: domain.ValidInt64{
Value: wallet.ID,
Valid: true,
},
CashierID: cashierID,
PaymentMethod: paymentMethod,
Type: domain.WITHDRAW,
ReferenceNumber: "",
})
return newTransfer, err
} }
// Directly Refilling wallet without
// func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
// receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID)
// if err != nil {
// return domain.Transfer{}, err
// }
// // Add to receiver
// senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID)
// if err != nil {
// return domain.Transfer{}, err
// } else if senderWallet.Balance < transfer.Amount {
// return domain.Transfer{}, ErrInsufficientBalance
// }
// err = s.walletStore.UpdateBalance(ctx, receiverWallet.ID, receiverWallet.Balance+transfer.Amount)
// if err != nil {
// return domain.Transfer{}, err
// }
// // Log the transfer so that if there is a mistake, it can be reverted
// newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
// CashierID: transfer.CashierID,
// ReceiverWalletID: transfer.ReceiverWalletID,
// Amount: transfer.Amount,
// Type: domain.DEPOSIT,
// PaymentMethod: transfer.PaymentMethod,
// Verified: true,
// })
// if err != nil {
// return domain.Transfer{}, err
// }
// return newTransfer, nil
// }
func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error { func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error {
return s.walletStore.UpdateWalletActive(ctx, id, isActive) return s.walletStore.UpdateWalletActive(ctx, id, isActive)
} }
func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error {
// Send notification to admin team
adminNotification := &domain.Notification{
RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT WARNING: System Running Out of Funds",
Message: fmt.Sprintf(
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f",
adminWallet.ID,
adminWallet.Balance.Float32(),
amount.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"required_amount": %d,
"notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance, amount),
}
// Get admin recipients and send to all
adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin)
if err != nil {
s.logger.Error("failed to get admin recipients", "error", err)
return err
} else {
for _, adminID := range adminRecipients {
adminNotification.RecipientID = adminID
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
s.logger.Error("failed to send admin notification",
"admin_id", adminID,
"error", err)
}
}
}
return nil
}

View File

@ -62,30 +62,30 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
// } // }
// }, // },
// }, // },
{ // {
spec: "0 */5 * * * *", // Every 5 Minutes // spec: "0 */5 * * * *", // Every 5 Minutes
task: func() { // task: func() {
log.Println("Updating expired events status...") // log.Println("Updating expired events status...")
if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { // if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil {
log.Printf("Failed to update events: %v", err) // log.Printf("Failed to update events: %v", err)
} else { // } else {
log.Printf("Successfully updated expired events") // log.Printf("Successfully updated expired events")
} // }
}, // },
}, // },
{ // {
spec: "0 */15 * * * *", // Every 15 Minutes // spec: "0 */15 * * * *", // Every 15 Minutes
task: func() { // task: func() {
log.Println("Fetching results for upcoming events...") // log.Println("Fetching results for upcoming events...")
if err := resultService.FetchAndProcessResults(context.Background()); err != nil { // if err := resultService.FetchAndProcessResults(context.Background()); err != nil {
log.Printf("Failed to process result: %v", err) // log.Printf("Failed to process result: %v", err)
} else { // } else {
log.Printf("Successfully processed all outcomes") // log.Printf("Successfully processed all outcomes")
} // }
}, // },
}, // },
} }
for _, job := range schedule { for _, job := range schedule {

View File

@ -36,15 +36,14 @@ type loginCustomerRes struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /auth/login [post] // @Router /auth/login [post]
func (h *Handler) LoginCustomer(c *fiber.Ctx) error { func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
var req loginCustomerReq var req loginCustomerReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse LoginCustomer request", "error", err) h.logger.Error("Failed to parse LoginCustomer request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
if valErrs, ok := h.validator.Validate(c, req); !ok { if _, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid Request")
} }
successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password) successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)

View File

@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"fmt"
"strconv" "strconv"
"time" "time"
@ -24,7 +23,6 @@ import (
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [post] // @Router /bet [post]
func (h *Handler) CreateBet(c *fiber.Ctx) error { func (h *Handler) CreateBet(c *fiber.Ctx) error {
fmt.Printf("Calling leagues")
// Get user_id from middleware // Get user_id from middleware
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
@ -161,12 +159,29 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [get] // @Router /bet [get]
func (h *Handler) GetAllBet(c *fiber.Ctx) error { func (h *Handler) GetAllBet(c *fiber.Ctx) error {
// role := c.Locals("role").(domain.Role)
companyID := c.Locals("company_id").(domain.ValidInt64) companyID := c.Locals("company_id").(domain.ValidInt64)
branchID := c.Locals("branch_id").(domain.ValidInt64) branchID := c.Locals("branch_id").(domain.ValidInt64)
var isShopBet domain.ValidBool
isShopBetQuery := c.Query("is_shop")
if isShopBetQuery != "" {
isShopBetParse, err := strconv.ParseBool(isShopBetQuery)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet")
}
isShopBet = domain.ValidBool{
Value: isShopBetParse,
Valid: true,
}
}
bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{ bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{
BranchID: branchID, BranchID: branchID,
CompanyID: companyID, CompanyID: companyID,
IsShopBet: isShopBet,
}) })
if err != nil { if err != nil {
h.logger.Error("Failed to get bets", "error", err) h.logger.Error("Failed to get bets", "error", err)

View File

@ -381,8 +381,23 @@ func (h *Handler) GetBranchByCompanyID(c *fiber.Ctx) error {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /branch [get] // @Router /branch [get]
func (h *Handler) GetAllBranches(c *fiber.Ctx) error { func (h *Handler) GetAllBranches(c *fiber.Ctx) error {
// TODO: Limit the get all branches to only the companies for branch manager and cashiers companyID := c.Locals("company_id").(domain.ValidInt64)
branches, err := h.branchSvc.GetAllBranches(c.Context()) isActiveParam := c.Params("is_active")
isActiveValid := isActiveParam != ""
isActive, err := strconv.ParseBool(isActiveParam)
if isActiveValid && err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid is_active param", err, nil)
}
branches, err := h.branchSvc.GetAllBranches(c.Context(),
domain.BranchFilter{
CompanyID: companyID,
IsSuspended: domain.ValidBool{
Value: isActive,
Valid: isActiveValid,
},
})
if err != nil { if err != nil {
h.logger.Error("Failed to get branches", "error", err) h.logger.Error("Failed to get branches", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil)

View File

@ -112,6 +112,7 @@ func (h *Handler) GetAllManagers(c *fiber.Ctx) error {
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
companyId := c.Locals("company_id").(domain.ValidInt64) companyId := c.Locals("company_id").(domain.ValidInt64)
// Checking to make sure that admin user has a company id in the token
if role != domain.RoleSuperAdmin && !companyId.Valid { if role != domain.RoleSuperAdmin && !companyId.Valid {
return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID") return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID")
} }
@ -182,32 +183,38 @@ func (h *Handler) GetAllManagers(c *fiber.Ctx) error {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /managers/{id} [get] // @Router /managers/{id} [get]
func (h *Handler) GetManagerByID(c *fiber.Ctx) error { func (h *Handler) GetManagerByID(c *fiber.Ctx) error {
// branchId := int64(12) //c.Locals("branch_id").(int64) role := c.Locals("role").(domain.Role)
// filter := user.Filter{ companyId := c.Locals("company_id").(domain.ValidInt64)
// Role: string(domain.RoleUser), requestUserID := c.Locals("user_id").(int64)
// BranchId: user.ValidBranchId{
// Value: branchId, // Only Super Admin / Admin / Branch Manager can view this route
// Valid: true, if role != domain.RoleSuperAdmin && role != domain.RoleAdmin && role != domain.RoleBranchManager {
// }, return fiber.NewError(fiber.StatusUnauthorized, "Role Unauthorized")
// Page: c.QueryInt("page", 1), }
// PageSize: c.QueryInt("page_size", 10),
// } if role != domain.RoleSuperAdmin && !companyId.Valid {
// valErrs, ok := validator.Validate(c, filter) return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID")
// if !ok { }
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
// }
userIDstr := c.Params("id") userIDstr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64) userID, err := strconv.ParseInt(userIDstr, 10, 64)
if err != nil { if err != nil {
h.logger.Error("failed to fetch user using UserID", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid managers ID")
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid managers ID", nil, nil)
} }
user, err := h.userSvc.GetUserByID(c.Context(), userID) user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil { if err != nil {
h.logger.Error("Get User By ID failed", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get managers")
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get managers", nil, nil) }
// A Branch Manager can only fetch his own branch info
if role == domain.RoleBranchManager && user.ID != requestUserID {
return fiber.NewError(fiber.StatusBadRequest, "User Access Not Allowed")
}
// Check that only admin from company can view this route
if role != domain.RoleSuperAdmin && user.CompanyID.Value != companyId.Value {
return fiber.NewError(fiber.StatusBadRequest, "Only company user can view manager info")
} }
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
@ -259,7 +266,9 @@ type updateManagerReq struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /managers/{id} [put] // @Router /managers/{id} [put]
func (h *Handler) UpdateManagers(c *fiber.Ctx) error { func (h *Handler) UpdateManagers(c *fiber.Ctx) error {
var req updateManagerReq var req updateManagerReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.logger.Error("UpdateManagers failed", "error", err) h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)

View File

@ -12,7 +12,7 @@ import (
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 {
client, err := mongo.Connect(appCtx, options.Client().ApplyURI("mongodb://root:secret@mongo:27017/?authSource=admin")) client, err := mongo.Connect(appCtx, options.Client().ApplyURI("mongodb://root:secret@localhost:27017/?authSource=admin"))
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())
} }
@ -32,7 +32,6 @@ 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) return c.JSON(logs)
} }
} }

View File

@ -15,7 +15,7 @@ type TransferWalletRes struct {
Verified bool `json:"verified" example:"true"` Verified bool `json:"verified" example:"true"`
Type string `json:"type" example:"transfer"` Type string `json:"type" example:"transfer"`
PaymentMethod string `json:"payment_method" example:"bank"` PaymentMethod string `json:"payment_method" example:"bank"`
ReceiverWalletID int64 `json:"receiver_wallet_id" example:"1"` ReceiverWalletID *int64 `json:"receiver_wallet_id" example:"1"`
SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` SenderWalletID *int64 `json:"sender_wallet_id" example:"1"`
CashierID *int64 `json:"cashier_id" example:"789"` CashierID *int64 `json:"cashier_id" example:"789"`
CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"`
@ -27,7 +27,7 @@ type RefillRes struct {
Verified bool `json:"verified" example:"true"` Verified bool `json:"verified" example:"true"`
Type string `json:"type" example:"transfer"` Type string `json:"type" example:"transfer"`
PaymentMethod string `json:"payment_method" example:"bank"` PaymentMethod string `json:"payment_method" example:"bank"`
ReceiverWalletID int64 `json:"receiver_wallet_id" example:"1"` ReceiverWalletID *int64 `json:"receiver_wallet_id" example:"1"`
SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` SenderWalletID *int64 `json:"sender_wallet_id" example:"1"`
CashierID *int64 `json:"cashier_id" example:"789"` CashierID *int64 `json:"cashier_id" example:"789"`
CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"`
@ -35,13 +35,20 @@ type RefillRes struct {
} }
func convertTransfer(transfer domain.Transfer) TransferWalletRes { func convertTransfer(transfer domain.Transfer) TransferWalletRes {
var senderWalletID *int64
senderWalletID = &transfer.SenderWalletID
var cashierID *int64 var cashierID *int64
if transfer.CashierID.Valid { if transfer.CashierID.Valid {
cashierID = &transfer.CashierID.Value cashierID = &transfer.CashierID.Value
} }
var receiverID *int64
if transfer.ReceiverWalletID.Valid {
receiverID = &transfer.ReceiverWalletID.Value
}
var senderId *int64
if transfer.SenderWalletID.Valid {
senderId = &transfer.SenderWalletID.Value
}
return TransferWalletRes{ return TransferWalletRes{
ID: transfer.ID, ID: transfer.ID,
@ -49,8 +56,8 @@ func convertTransfer(transfer domain.Transfer) TransferWalletRes {
Verified: transfer.Verified, Verified: transfer.Verified,
Type: string(transfer.Type), Type: string(transfer.Type),
PaymentMethod: string(transfer.PaymentMethod), PaymentMethod: string(transfer.PaymentMethod),
ReceiverWalletID: transfer.ReceiverWalletID, ReceiverWalletID: receiverID,
SenderWalletID: senderWalletID, SenderWalletID: senderId,
CashierID: cashierID, CashierID: cashierID,
CreatedAt: transfer.CreatedAt, CreatedAt: transfer.CreatedAt,
UpdatedAt: transfer.UpdatedAt, UpdatedAt: transfer.UpdatedAt,
@ -126,16 +133,16 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error {
} }
// Get sender ID from the cashier // Get sender ID from the cashier
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
role := string(c.Locals("role").(domain.Role)) role := c.Locals("role").(domain.Role)
companyID := c.Locals("company_id").(int64) companyID := c.Locals("company_id").(int64)
var senderID int64 var senderID int64
//TODO: check to make sure that the cashiers aren't transferring TO branch wallet //TODO: check to make sure that the cashiers aren't transferring TO branch wallet
if role == string(domain.RoleCustomer) { if role == domain.RoleCustomer {
h.logger.Error("Unauthorized access", "userID", userID, "role", role) h.logger.Error("Unauthorized access", "userID", userID, "role", role)
return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil)
} else if role == string(domain.RoleBranchManager) || role == string(domain.RoleAdmin) || role == string(domain.RoleSuperAdmin) { } else if role == domain.RoleBranchManager || role == domain.RoleAdmin || role == domain.RoleSuperAdmin {
company, err := h.companySvc.GetCompanyByID(c.Context(), companyID) company, err := h.companySvc.GetCompanyByID(c.Context(), companyID)
if err != nil { if err != nil {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching company", err, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching company", err, nil)
@ -190,6 +197,7 @@ func (h *Handler) RefillWallet(c *fiber.Ctx) error {
receiverIDString := c.Params("id") receiverIDString := c.Params("id")
userID := c.Locals("user_id").(int64)
receiverID, err := strconv.ParseInt(receiverIDString, 10, 64) receiverID, err := strconv.ParseInt(receiverIDString, 10, 64)
if err != nil { if err != nil {
@ -197,13 +205,6 @@ func (h *Handler) RefillWallet(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil)
} }
// Get sender ID from the cashier // Get sender ID from the cashier
userID := c.Locals("user_id").(int64)
role := string(c.Locals("role").(domain.Role))
if role != string(domain.RoleSuperAdmin) {
h.logger.Error("Unauthorized access", "userID", userID, "role", role)
return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil)
}
var req CreateRefillReq var req CreateRefillReq
@ -217,16 +218,11 @@ func (h *Handler) RefillWallet(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
} }
transfer, err := h.walletSvc.RefillWallet(c.Context(), domain.CreateTransfer{ transfer, err := h.walletSvc.AddToWallet(
Amount: domain.ToCurrency(req.Amount), c.Context(), receiverID, domain.ToCurrency(req.Amount), domain.ValidInt64{
PaymentMethod: domain.TRANSFER_BANK,
ReceiverWalletID: receiverID,
CashierID: domain.ValidInt64{
Value: userID, Value: userID,
Valid: true, Valid: true,
}, }, domain.TRANSFER_BANK, domain.PaymentDetails{})
Type: domain.TransferType("deposit"),
})
if !ok { if !ok {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Creating Transfer Failed", err, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "Creating Transfer Failed", err, nil)

View File

@ -192,7 +192,8 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
} }
// TODO: Remove later // TODO: Remove later
err = h.walletSvc.AddToWallet(c.Context(), newWallet.ID, domain.ToCurrency(100.0)) _, err = h.walletSvc.AddToWallet(
c.Context(), newWallet.ID, domain.ToCurrency(100.0), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { if err != nil {
h.logger.Error("Failed to update wallet for user", "userID", newUser.ID, "error", err) h.logger.Error("Failed to update wallet for user", "userID", newUser.ID, "error", err)
@ -413,6 +414,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
return nil return nil
} }
companyID := c.Locals("company_id").(domain.ValidInt64) companyID := c.Locals("company_id").(domain.ValidInt64)
users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, req.Role, companyID) users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, req.Role, companyID)
if err != nil { if err != nil {
h.logger.Error("SearchUserByNameOrPhone failed", "error", err) h.logger.Error("SearchUserByNameOrPhone failed", "error", err)

View File

@ -87,6 +87,22 @@ func (a *App) CompanyOnly(c *fiber.Ctx) error {
return c.Next() return c.Next()
} }
func (a *App) OnlyAdminAndAbove(c *fiber.Ctx) error {
userRole := c.Locals("role").(domain.Role)
if userRole != domain.RoleSuperAdmin && userRole != domain.RoleAdmin {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid access token")
}
return c.Next()
}
func (a *App) OnlyBranchManagerAndAbove(c *fiber.Ctx) error {
userRole := c.Locals("role").(domain.Role)
if userRole != domain.RoleSuperAdmin && userRole != domain.RoleAdmin && userRole != domain.RoleBranchManager {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid access token")
}
return c.Next()
}
func (a *App) WebsocketAuthMiddleware(c *fiber.Ctx) error { func (a *App) WebsocketAuthMiddleware(c *fiber.Ctx) error {
tokenStr := c.Query("token") tokenStr := c.Query("token")
if tokenStr == "" { if tokenStr == "" {

View File

@ -189,7 +189,7 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/wallet/:id", h.GetWalletByID) a.fiber.Get("/wallet/:id", h.GetWalletByID)
a.fiber.Put("/wallet/:id", h.UpdateWalletActive) a.fiber.Put("/wallet/:id", h.UpdateWalletActive)
a.fiber.Get("/branchWallet", a.authMiddleware, h.GetAllBranchWallets) a.fiber.Get("/branchWallet", a.authMiddleware, h.GetAllBranchWallets)
a.fiber.Get("/cashierWallet", a.authMiddleware, h.GetWalletForCashier) a.fiber.Get("/cashierWallet", a.authMiddleware, h.GetWalletForCashier)
// Transfer // Transfer
// /transfer/wallet - transfer from one wallet to another wallet // /transfer/wallet - transfer from one wallet to another wallet

View File

@ -0,0 +1,2 @@
time=2025-06-16T02:21:34.859+03:00 level=INFO msg="Authenticated WebSocket connection" service_info.env=development userID=3
time=2025-06-16T02:23:59.721+03:00 level=INFO msg="Starting server" service_info.env=development port=8080