feat: referal completed

This commit is contained in:
dawitel 2025-04-12 03:27:50 +03:00
parent 49a97484c7
commit f796b97afe
25 changed files with 2239 additions and 1015 deletions

View File

@ -1,4 +1,5 @@
# FortuneBet-Backend
# Directory Structure # Directory Structure
├── cmd ├── cmd
@ -9,13 +10,19 @@
│ │ ├── 000001_fortune.up.sql │ │ ├── 000001_fortune.up.sql
│ │ ├── 000002_notification.down.sql │ │ ├── 000002_notification.down.sql
│ │ ├── 000002_notification.up.sql │ │ ├── 000002_notification.up.sql
│ │ ├── 000003_referal.down.sql
│ │ ├── 000003_referal.up.sql
│ └── query │ └── query
│ ├── auth.sql │ ├── auth.sql
│ ├── bet.sql │ ├── bet.sql
│ ├── notification.sql │ ├── notification.sql
│ ├── otp.sql │ ├── otp.sql
│ ├── referal.sql
│ ├── ticket.sql │ ├── ticket.sql
│ ├── transactions.sql
│ ├── transfer.sql
│ ├── user.sql │ ├── user.sql
│ ├── wallet.sql
├── docs ├── docs
│ ├── docs.go │ ├── docs.go
│ ├── swagger.json │ ├── swagger.json
@ -28,8 +35,12 @@
│ ├── models.go │ ├── models.go
│ ├── notification.sql.go │ ├── notification.sql.go
│ ├── otp.sql.go │ ├── otp.sql.go
│ ├── referal.sql.go
│ ├── ticket.sql.go │ ├── ticket.sql.go
│ ├── transactions.sql.go
│ ├── transfer.sql.go
│ ├── user.sql.go │ ├── user.sql.go
│ ├── wallet.sql.go
└── internal └── internal
├── config ├── config
│ ├── config.go │ ├── config.go
@ -41,9 +52,13 @@
│ ├── event.go │ ├── event.go
│ ├── notification.go │ ├── notification.go
│ ├── otp.go │ ├── otp.go
│ ├── referal.go
│ ├── role.go │ ├── role.go
│ ├── ticket.go │ ├── ticket.go
│ ├── transaction.go
│ ├── transfer.go
│ ├── user.go │ ├── user.go
│ ├── wallet.go
├── logger ├── logger
│ ├── logger.go │ ├── logger.go
├── mocks ├── mocks
@ -59,9 +74,13 @@
│ ├── bet.go │ ├── bet.go
│ ├── notification.go │ ├── notification.go
│ ├── otp.go │ ├── otp.go
│ ├── referal.go
│ ├── store.go │ ├── store.go
│ ├── ticket.go │ ├── ticket.go
│ ├── transaction.go
│ ├── transfer.go
│ ├── user.go │ ├── user.go
│ ├── wallet.go
├── services ├── services
│ ├── authentication │ ├── authentication
│ │ ├── impl.go │ │ ├── impl.go
@ -73,6 +92,9 @@
│ ├── notfication │ ├── notfication
│ │ ├── port.go │ │ ├── port.go
│ │ ├── service.go │ │ ├── service.go
│ ├── referal
│ │ ├── port.go
│ │ ├── service.go
│ ├── sportsbook │ ├── sportsbook
│ │ ├── events.go │ │ ├── events.go
│ │ ├── odds.go │ │ ├── odds.go
@ -80,20 +102,33 @@
│ ├── ticket │ ├── ticket
│ │ ├── port.go │ │ ├── port.go
│ │ ├── service.go │ │ ├── service.go
│ └── user │ ├── transaction
│ ├── common.go │ │ ├── port.go
│ │ ├── service.go
│ ├── transfer
│ │ ├── chapa.go
│ │ ├── port.go
│ │ ├── service.go
│ ├── user
│ │ ├── common.go
│ │ ├── port.go
│ │ ├── register.go
│ │ ├── reset.go
│ │ ├── service.go
│ │ ├── user.go
│ └── wallet
│ ├── port.go │ ├── port.go
│ ├── register.go
│ ├── reset.go
│ ├── service.go │ ├── service.go
│ ├── user.go
└── web_server └── web_server
├── handlers ├── handlers
│ ├── auth_handler.go │ ├── auth_handler.go
│ ├── bet_handler.go │ ├── bet_handler.go
│ ├── handlers.go
│ ├── notification_handler.go │ ├── notification_handler.go
│ ├── ticket_handler.go │ ├── ticket_handler.go
│ ├── transaction_handler.go
│ ├── user.go │ ├── user.go
│ ├── wallet_handler.go
├── jwt ├── jwt
│ ├── jwt.go │ ├── jwt.go
│ ├── jwt_test.go │ ├── jwt_test.go

View File

@ -149,6 +149,3 @@ INSERT INTO users (
NULL, NULL,
FALSE FALSE
); );

View File

@ -0,0 +1,17 @@
DROP TABLE IF EXISTS referrals;
DROP TABLE IF EXISTS referral_settings;
DROP TYPE IF EXISTS ReferralStatus;
ALTER TABLE users
DROP COLUMN referral_code;
ALTER TABLE users
DROP COLUMN referred_by;
ALTER TABLE wallet
DROP COLUMN bonus_balance;
ALTER TABLE wallet
DROP COLUMN cash_balance;

View File

@ -0,0 +1,53 @@
CREATE TYPE ReferralStatus AS ENUM ('PENDING', 'COMPLETED', 'EXPIRED', 'CANCELLED');
CREATE TABLE IF NOT EXISTS referral_settings (
id BIGSERIAL PRIMARY KEY,
referral_reward_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
cashback_percentage DECIMAL(5, 2) NOT NULL DEFAULT 0.00,
bet_referral_bonus_percentage NUMERIC DEFAULT 5.0,
max_referrals INTEGER NOT NULL DEFAULT 0,
expires_after_days INTEGER NOT NULL DEFAULT 30,
updated_by VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
version INTEGER NOT NULL DEFAULT 0,
CONSTRAINT referral_reward_amount_positive CHECK (referral_reward_amount >= 0),
CONSTRAINT cashback_percentage_range CHECK (
cashback_percentage >= 0
AND cashback_percentage <= 100
)
);
CREATE TABLE IF NOT EXISTS referrals (
id BIGSERIAL PRIMARY KEY,
referral_code VARCHAR(10) NOT NULL UNIQUE,
referrer_id VARCHAR(255) NOT NULL,
referred_id VARCHAR(255) UNIQUE,
status ReferralStatus NOT NULL DEFAULT 'PENDING',
reward_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
cashback_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ NOT NULL,
FOREIGN KEY (referrer_id) REFERENCES users (id),
FOREIGN KEY (referred_id) REFERENCES users (id),
CONSTRAINT reward_amount_positive CHECK (reward_amount >= 0),
CONSTRAINT cashback_amount_positive CHECK (cashback_amount >= 0)
);
CREATE INDEX idx_referrals_referral_code ON referrals (referral_code);
CREATE INDEX idx_referrals_referrer_id ON referrals (referrer_id);
CREATE INDEX idx_referrals_status ON referrals (status);
ALTER TABLE users
ADD COLUMN IF NOT EXISTS referral_code VARCHAR(10) UNIQUE,
ADD COLUMN IF NOT EXISTS referred_by VARCHAR(10);
-- Modify wallet table to track bonus money separately
ALTER TABLE wallets
ADD COLUMN IF NOT EXISTS bonus_balance DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
ADD COLUMN IF NOT EXISTS cash_balance DECIMAL(15, 2) NOT NULL DEFAULT 0.00,
ADD CONSTRAINT bonus_balance_positive CHECK (bonus_balance >= 0),
ADD CONSTRAINT cash_balance_positive CHECK (cash_balance >= 0);

65
db/query/referal.sql Normal file
View File

@ -0,0 +1,65 @@
-- name: CreateReferral :one
INSERT INTO referrals (
referral_code,
referrer_id,
status,
reward_amount,
expires_at
) VALUES (
$1, $2, $3, $4, $5
) RETURNING *;
-- name: GetReferralByCode :one
SELECT * FROM referrals
WHERE referral_code = $1;
-- name: UpdateReferral :one
UPDATE referrals
SET
referred_id = $2,
status = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING *;
-- name: GetReferralStats :one
SELECT
COUNT(*) as total_referrals,
COUNT(CASE WHEN status = 'COMPLETED' THEN 1 END) as completed_referrals,
COALESCE(SUM(reward_amount), 0) as total_reward_earned,
COALESCE(SUM(CASE WHEN status = 'PENDING' THEN reward_amount END), 0) as pending_rewards
FROM referrals
WHERE referrer_id = $1;
-- name: GetReferralSettings :one
SELECT * FROM referral_settings
WHERE id = 'default'
LIMIT 1;
-- name: UpdateReferralSettings :one
UPDATE referral_settings
SET
referral_reward_amount = $2,
cashback_percentage = $3,
bet_referral_bonus_percentage= $4,
max_referrals = $5,
expires_after_days = $6,
updated_by = $7,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING *;
-- name: CreateReferralSettings :one
INSERT INTO referral_settings (
referral_reward_amount,
cashback_percentage,
max_referrals,
bet_referral_bonus_percentage,
expires_after_days,
updated_by
) VALUES (
$1, $2, $3, $4, $5, $6
) RETURNING *;
-- name: GetReferralByReferredID :one
SELECT * FROM referrals WHERE referred_id = $1 LIMIT 1;

View File

@ -55,7 +55,7 @@ func (q *Queries) GetRefreshToken(ctx context.Context, token string) (RefreshTok
} }
const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one
SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, suspended_at, suspended FROM users SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, suspended_at, suspended, referral_code, referred_by FROM users
WHERE email = $1 OR phone_number = $2 WHERE email = $1 OR phone_number = $2
` `
@ -81,6 +81,8 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
&i.UpdatedAt, &i.UpdatedAt,
&i.SuspendedAt, &i.SuspendedAt,
&i.Suspended, &i.Suspended,
&i.ReferralCode,
&i.ReferredBy,
) )
return i, err return i, err
} }

View File

@ -5,9 +5,56 @@
package dbgen package dbgen
import ( import (
"database/sql/driver"
"fmt"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
type Referralstatus string
const (
ReferralstatusPENDING Referralstatus = "PENDING"
ReferralstatusCOMPLETED Referralstatus = "COMPLETED"
ReferralstatusEXPIRED Referralstatus = "EXPIRED"
ReferralstatusCANCELLED Referralstatus = "CANCELLED"
)
func (e *Referralstatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = Referralstatus(s)
case string:
*e = Referralstatus(s)
default:
return fmt.Errorf("unsupported scan type for Referralstatus: %T", src)
}
return nil
}
type NullReferralstatus struct {
Referralstatus Referralstatus
Valid bool // Valid is true if Referralstatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullReferralstatus) Scan(value interface{}) error {
if value == nil {
ns.Referralstatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.Referralstatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullReferralstatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.Referralstatus), nil
}
type Bet struct { type Bet struct {
ID int64 ID int64
Amount int64 Amount int64
@ -62,6 +109,32 @@ type Otp struct {
ExpiresAt pgtype.Timestamptz ExpiresAt pgtype.Timestamptz
} }
type Referral struct {
ID int64
ReferralCode string
ReferrerID string
ReferredID pgtype.Text
Status Referralstatus
RewardAmount pgtype.Numeric
CashbackAmount pgtype.Numeric
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
ExpiresAt pgtype.Timestamptz
}
type ReferralSetting struct {
ID int64
ReferralRewardAmount pgtype.Numeric
CashbackPercentage pgtype.Numeric
BetReferralBonusPercentage pgtype.Numeric
MaxReferrals int32
ExpiresAfterDays int32
UpdatedBy string
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
Version int32
}
type RefreshToken struct { type RefreshToken struct {
ID int64 ID int64
UserID int64 UserID int64
@ -112,6 +185,8 @@ type User struct {
UpdatedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz
SuspendedAt pgtype.Timestamptz SuspendedAt pgtype.Timestamptz
Suspended bool Suspended bool
ReferralCode pgtype.Text
ReferredBy pgtype.Text
} }
type Wallet struct { type Wallet struct {
@ -123,6 +198,8 @@ type Wallet struct {
IsActive bool IsActive bool
CreatedAt pgtype.Timestamp CreatedAt pgtype.Timestamp
UpdatedAt pgtype.Timestamp UpdatedAt pgtype.Timestamp
BonusBalance pgtype.Numeric
CashBalance pgtype.Numeric
} }
type WalletTransfer struct { type WalletTransfer struct {

285
gen/db/referal.sql.go Normal file
View File

@ -0,0 +1,285 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// source: referal.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateReferral = `-- name: CreateReferral :one
INSERT INTO referrals (
referral_code,
referrer_id,
status,
reward_amount,
expires_at
) VALUES (
$1, $2, $3, $4, $5
) RETURNING id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at
`
type CreateReferralParams struct {
ReferralCode string
ReferrerID string
Status Referralstatus
RewardAmount pgtype.Numeric
ExpiresAt pgtype.Timestamptz
}
func (q *Queries) CreateReferral(ctx context.Context, arg CreateReferralParams) (Referral, error) {
row := q.db.QueryRow(ctx, CreateReferral,
arg.ReferralCode,
arg.ReferrerID,
arg.Status,
arg.RewardAmount,
arg.ExpiresAt,
)
var i Referral
err := row.Scan(
&i.ID,
&i.ReferralCode,
&i.ReferrerID,
&i.ReferredID,
&i.Status,
&i.RewardAmount,
&i.CashbackAmount,
&i.CreatedAt,
&i.UpdatedAt,
&i.ExpiresAt,
)
return i, err
}
const CreateReferralSettings = `-- name: CreateReferralSettings :one
INSERT INTO referral_settings (
referral_reward_amount,
cashback_percentage,
max_referrals,
bet_referral_bonus_percentage,
expires_after_days,
updated_by
) VALUES (
$1, $2, $3, $4, $5, $6
) RETURNING id, referral_reward_amount, cashback_percentage, bet_referral_bonus_percentage, max_referrals, expires_after_days, updated_by, created_at, updated_at, version
`
type CreateReferralSettingsParams struct {
ReferralRewardAmount pgtype.Numeric
CashbackPercentage pgtype.Numeric
MaxReferrals int32
BetReferralBonusPercentage pgtype.Numeric
ExpiresAfterDays int32
UpdatedBy string
}
func (q *Queries) CreateReferralSettings(ctx context.Context, arg CreateReferralSettingsParams) (ReferralSetting, error) {
row := q.db.QueryRow(ctx, CreateReferralSettings,
arg.ReferralRewardAmount,
arg.CashbackPercentage,
arg.MaxReferrals,
arg.BetReferralBonusPercentage,
arg.ExpiresAfterDays,
arg.UpdatedBy,
)
var i ReferralSetting
err := row.Scan(
&i.ID,
&i.ReferralRewardAmount,
&i.CashbackPercentage,
&i.BetReferralBonusPercentage,
&i.MaxReferrals,
&i.ExpiresAfterDays,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.Version,
)
return i, err
}
const GetReferralByCode = `-- name: GetReferralByCode :one
SELECT id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at FROM referrals
WHERE referral_code = $1
`
func (q *Queries) GetReferralByCode(ctx context.Context, referralCode string) (Referral, error) {
row := q.db.QueryRow(ctx, GetReferralByCode, referralCode)
var i Referral
err := row.Scan(
&i.ID,
&i.ReferralCode,
&i.ReferrerID,
&i.ReferredID,
&i.Status,
&i.RewardAmount,
&i.CashbackAmount,
&i.CreatedAt,
&i.UpdatedAt,
&i.ExpiresAt,
)
return i, err
}
const GetReferralByReferredID = `-- name: GetReferralByReferredID :one
SELECT id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at FROM referrals WHERE referred_id = $1 LIMIT 1
`
func (q *Queries) GetReferralByReferredID(ctx context.Context, referredID pgtype.Text) (Referral, error) {
row := q.db.QueryRow(ctx, GetReferralByReferredID, referredID)
var i Referral
err := row.Scan(
&i.ID,
&i.ReferralCode,
&i.ReferrerID,
&i.ReferredID,
&i.Status,
&i.RewardAmount,
&i.CashbackAmount,
&i.CreatedAt,
&i.UpdatedAt,
&i.ExpiresAt,
)
return i, err
}
const GetReferralSettings = `-- name: GetReferralSettings :one
SELECT id, referral_reward_amount, cashback_percentage, bet_referral_bonus_percentage, max_referrals, expires_after_days, updated_by, created_at, updated_at, version FROM referral_settings
WHERE id = 'default'
LIMIT 1
`
func (q *Queries) GetReferralSettings(ctx context.Context) (ReferralSetting, error) {
row := q.db.QueryRow(ctx, GetReferralSettings)
var i ReferralSetting
err := row.Scan(
&i.ID,
&i.ReferralRewardAmount,
&i.CashbackPercentage,
&i.BetReferralBonusPercentage,
&i.MaxReferrals,
&i.ExpiresAfterDays,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.Version,
)
return i, err
}
const GetReferralStats = `-- name: GetReferralStats :one
SELECT
COUNT(*) as total_referrals,
COUNT(CASE WHEN status = 'COMPLETED' THEN 1 END) as completed_referrals,
COALESCE(SUM(reward_amount), 0) as total_reward_earned,
COALESCE(SUM(CASE WHEN status = 'PENDING' THEN reward_amount END), 0) as pending_rewards
FROM referrals
WHERE referrer_id = $1
`
type GetReferralStatsRow struct {
TotalReferrals int64
CompletedReferrals int64
TotalRewardEarned float64
PendingRewards float64
}
func (q *Queries) GetReferralStats(ctx context.Context, referrerID string) (GetReferralStatsRow, error) {
row := q.db.QueryRow(ctx, GetReferralStats, referrerID)
var i GetReferralStatsRow
err := row.Scan(
&i.TotalReferrals,
&i.CompletedReferrals,
&i.TotalRewardEarned,
&i.PendingRewards,
)
return i, err
}
const UpdateReferral = `-- name: UpdateReferral :one
UPDATE referrals
SET
referred_id = $2,
status = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING id, referral_code, referrer_id, referred_id, status, reward_amount, cashback_amount, created_at, updated_at, expires_at
`
type UpdateReferralParams struct {
ID int64
ReferredID pgtype.Text
Status Referralstatus
}
func (q *Queries) UpdateReferral(ctx context.Context, arg UpdateReferralParams) (Referral, error) {
row := q.db.QueryRow(ctx, UpdateReferral, arg.ID, arg.ReferredID, arg.Status)
var i Referral
err := row.Scan(
&i.ID,
&i.ReferralCode,
&i.ReferrerID,
&i.ReferredID,
&i.Status,
&i.RewardAmount,
&i.CashbackAmount,
&i.CreatedAt,
&i.UpdatedAt,
&i.ExpiresAt,
)
return i, err
}
const UpdateReferralSettings = `-- name: UpdateReferralSettings :one
UPDATE referral_settings
SET
referral_reward_amount = $2,
cashback_percentage = $3,
bet_referral_bonus_percentage= $4,
max_referrals = $5,
expires_after_days = $6,
updated_by = $7,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
RETURNING id, referral_reward_amount, cashback_percentage, bet_referral_bonus_percentage, max_referrals, expires_after_days, updated_by, created_at, updated_at, version
`
type UpdateReferralSettingsParams struct {
ID int64
ReferralRewardAmount pgtype.Numeric
CashbackPercentage pgtype.Numeric
BetReferralBonusPercentage pgtype.Numeric
MaxReferrals int32
ExpiresAfterDays int32
UpdatedBy string
}
func (q *Queries) UpdateReferralSettings(ctx context.Context, arg UpdateReferralSettingsParams) (ReferralSetting, error) {
row := q.db.QueryRow(ctx, UpdateReferralSettings,
arg.ID,
arg.ReferralRewardAmount,
arg.CashbackPercentage,
arg.BetReferralBonusPercentage,
arg.MaxReferrals,
arg.ExpiresAfterDays,
arg.UpdatedBy,
)
var i ReferralSetting
err := row.Scan(
&i.ID,
&i.ReferralRewardAmount,
&i.CashbackPercentage,
&i.BetReferralBonusPercentage,
&i.MaxReferrals,
&i.ExpiresAfterDays,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.Version,
)
return i, err
}

View File

@ -193,7 +193,7 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUse
} }
const GetUserByID = `-- name: GetUserByID :one const GetUserByID = `-- name: GetUserByID :one
SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, suspended_at, suspended SELECT id, first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at, suspended_at, suspended, referral_code, referred_by
FROM users FROM users
WHERE id = $1 WHERE id = $1
` `
@ -215,6 +215,8 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
&i.UpdatedAt, &i.UpdatedAt,
&i.SuspendedAt, &i.SuspendedAt,
&i.Suspended, &i.Suspended,
&i.ReferralCode,
&i.ReferredBy,
) )
return i, err return i, err
} }

View File

@ -43,7 +43,7 @@ func (q *Queries) CreateCustomerWallet(ctx context.Context, arg CreateCustomerWa
} }
const CreateWallet = `-- name: CreateWallet :one const CreateWallet = `-- name: CreateWallet :one
INSERT INTO wallets (is_withdraw, is_bettable, user_id) VALUES ($1, $2, $3) RETURNING id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at INSERT INTO wallets (is_withdraw, is_bettable, user_id) VALUES ($1, $2, $3) RETURNING id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance
` `
type CreateWalletParams struct { type CreateWalletParams struct {
@ -64,12 +64,14 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.BonusBalance,
&i.CashBalance,
) )
return i, err return i, err
} }
const GetAllWallets = `-- name: GetAllWallets :many const GetAllWallets = `-- name: GetAllWallets :many
SELECT id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at FROM wallets SELECT id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance FROM wallets
` `
func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) {
@ -90,6 +92,8 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) {
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.BonusBalance,
&i.CashBalance,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -156,7 +160,7 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, arg GetCustomerWalletPa
} }
const GetWalletByID = `-- name: GetWalletByID :one const GetWalletByID = `-- name: GetWalletByID :one
SELECT id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at FROM wallets WHERE id = $1 SELECT id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance FROM wallets WHERE id = $1
` `
func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) {
@ -171,12 +175,14 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) {
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.BonusBalance,
&i.CashBalance,
) )
return i, err return i, err
} }
const GetWalletByUserID = `-- name: GetWalletByUserID :many const GetWalletByUserID = `-- name: GetWalletByUserID :many
SELECT id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at FROM wallets WHERE user_id = $1 SELECT id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance FROM wallets WHERE user_id = $1
` `
func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet, error) { func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet, error) {
@ -197,6 +203,8 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.BonusBalance,
&i.CashBalance,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@ -0,0 +1,65 @@
package domain
import (
"database/sql/driver"
"fmt"
"time"
)
type ReferralStatus string
const (
ReferralPending ReferralStatus = "PENDING"
ReferralCompleted ReferralStatus = "COMPLETED"
ReferralExpired ReferralStatus = "EXPIRED"
ReferralCancelled ReferralStatus = "CANCELLED"
)
func (rs *ReferralStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*rs = ReferralStatus(s)
case string:
*rs = ReferralStatus(s)
default:
return fmt.Errorf("unsupported scan type for ReferralStatus: %T", src)
}
return nil
}
func (rs ReferralStatus) Value() (driver.Value, error) {
return string(rs), nil
}
type ReferralStats struct {
TotalReferrals int
CompletedReferrals int
TotalRewardEarned float64
PendingRewards float64
}
type ReferralSettings struct {
ID int64
ReferralRewardAmount float64
CashbackPercentage float64
BetReferralBonusPercentage float64
MaxReferrals int32
ExpiresAfterDays int32
UpdatedBy string
CreatedAt time.Time
UpdatedAt time.Time
Version int32
}
type Referral struct {
ID int64
ReferralCode string
ReferrerID string
ReferredID *string
Status ReferralStatus
RewardAmount float64
CashbackAmount float64
CreatedAt time.Time
UpdatedAt time.Time
ExpiresAt time.Time
}

View File

@ -35,8 +35,7 @@ type RegisterUserReq struct {
Password string Password string
//Role string //Role string
Otp string Otp string
ReferalCode string ReferralCode string `json:"referral_code"`
//
OtpMedium OtpMedium OtpMedium OtpMedium
} }
type ResetPasswordReq struct { type ResetPasswordReq struct {

View File

@ -0,0 +1,240 @@
package repository
import (
"context"
"database/sql"
"errors"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
type ReferralRepository interface {
CreateReferral(ctx context.Context, referral *domain.Referral) error
GetReferralByCode(ctx context.Context, code string) (*domain.Referral, error)
UpdateReferral(ctx context.Context, referral *domain.Referral) error
GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error)
GetSettings(ctx context.Context) (*domain.ReferralSettings, error)
UpdateSettings(ctx context.Context, settings *domain.ReferralSettings) error
CreateSettings(ctx context.Context, settings *domain.ReferralSettings) error
GetReferralByReferredID(ctx context.Context, referredID string) (*domain.Referral, error) // New method
}
type ReferralRepo struct {
store *Store
}
func NewReferralRepository(store *Store) ReferralRepository {
return &ReferralRepo{store: store}
}
func (r *ReferralRepo) CreateReferral(ctx context.Context, referral *domain.Referral) error {
rewardAmount := pgtype.Numeric{}
if err := rewardAmount.Scan(referral.RewardAmount); err != nil {
return err
}
params := dbgen.CreateReferralParams{
ReferralCode: referral.ReferralCode,
ReferrerID: referral.ReferrerID,
Status: dbgen.Referralstatus(referral.Status),
RewardAmount: rewardAmount,
ExpiresAt: pgtype.Timestamptz{Time: referral.ExpiresAt, Valid: true},
}
_, err := r.store.queries.CreateReferral(ctx, params)
return err
}
func (r *ReferralRepo) GetReferralByCode(ctx context.Context, code string) (*domain.Referral, error) {
dbReferral, err := r.store.queries.GetReferralByCode(ctx, code)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return r.mapToDomainReferral(&dbReferral), nil
}
func (r *ReferralRepo) UpdateReferral(ctx context.Context, referral *domain.Referral) error {
var referredID pgtype.Text
if referral.ReferredID != nil {
referredID = pgtype.Text{String: *referral.ReferredID, Valid: true}
}
params := dbgen.UpdateReferralParams{
ID: referral.ID,
ReferredID: referredID,
Status: dbgen.Referralstatus(referral.Status),
}
_, err := r.store.queries.UpdateReferral(ctx, params)
return err
}
func (r *ReferralRepo) GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error) {
stats, err := r.store.queries.GetReferralStats(ctx, userID)
if err != nil {
return nil, err
}
return &domain.ReferralStats{
TotalReferrals: int(stats.TotalReferrals),
CompletedReferrals: int(stats.CompletedReferrals),
TotalRewardEarned: float64(stats.TotalRewardEarned),
PendingRewards: float64(stats.PendingRewards),
}, nil
}
func (r *ReferralRepo) GetSettings(ctx context.Context) (*domain.ReferralSettings, error) {
settings, err := r.store.queries.GetReferralSettings(ctx)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return r.mapToDomainSettings(&settings), nil
}
func (r *ReferralRepo) UpdateSettings(ctx context.Context, settings *domain.ReferralSettings) error {
rewardAmount := pgtype.Numeric{}
if err := rewardAmount.Scan(settings.ReferralRewardAmount); err != nil {
return err
}
cashbackPercentage := pgtype.Numeric{}
if err := cashbackPercentage.Scan(settings.CashbackPercentage); err != nil {
return err
}
betReferralBonusPercentage := pgtype.Numeric{}
if err := betReferralBonusPercentage.Scan(settings.BetReferralBonusPercentage); err != nil {
return err
}
params := dbgen.UpdateReferralSettingsParams{
ID: settings.ID,
ReferralRewardAmount: rewardAmount,
CashbackPercentage: cashbackPercentage,
BetReferralBonusPercentage: betReferralBonusPercentage, // New field
MaxReferrals: settings.MaxReferrals,
ExpiresAfterDays: settings.ExpiresAfterDays,
UpdatedBy: settings.UpdatedBy,
}
_, err := r.store.queries.UpdateReferralSettings(ctx, params)
return err
}
func (r *ReferralRepo) CreateSettings(ctx context.Context, settings *domain.ReferralSettings) error {
rewardAmount := pgtype.Numeric{}
if err := rewardAmount.Scan(settings.ReferralRewardAmount); err != nil {
return err
}
cashbackPercentage := pgtype.Numeric{}
if err := cashbackPercentage.Scan(settings.CashbackPercentage); err != nil {
return err
}
betReferralBonusPercentage := pgtype.Numeric{}
if err := betReferralBonusPercentage.Scan(settings.BetReferralBonusPercentage); err != nil {
return err
}
params := dbgen.CreateReferralSettingsParams{
ReferralRewardAmount: rewardAmount,
CashbackPercentage: cashbackPercentage,
BetReferralBonusPercentage: betReferralBonusPercentage, // New field
MaxReferrals: settings.MaxReferrals,
ExpiresAfterDays: settings.ExpiresAfterDays,
UpdatedBy: settings.UpdatedBy,
}
_, err := r.store.queries.CreateReferralSettings(ctx, params)
return err
}
func (r *ReferralRepo) GetReferralByReferredID(ctx context.Context, referredID string) (*domain.Referral, error) {
dbReferral, err := r.store.queries.GetReferralByReferredID(ctx, pgtype.Text{String: referredID, Valid: true})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return r.mapToDomainReferral(&dbReferral), nil
}
func (r *ReferralRepo) mapToDomainReferral(dbRef *dbgen.Referral) *domain.Referral {
var referredID *string
if dbRef.ReferredID.Valid {
referredID = &dbRef.ReferredID.String
}
rewardAmount := 0.0
if dbRef.RewardAmount.Valid {
if f8, err := dbRef.RewardAmount.Float64Value(); err == nil {
rewardAmount = f8.Float64
}
}
cashbackAmount := 0.0
if dbRef.CashbackAmount.Valid {
if f8, err := dbRef.CashbackAmount.Float64Value(); err == nil {
cashbackAmount = f8.Float64
}
}
return &domain.Referral{
ID: dbRef.ID,
ReferralCode: dbRef.ReferralCode,
ReferrerID: dbRef.ReferrerID,
ReferredID: referredID,
Status: domain.ReferralStatus(dbRef.Status),
RewardAmount: rewardAmount,
CashbackAmount: cashbackAmount,
CreatedAt: dbRef.CreatedAt.Time,
UpdatedAt: dbRef.UpdatedAt.Time,
ExpiresAt: dbRef.ExpiresAt.Time,
}
}
func (r *ReferralRepo) mapToDomainSettings(dbSettings *dbgen.ReferralSetting) *domain.ReferralSettings {
rewardAmount := 0.0
if dbSettings.ReferralRewardAmount.Valid {
if f8, err := dbSettings.ReferralRewardAmount.Float64Value(); err == nil {
rewardAmount = f8.Float64
}
}
cashbackPercentage := 0.0
if dbSettings.CashbackPercentage.Valid {
if f8, err := dbSettings.CashbackPercentage.Float64Value(); err == nil {
cashbackPercentage = f8.Float64
}
}
betReferralBonusPercentage := 0.0
if dbSettings.BetReferralBonusPercentage.Valid {
if f8, err := dbSettings.BetReferralBonusPercentage.Float64Value(); err == nil {
betReferralBonusPercentage = f8.Float64
}
}
return &domain.ReferralSettings{
ID: dbSettings.ID,
ReferralRewardAmount: rewardAmount,
CashbackPercentage: cashbackPercentage,
BetReferralBonusPercentage: betReferralBonusPercentage, // New field
MaxReferrals: dbSettings.MaxReferrals,
ExpiresAfterDays: dbSettings.ExpiresAfterDays,
UpdatedBy: dbSettings.UpdatedBy,
CreatedAt: dbSettings.CreatedAt.Time,
UpdatedAt: dbSettings.UpdatedAt.Time,
Version: dbSettings.Version,
}
}

View File

@ -0,0 +1,18 @@
package referralservice
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type ReferralStore interface {
GenerateReferralCode() (string, error)
CreateReferral(ctx context.Context, userID string) (*domain.Referral, error)
ProcessReferral(ctx context.Context, referredID, referralCode string) error
ProcessDepositBonus(ctx context.Context, userID string, amount float64) error
GetReferralStats(ctx context.Context, userID string) (*domain.ReferralStats, error)
UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error
GetReferralSettings(ctx context.Context) (*domain.ReferralSettings, error)
ProcessBetReferral(ctx context.Context, userPhone string, betAmount float64) error
}

View File

@ -0,0 +1,214 @@
package referralservice
import (
"context"
"crypto/rand"
"encoding/base32"
"errors"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
)
type Service struct {
repo repository.ReferralRepository
walletSvc wallet.Service
store *repository.Store
}
func NewService(repo repository.ReferralRepository, walletSvc wallet.Service, store *repository.Store) *Service {
return &Service{
repo: repo,
walletSvc: walletSvc,
store: store,
}
}
var (
ErrInvalidReferral = errors.New("invalid or expired referral")
ErrInvalidReferralSignup = errors.New("referral requires phone signup")
ErrUserNotFound = errors.New("user not found")
ErrNoReferralFound = errors.New("no referral found for this user")
)
func (s *Service) GenerateReferralCode() (string, error) {
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base32.StdEncoding.EncodeToString(b)[:10], nil
}
func (s *Service) CreateReferral(ctx context.Context, userPhone string) (*domain.Referral, error) {
settings, err := s.repo.GetSettings(ctx)
if err != nil {
return nil, err
}
code, err := s.GenerateReferralCode()
if err != nil {
return nil, err
}
userID, err := strconv.ParseInt(userPhone, 10, 64)
if err != nil {
return nil, errors.New("invalid phone number format")
}
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
if err != nil {
return nil, err
}
if len(wallets) == 0 {
_, err = s.walletSvc.CreateWallet(ctx, domain.CreateWallet{
IsWithdraw: true,
IsBettable: true,
UserID: userID,
})
if err != nil {
return nil, err
}
}
referral := &domain.Referral{
ReferrerID: userPhone,
ReferralCode: code,
Status: domain.ReferralPending,
RewardAmount: settings.ReferralRewardAmount,
ExpiresAt: time.Now().Add(time.Duration(settings.ExpiresAfterDays) * 24 * time.Hour),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.repo.CreateReferral(ctx, referral); err != nil {
return nil, err
}
return referral, nil
}
func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCode string) error {
referral, err := s.repo.GetReferralByCode(ctx, referralCode)
if err != nil {
return err
}
if referral == nil || referral.Status != domain.ReferralPending || referral.ExpiresAt.Before(time.Now()) {
return ErrInvalidReferral
}
user, err := s.store.GetUserByPhone(ctx, referredPhone)
if err != nil {
if errors.Is(err, domain.ErrUserNotFound) {
return ErrUserNotFound
}
return err
}
if !user.PhoneVerified {
return ErrInvalidReferralSignup
}
referral.ReferredID = &referredPhone
referral.Status = domain.ReferralCompleted
referral.UpdatedAt = time.Now()
if err := s.repo.UpdateReferral(ctx, referral); err != nil {
return err
}
referrerID, err := strconv.ParseInt(referral.ReferrerID, 10, 64)
if err != nil {
return errors.New("invalid referrer phone number format")
}
wallets, err := s.walletSvc.GetWalletsByUser(ctx, referrerID)
if err != nil {
return err
}
if len(wallets) == 0 {
return errors.New("referrer has no wallet")
}
walletID := wallets[0].ID
currentBonus := float64(wallets[0].Balance)
return s.walletSvc.Add(ctx, walletID, domain.Currency(int64((currentBonus+referral.RewardAmount)*100)))
}
func (s *Service) ProcessDepositBonus(ctx context.Context, userPhone string, amount float64) error {
settings, err := s.repo.GetSettings(ctx)
if err != nil {
return err
}
userID, err := strconv.ParseInt(userPhone, 10, 64)
if err != nil {
return errors.New("invalid phone number format")
}
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
if err != nil {
return err
}
if len(wallets) == 0 {
return errors.New("user has no wallet")
}
walletID := wallets[0].ID
bonus := amount * (settings.CashbackPercentage / 100)
currentBonus := float64(wallets[0].Balance)
return s.walletSvc.Add(ctx, walletID, domain.Currency(int64((currentBonus+bonus)*100)))
}
func (s *Service) ProcessBetReferral(ctx context.Context, userPhone string, betAmount float64) error {
settings, err := s.repo.GetSettings(ctx)
if err != nil {
return err
}
referral, err := s.repo.GetReferralByReferredID(ctx, userPhone)
if err != nil {
return err
}
if referral == nil || referral.Status != domain.ReferralCompleted {
return ErrNoReferralFound
}
referrerID, err := strconv.ParseInt(referral.ReferrerID, 10, 64)
if err != nil {
return errors.New("invalid referrer phone number format")
}
wallets, err := s.walletSvc.GetWalletsByUser(ctx, referrerID)
if err != nil {
return err
}
if len(wallets) == 0 {
return errors.New("referrer has no wallet")
}
bonusPercentage := settings.BetReferralBonusPercentage
if bonusPercentage == 0 {
bonusPercentage = 5.0
}
bonus := betAmount * (bonusPercentage / 100)
walletID := wallets[0].ID
currentBalance := float64(wallets[0].Balance)
return s.walletSvc.Add(ctx, walletID, domain.Currency(int64((currentBalance+bonus)*100)))
}
func (s *Service) GetReferralStats(ctx context.Context, userPhone string) (*domain.ReferralStats, error) {
return s.repo.GetReferralStats(ctx, userPhone)
}
func (s *Service) UpdateReferralSettings(ctx context.Context, settings *domain.ReferralSettings) error {
settings.UpdatedAt = time.Now()
return s.repo.UpdateSettings(ctx, settings)
}
func (s *Service) GetReferralSettings(ctx context.Context) (*domain.ReferralSettings, error) {
return s.repo.GetSettings(ctx)
}

View File

@ -6,6 +6,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
@ -22,13 +23,14 @@ type App struct {
fiber *fiber.App fiber *fiber.App
logger *slog.Logger logger *slog.Logger
NotidicationStore notificationservice.NotificationStore NotidicationStore notificationservice.NotificationStore
referralSvc referralservice.ReferralStore
port int port int
authSvc *authentication.Service authSvc *authentication.Service
userSvc *user.Service userSvc *user.Service
ticketSvc *ticket.Service
betSvc *bet.Service betSvc *bet.Service
walletSvc *wallet.Service walletSvc *wallet.Service
transactionSvc *transaction.Service transactionSvc *transaction.Service
ticketSvc *ticket.Service
validator *customvalidator.CustomValidator validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig JwtConfig jwtutil.JwtConfig
Logger *slog.Logger Logger *slog.Logger
@ -45,6 +47,7 @@ func NewApp(
walletSvc *wallet.Service, walletSvc *wallet.Service,
transactionSvc *transaction.Service, transactionSvc *transaction.Service,
notidicationStore notificationservice.NotificationStore, notidicationStore notificationservice.NotificationStore,
referralSvc referralservice.ReferralStore,
) *App { ) *App {
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
CaseSensitive: true, CaseSensitive: true,
@ -65,6 +68,7 @@ func NewApp(
walletSvc: walletSvc, walletSvc: walletSvc,
transactionSvc: transactionSvc, transactionSvc: transactionSvc,
NotidicationStore: notidicationStore, NotidicationStore: notidicationStore,
referralSvc: referralSvc,
Logger: logger, Logger: logger,
} }

View File

@ -2,26 +2,13 @@ package handlers
import ( import (
"errors" "errors"
"log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type loginCustomerReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
}
type loginCustomerRes struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// LoginCustomer godoc // LoginCustomer godoc
// @Summary Login customer // @Summary Login customer
// @Description Login customer // @Description Login customer
@ -34,50 +21,51 @@ type loginCustomerRes struct {
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /auth/login [post] // @Router /auth/login [post]
func LoginCustomer( func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
logger *slog.Logger, authSvc *authentication.Service, type loginCustomerReq struct {
validator *customvalidator.CustomValidator, JwtConfig jwtutil.JwtConfig) fiber.Handler { Email string `json:"email" validate:"email" example:"john.doe@example.com"`
return func(c *fiber.Ctx) error { PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Password string `json:"password" validate:"required" example:"password123"`
}
type loginCustomerRes struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
var req loginCustomerReq var req loginCustomerReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
logger.Error("Login failed", "error", err) h.logger.Error("Failed to parse LoginCustomer request", "error", err)
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
valErrs, ok := validator.Validate(c, req)
if !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) if valErrs, ok := h.validator.Validate(c, req); !ok {
return nil return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
} }
successRes, err := authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)
successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)
if err != nil { if err != nil {
logger.Info("Login failed", "error", err) h.logger.Info("Login attempt failed", "email", req.Email, "phone", req.PhoneNumber, "error", err)
if errors.Is(err, authentication.ErrInvalidPassword) { switch {
response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid password or not registered", nil, nil) case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
return nil return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials")
default:
h.logger.Error("Login failed", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
} }
if errors.Is(err, authentication.ErrUserNotFound) {
response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid password or not registered", nil, nil)
return nil
} }
logger.Error("Login failed", "error", err)
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
return nil
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
if err != nil {
h.logger.Error("Failed to create access token", "userID", successRes.UserId, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
} }
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry)
res := loginCustomerRes{ res := loginCustomerRes{
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: successRes.RfToken, RefreshToken: successRes.RfToken,
} }
return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil)
} }
}
type refreshToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// RefreshToken godoc // RefreshToken godoc
// @Summary Refresh token // @Summary Refresh token
@ -91,50 +79,52 @@ type refreshToken struct {
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /auth/refresh [post] // @Router /auth/refresh [post]
func RefreshToken(logger *slog.Logger, authSvc *authentication.Service, func (h *Handler) RefreshToken(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator, JwtConfig jwtutil.JwtConfig) fiber.Handler { type refreshTokenReq struct {
return func(c *fiber.Ctx) error { AccessToken string `json:"access_token" validate:"required" example:"<jwt-token>"`
var req refreshToken RefreshToken string `json:"refresh_token" validate:"required" example:"<refresh-token>"`
}
type loginCustomerRes struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
var req refreshTokenReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) h.logger.Error("Failed to parse RefreshToken request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
valErrs, ok := validator.Validate(c, req)
if !ok { if valErrs, ok := h.validator.Validate(c, req); !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
rf, err := authSvc.RefreshToken(c.Context(), req.RefreshToken)
rf, err := h.authSvc.RefreshToken(c.Context(), req.RefreshToken)
if err != nil { if err != nil {
logger.Info("Refresh token failed", "error", err) h.logger.Info("Refresh token attempt failed", "refreshToken", req.RefreshToken, "error", err)
if errors.Is(err, authentication.ErrExpiredToken) { switch {
response.WriteJSON(c, fiber.StatusUnauthorized, "The refresh token has expired", nil, nil) case errors.Is(err, authentication.ErrExpiredToken):
return nil return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired")
case errors.Is(err, authentication.ErrRefreshTokenNotFound):
return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found")
default:
h.logger.Error("Refresh token failed", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
} }
if errors.Is(err, authentication.ErrRefreshTokenNotFound) {
response.WriteJSON(c, fiber.StatusUnauthorized, "Refresh token not found", nil, nil)
return nil
} }
logger.Error("Refresh token failed", "error", err)
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil) // Assuming the refreshed token includes userID and role info; adjust if needed
return nil accessToken, err := jwtutil.CreateJwt(0, "", h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
}
accessToken, err := jwtutil.CreateJwt(0, "", JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry)
if err != nil { if err != nil {
logger.Error("Create jwt failed", "error", err) h.logger.Error("Failed to create new access token", "error", err)
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
return nil
} }
res := loginCustomerRes{ res := loginCustomerRes{
AccessToken: accessToken, AccessToken: accessToken,
RefreshToken: rf, RefreshToken: rf,
} }
return response.WriteJSON(c, fiber.StatusOK, "refresh successful", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Refresh successful", res, nil)
}
}
type logoutReq struct {
RefreshToken string `json:"refresh_token"`
} }
// LogOutCustomer godoc // LogOutCustomer godoc
@ -149,34 +139,34 @@ type logoutReq struct {
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /auth/logout [post] // @Router /auth/logout [post]
func LogOutCustomer( func (h *Handler) LogOutCustomer(c *fiber.Ctx) error {
logger *slog.Logger, authSvc *authentication.Service, type logoutReq struct {
validator *customvalidator.CustomValidator) fiber.Handler { RefreshToken string `json:"refresh_token" validate:"required" example:"<refresh-token>"`
return func(c *fiber.Ctx) error { }
var req logoutReq var req logoutReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) h.logger.Error("Failed to parse LogOutCustomer request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
valErrs, ok := validator.Validate(c, req)
if !ok { if valErrs, ok := h.validator.Validate(c, req); !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
err := authSvc.Logout(c.Context(), req.RefreshToken)
err := h.authSvc.Logout(c.Context(), req.RefreshToken)
if err != nil { if err != nil {
logger.Info("Logout failed", "error", err) h.logger.Info("Logout attempt failed", "refreshToken", req.RefreshToken, "error", err)
if errors.Is(err, authentication.ErrExpiredToken) { switch {
response.WriteJSON(c, fiber.StatusUnauthorized, "The refresh token has expired", nil, nil) case errors.Is(err, authentication.ErrExpiredToken):
return nil return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired")
case errors.Is(err, authentication.ErrRefreshTokenNotFound):
return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found")
default:
h.logger.Error("Logout failed", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
} }
if errors.Is(err, authentication.ErrRefreshTokenNotFound) {
response.WriteJSON(c, fiber.StatusUnauthorized, "Refresh token not found", nil, nil)
return nil
}
logger.Error("Logout failed", "error", err)
response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil)
return nil
} }
return response.WriteJSON(c, fiber.StatusOK, "Logout successful", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Logout successful", nil, nil)
} }
}

View File

@ -1,26 +1,13 @@
package handlers package handlers
import ( import (
"log/slog"
"strconv" "strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type CreateBetReq struct {
Outcomes []int64 `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status domain.BetStatus `json:"status" example:"1"`
FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
}
type BetRes struct { type BetRes struct {
ID int64 `json:"id" example:"1"` ID int64 `json:"id" example:"1"`
Outcomes []domain.Outcome `json:"outcomes"` Outcomes []domain.Outcome `json:"outcomes"`
@ -34,20 +21,6 @@ type BetRes struct {
IsShopBet bool `json:"is_shop_bet" example:"false"` IsShopBet bool `json:"is_shop_bet" example:"false"`
} }
func convertBet(bet domain.Bet) BetRes {
return BetRes{
ID: bet.ID,
Outcomes: bet.Outcomes,
Amount: bet.Amount.Float64(),
TotalOdds: bet.TotalOdds,
Status: bet.Status,
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: bet.BranchID.Value,
UserID: bet.UserID.Value,
}
}
// CreateBet godoc // CreateBet godoc
// @Summary Create a bet // @Summary Create a bet
// @Description Creates a bet // @Description Creates a bet
@ -59,42 +32,41 @@ func convertBet(bet domain.Bet) BetRes {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [post] // @Router /bet [post]
func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) CreateBet(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type CreateBetReq struct {
Outcomes []int64 `json:"outcomes" validate:"required" example:"[1, 2, 3]"`
// TODO: Check the token, and find the role and get the branch id from there Amount float32 `json:"amount" validate:"required" example:"100.0"`
TotalOdds float32 `json:"total_odds" validate:"required" example:"4.22"`
// TODO Reduce amount from the branch wallet Status domain.BetStatus `json:"status" validate:"required" example:"1"`
FullName string `json:"full_name" example:"John"`
var isShopBet bool = true PhoneNumber string `json:"phone_number" validate:"required" example:"1234567890"`
var branchID int64 = 1 IsShopBet bool `json:"is_shop_bet" example:"false"`
var userID int64 }
var req CreateBetReq var req CreateBetReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
logger.Error("CreateBetReq failed", "error", err) h.logger.Error("Failed to parse CreateBet request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
"error": "Invalid request",
})
} }
valErrs, ok := validator.Validate(c, req) if valErrs, ok := h.validator.Validate(c, req); !ok {
if !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
// TODO Validate Outcomes Here and make sure they didn't expire // TODO: Check the token, find the role, and get the branch ID from there
isShopBet := true
branchID := int64(1)
var userID int64
bet, err := betSvc.CreateBet(c.Context(), domain.CreateBet{ // TODO: Validate Outcomes Here and make sure they didn't expire
bet, err := h.betSvc.CreateBet(c.Context(), domain.CreateBet{
Outcomes: req.Outcomes, Outcomes: req.Outcomes,
Amount: domain.Currency(req.Amount), Amount: domain.Currency(req.Amount),
TotalOdds: req.TotalOdds, TotalOdds: req.TotalOdds,
Status: req.Status, Status: req.Status,
FullName: req.FullName, FullName: req.FullName,
PhoneNumber: req.PhoneNumber, PhoneNumber: req.PhoneNumber,
BranchID: domain.ValidInt64{ BranchID: domain.ValidInt64{
Value: branchID, Value: branchID,
Valid: isShopBet, Valid: isShopBet,
@ -105,16 +77,22 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida
}, },
IsShopBet: req.IsShopBet, IsShopBet: req.IsShopBet,
}) })
if err != nil { if err != nil {
logger.Error("CreateBetReq failed", "error", err) h.logger.Error("Failed to create bet", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create bet")
}
// TODO: Reduce amount from the branch wallet (assuming walletSvc integration)
// This would typically be done here or in the bet service
if !req.IsShopBet && req.PhoneNumber != "" {
if err := h.referralSvc.ProcessBetReferral(c.Context(), req.PhoneNumber, float64(req.Amount)); err != nil {
h.logger.Warn("Failed to process bet referral", "phone", req.PhoneNumber, "amount", req.Amount, "error", err)
}
} }
res := convertBet(bet) res := convertBet(bet)
return response.WriteJSON(c, fiber.StatusOK, "Bet created successfully", res, nil)
return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
}
} }
// GetAllBet godoc // GetAllBet godoc
@ -127,22 +105,19 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [get] // @Router /bet [get]
func GetAllBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetAllBet(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { bets, err := h.betSvc.GetAllBets(c.Context())
bets, err := betSvc.GetAllBets(c.Context())
if err != nil { if err != nil {
logger.Error("Failed to get bets", "error", err) h.logger.Error("Failed to get bets", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bets", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets")
} }
var res []BetRes = make([]BetRes, len(bets)) res := make([]BetRes, len(bets))
for _, bet := range bets { for i, bet := range bets {
res = append(res, convertBet(bet)) res[i] = convertBet(bet)
} }
return response.WriteJSON(c, fiber.StatusOK, "All Bets Retrieved", res, nil) return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil)
}
} }
// GetBetByID godoc // GetBetByID godoc
@ -156,32 +131,35 @@ func GetAllBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [get] // @Router /bet/{id} [get]
func GetBetByID(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetBetByID(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type BetRes struct {
betID := c.Params("id") ID int64 `json:"id" example:"1"`
id, err := strconv.ParseInt(betID, 10, 64) Outcomes []domain.Outcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
if err != nil { TotalOdds float32 `json:"total_odds" example:"4.22"`
logger.Error("Invalid bet ID", "betID", betID, "error", err) Status domain.BetStatus `json:"status" example:"1"`
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
BranchID int64 `json:"branch_id" example:"2"`
UserID int64 `json:"user_id" example:"2"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
} }
bet, err := betSvc.GetBetByID(c.Context(), id) betID := c.Params("id")
id, err := strconv.ParseInt(betID, 10, 64)
if err != nil { if err != nil {
logger.Error("Failed to get bet by ID", "betID", id, "error", err) h.logger.Error("Invalid bet ID", "betID", betID, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID")
}
bet, err := h.betSvc.GetBetByID(c.Context(), id)
if err != nil {
h.logger.Error("Failed to get bet by ID", "betID", id, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bet")
} }
res := convertBet(bet) res := convertBet(bet)
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)
}
}
type UpdateCashOutReq struct {
CashedOut bool
} }
// UpdateCashOut godoc // UpdateCashOut godoc
@ -196,41 +174,36 @@ type UpdateCashOutReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [patch] // @Router /bet/{id} [patch]
func UpdateCashOut(logger *slog.Logger, betSvc *bet.Service, func (h *Handler) UpdateCashOut(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type UpdateCashOutReq struct {
return func(c *fiber.Ctx) error { CashedOut bool `json:"cashed_out" validate:"required" example:"true"`
}
betID := c.Params("id") betID := c.Params("id")
id, err := strconv.ParseInt(betID, 10, 64) id, err := strconv.ParseInt(betID, 10, 64)
if err != nil { if err != nil {
logger.Error("Invalid bet ID", "betID", betID, "error", err) h.logger.Error("Invalid bet ID", "betID", betID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID")
} }
var req UpdateCashOutReq var req UpdateCashOutReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
logger.Error("UpdateCashOutReq failed", "error", err) h.logger.Error("Failed to parse UpdateCashOut request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
"error": "Invalid request",
})
} }
valErrs, ok := validator.Validate(c, req) if valErrs, ok := h.validator.Validate(c, req); !ok {
if !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
err = betSvc.UpdateCashOut(c.Context(), id, req.CashedOut) err = h.betSvc.UpdateCashOut(c.Context(), id, req.CashedOut)
if err != nil { if err != nil {
logger.Error("Failed to update cash out bet", "betID", id, "error", err) h.logger.Error("Failed to update cash out bet", "betID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update cash out bet", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update cash out bet")
} }
return response.WriteJSON(c, fiber.StatusOK, "Bet updated successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Bet updated successfully", nil, nil)
} }
}
// DeleteBet godoc // DeleteBet godoc
// @Summary Deletes bet by id // @Summary Deletes bet by id
@ -243,24 +216,34 @@ func UpdateCashOut(logger *slog.Logger, betSvc *bet.Service,
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [delete] // @Router /bet/{id} [delete]
func DeleteBet(logger *slog.Logger, betSvc *bet.Service, func (h *Handler) DeleteBet(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
betID := c.Params("id") betID := c.Params("id")
id, err := strconv.ParseInt(betID, 10, 64) id, err := strconv.ParseInt(betID, 10, 64)
if err != nil { if err != nil {
logger.Error("Invalid bet ID", "betID", betID, "error", err) h.logger.Error("Invalid bet ID", "betID", betID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID")
} }
err = betSvc.DeleteBet(c.Context(), id) err = h.betSvc.DeleteBet(c.Context(), id)
if err != nil { if err != nil {
logger.Error("Failed to delete by ID", "betID", id, "error", err) h.logger.Error("Failed to delete bet by ID", "betID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to delete bet", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete bet")
} }
return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil)
} }
func convertBet(bet domain.Bet) BetRes {
return BetRes{
ID: bet.ID,
Outcomes: bet.Outcomes,
Amount: bet.Amount.Float64(),
TotalOdds: bet.TotalOdds,
Status: bet.Status,
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: bet.BranchID.Value,
UserID: bet.UserID.Value,
IsShopBet: bet.IsShopBet,
}
} }

View File

@ -3,20 +3,45 @@ package handlers
import ( import (
"log/slog" "log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
) )
type Handler struct { type Handler struct {
logger *slog.Logger logger *slog.Logger
notificationSvc notificationservice.NotificationStore notificationSvc notificationservice.NotificationStore
userSvc *user.Service
referralSvc referralservice.ReferralStore
walletSvc *wallet.Service
transactionSvc *transaction.Service
ticketSvc *ticket.Service
betSvc *bet.Service
authSvc *authentication.Service
jwtConfig jwtutil.JwtConfig
validator *customvalidator.CustomValidator validator *customvalidator.CustomValidator
} }
func New(logger *slog.Logger, notificationSvc notificationservice.NotificationStore, validator *customvalidator.CustomValidator) *Handler { func New(logger *slog.Logger, notificationSvc notificationservice.NotificationStore, validator *customvalidator.CustomValidator, walletSvc *wallet.Service,
referralSvc referralservice.ReferralStore, userSvc *user.Service, transactionSvc *transaction.Service, ticketSvc *ticket.Service, betSvc *bet.Service, authSvc *authentication.Service, jwtConfig jwtutil.JwtConfig) *Handler {
return &Handler{ return &Handler{
logger: logger, logger: logger,
notificationSvc: notificationSvc, notificationSvc: notificationSvc,
walletSvc: walletSvc,
referralSvc: referralSvc,
validator: validator, validator: validator,
userSvc: userSvc,
transactionSvc: transactionSvc,
ticketSvc: ticketSvc,
betSvc: betSvc,
authSvc: authSvc,
jwtConfig: jwtConfig,
} }
} }

View File

@ -0,0 +1,145 @@
package handlers
import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
func (h *Handler) CreateReferral(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.logger.Error("Failed to get user", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user")
}
referral, err := h.referralSvc.CreateReferral(c.Context(), user.PhoneNumber)
if err != nil {
h.logger.Error("Failed to create referral", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create referral")
}
return response.WriteJSON(c, fiber.StatusOK, "Referral created successfully", referral, nil)
}
// GetReferralStats godoc
// @Summary Get referral statistics
// @Description Retrieves referral statistics for the authenticated user
// @Tags referral
// @Accept json
// @Produce json
// @Success 200 {object} domain.ReferralStats
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /referral/stats [get]
func (h *Handler) GetReferralStats(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.logger.Error("Failed to get user", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user")
}
stats, err := h.referralSvc.GetReferralStats(c.Context(), user.PhoneNumber)
if err != nil {
h.logger.Error("Failed to get referral stats", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve referral stats")
}
return response.WriteJSON(c, fiber.StatusOK, "Referral stats retrieved successfully", stats, nil)
}
// UpdateReferralSettings godoc
// @Summary Update referral settings
// @Description Updates referral settings (admin only)
// @Tags referral
// @Accept json
// @Produce json
// @Param settings body domain.ReferralSettings true "Referral settings"
// @Success 200 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 403 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /referral/settings [put]
func (h *Handler) UpdateReferralSettings(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.logger.Error("Failed to get user", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user")
}
if user.Role != domain.RoleAdmin {
return fiber.NewError(fiber.StatusForbidden, "Admin access required")
}
var settings domain.ReferralSettings
if err := c.BodyParser(&settings); err != nil {
h.logger.Error("Failed to parse settings", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
settings.UpdatedBy = user.PhoneNumber
if err := h.referralSvc.UpdateReferralSettings(c.Context(), &settings); err != nil {
h.logger.Error("Failed to update referral settings", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update referral settings")
}
return response.WriteJSON(c, fiber.StatusOK, "Referral settings updated successfully", nil, nil)
}
// GetReferralSettings godoc
// @Summary Get referral settings
// @Description Retrieves current referral settings (admin only)
// @Tags referral
// @Accept json
// @Produce json
// @Success 200 {object} domain.ReferralSettings
// @Failure 401 {object} response.APIResponse
// @Failure 403 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /referral/settings [get]
func (h *Handler) GetReferralSettings(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.logger.Error("Failed to get user", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user")
}
if user.Role != domain.RoleAdmin {
return fiber.NewError(fiber.StatusForbidden, "Admin access required")
}
settings, err := h.referralSvc.GetReferralSettings(c.Context())
if err != nil {
h.logger.Error("Failed to get referral settings", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve referral settings")
}
return response.WriteJSON(c, fiber.StatusOK, "Referral settings retrieved successfully", settings, nil)
}

View File

@ -1,25 +1,13 @@
package handlers package handlers
import ( import (
"log/slog"
"strconv" "strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type CreateTicketReq struct {
Outcomes []int64 `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
}
type CreateTicketRes struct {
FastCode int64 `json:"fast_code" example:"1234"`
}
// CreateTicket godoc // CreateTicket godoc
// @Summary Create a temporary ticket // @Summary Create a temporary ticket
// @Description Creates a temporary ticket // @Description Creates a temporary ticket
@ -31,48 +19,43 @@ type CreateTicketRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /ticket [post] // @Router /ticket [post]
func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, func (h *Handler) CreateTicket(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type CreateTicketReq struct {
return func(c *fiber.Ctx) error { Outcomes []int64 `json:"outcomes" validate:"required" example:"[1, 2, 3]"`
Amount float32 `json:"amount" validate:"required" example:"100.0"`
TotalOdds float32 `json:"total_odds" validate:"required" example:"4.22"`
}
type CreateTicketRes struct {
FastCode int64 `json:"fast_code" example:"1234"`
}
var req CreateTicketReq var req CreateTicketReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
logger.Error("CreateTicketReq failed", "error", err) h.logger.Error("Failed to parse CreateTicket request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
"error": "Invalid request",
})
} }
valErrs, ok := validator.Validate(c, req) if valErrs, ok := h.validator.Validate(c, req); !ok {
if !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
// TODO Validate Outcomes Here and make sure they didn't expire // TODO: Validate Outcomes Here and make sure they didn't expire
ticket, err := ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ ticket, err := h.ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{
Outcomes: req.Outcomes, Outcomes: req.Outcomes,
Amount: domain.Currency(req.Amount), Amount: domain.Currency(req.Amount),
TotalOdds: req.TotalOdds, TotalOdds: req.TotalOdds,
}) })
if err != nil { if err != nil {
logger.Error("CreateTicketReq failed", "error", err) h.logger.Error("Failed to create ticket", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return fiber.NewError(fiber.StatusInternalServerError, "Failed to create ticket")
"error": "Internal server error",
})
} }
res := CreateTicketRes{ res := CreateTicketRes{
FastCode: ticket.ID, FastCode: ticket.ID,
} }
return response.WriteJSON(c, fiber.StatusOK, "Ticket Created", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Ticket created successfully", res, nil)
}
}
type TicketRes struct {
ID int64 `json:"id" example:"1"`
Outcomes []domain.Outcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
} }
// GetTicketByID godoc // GetTicketByID godoc
@ -86,22 +69,25 @@ type TicketRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /ticket/{id} [get] // @Router /ticket/{id} [get]
func GetTicketByID(logger *slog.Logger, ticketSvc *ticket.Service, func (h *Handler) GetTicketByID(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type TicketRes struct {
return func(c *fiber.Ctx) error { ID int64 `json:"id" example:"1"`
ticketID := c.Params("id") Outcomes []domain.Outcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
id, err := strconv.ParseInt(ticketID, 10, 64) TotalOdds float32 `json:"total_odds" example:"4.22"`
if err != nil {
logger.Error("Invalid ticket ID", "ticketID", ticketID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid ticket ID", err, nil)
} }
ticket, err := ticketSvc.GetTicketByID(c.Context(), id) ticketID := c.Params("id")
id, err := strconv.ParseInt(ticketID, 10, 64)
if err != nil { if err != nil {
logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err) h.logger.Error("Invalid ticket ID", "ticketID", ticketID, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve ticket", err, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid ticket ID")
}
ticket, err := h.ticketSvc.GetTicketByID(c.Context(), id)
if err != nil {
h.logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve ticket")
} }
res := TicketRes{ res := TicketRes{
@ -110,10 +96,7 @@ func GetTicketByID(logger *slog.Logger, ticketSvc *ticket.Service,
Amount: ticket.Amount.Float64(), Amount: ticket.Amount.Float64(),
TotalOdds: ticket.TotalOdds, TotalOdds: ticket.TotalOdds,
} }
return response.WriteJSON(c, fiber.StatusOK, "Ticket retrieved successfully", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Ticket retrieved successfully", res, nil)
}
} }
// GetAllTickets godoc // GetAllTickets godoc
@ -126,28 +109,29 @@ func GetTicketByID(logger *slog.Logger, ticketSvc *ticket.Service,
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /ticket [get] // @Router /ticket [get]
func GetAllTickets(logger *slog.Logger, ticketSvc *ticket.Service, func (h *Handler) GetAllTickets(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type TicketRes struct {
return func(c *fiber.Ctx) error { ID int64 `json:"id" example:"1"`
tickets, err := ticketSvc.GetAllTickets(c.Context()) Outcomes []domain.Outcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
if err != nil { TotalOdds float32 `json:"total_odds" example:"4.22"`
logger.Error("Failed to get tickets", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve tickets", err, nil)
} }
var res []TicketRes = make([]TicketRes, len(tickets)) tickets, err := h.ticketSvc.GetAllTickets(c.Context())
if err != nil {
h.logger.Error("Failed to get tickets", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve tickets")
}
for _, ticket := range tickets { res := make([]TicketRes, len(tickets))
res = append(res, TicketRes{ for i, ticket := range tickets {
res[i] = TicketRes{
ID: ticket.ID, ID: ticket.ID,
Outcomes: ticket.Outcomes, Outcomes: ticket.Outcomes,
Amount: ticket.Amount.Float64(), Amount: ticket.Amount.Float64(),
TotalOdds: ticket.TotalOdds, TotalOdds: ticket.TotalOdds,
}) }
} }
return response.WriteJSON(c, fiber.StatusOK, "All Tickets retrieved", res, nil) return response.WriteJSON(c, fiber.StatusOK, "All tickets retrieved successfully", res, nil)
}
} }

View File

@ -1,13 +1,10 @@
package handlers package handlers
import ( import (
"log/slog"
"strconv" "strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -75,28 +72,41 @@ func convertTransaction(transaction domain.Transaction) TransactionRes {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /transaction [post] // @Router /transaction [post]
func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { // Update transaction handler to include deposit bonus
return func(c *fiber.Ctx) error { func (h *Handler) CreateTransaction(c *fiber.Ctx) error {
type CreateTransactionReq struct {
Amount float32 `json:"amount" validate:"required" example:"100.0"`
BranchID int64 `json:"branch_id" example:"1"`
CashierID int64 `json:"cashier_id" example:"1"`
BetID int64 `json:"bet_id" example:"1"`
PaymentOption domain.PaymentOption `json:"payment_option" validate:"required" example:"1"`
FullName string `json:"full_name" example:"John Smith"`
PhoneNumber string `json:"phone_number" validate:"required" example:"0911111111"`
BankCode string `json:"bank_code"`
BeneficiaryName string `json:"beneficiary_name"`
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
ReferenceNumber string `json:"reference_number"`
}
var req CreateTransactionReq var req CreateTransactionReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
logger.Error("CreateTransactionReq failed", "error", err) h.logger.Error("CreateTransaction failed to parse request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
"error": "Invalid request",
})
} }
valErrs, ok := validator.Validate(c, req) if valErrs, ok := h.validator.Validate(c, req); !ok {
if !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
transaction, err := transactionSvc.CreateTransaction(c.Context(), domain.CreateTransaction{ isDeposit := req.PaymentOption == domain.BANK
transaction, err := h.transactionSvc.CreateTransaction(c.Context(), domain.CreateTransaction{
Amount: domain.Currency(req.Amount), Amount: domain.Currency(req.Amount),
BranchID: req.BranchID, BranchID: req.BranchID,
CashierID: req.CashierID, CashierID: req.CashierID,
BetID: req.BetID, BetID: req.BetID,
PaymentOption: domain.PaymentOption(req.PaymentOption), PaymentOption: req.PaymentOption,
FullName: req.FullName, FullName: req.FullName,
PhoneNumber: req.PhoneNumber, PhoneNumber: req.PhoneNumber,
BankCode: req.BankCode, BankCode: req.BankCode,
@ -105,16 +115,19 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service,
AccountNumber: req.AccountNumber, AccountNumber: req.AccountNumber,
ReferenceNumber: req.ReferenceNumber, ReferenceNumber: req.ReferenceNumber,
}) })
if err != nil { if err != nil {
logger.Error("CreateTransactionReq failed", "error", err) h.logger.Error("CreateTransaction failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transaction")
}
if isDeposit {
if err := h.referralSvc.ProcessDepositBonus(c.Context(), req.PhoneNumber, float64(req.Amount)); err != nil {
h.logger.Warn("Failed to process deposit bonus", "phone", req.PhoneNumber, "amount", req.Amount, "error", err)
}
} }
res := convertTransaction(transaction) res := convertTransaction(transaction)
return response.WriteJSON(c, fiber.StatusOK, "Transaction created successfully", res, nil)
return response.WriteJSON(c, fiber.StatusOK, "Transaction Created", res, nil)
}
} }
// GetAllTransactions godoc // GetAllTransactions godoc
@ -127,22 +140,19 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service,
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /transaction [get] // @Router /transaction [get]
func GetAllTransactions(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetAllTransactions(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { transactions, err := h.transactionSvc.GetAllTransactions(c.Context())
transactions, err := transactionSvc.GetAllTransactions(c.Context())
if err != nil { if err != nil {
logger.Error("Failed to get transaction", "error", err) h.logger.Error("Failed to get transactions", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve transaction", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve transactions")
} }
var res []TransactionRes = make([]TransactionRes, len(transactions)) res := make([]TransactionRes, len(transactions))
for _, transaction := range transactions { for i, transaction := range transactions {
res = append(res, convertTransaction(transaction)) res[i] = convertTransaction(transaction)
} }
return response.WriteJSON(c, fiber.StatusOK, "All Transactions Retrieved", res, nil) return response.WriteJSON(c, fiber.StatusOK, "All transactions retrieved", res, nil)
}
} }
// GetTransactionByID godoc // GetTransactionByID godoc
@ -156,77 +166,63 @@ func GetAllTransactions(logger *slog.Logger, transactionSvc *transaction.Service
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /transaction/{id} [get] // @Router /transaction/{id} [get]
func GetTransactionByID(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetTransactionByID(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
transactionID := c.Params("id") transactionID := c.Params("id")
id, err := strconv.ParseInt(transactionID, 10, 64) id, err := strconv.ParseInt(transactionID, 10, 64)
if err != nil { if err != nil {
logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err) h.logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid transaction ID")
} }
transaction, err := transactionSvc.GetTransactionByID(c.Context(), id) transaction, err := h.transactionSvc.GetTransactionByID(c.Context(), id)
if err != nil { if err != nil {
logger.Error("Failed to get transaction by ID", "transactionID", id, "error", err) h.logger.Error("Failed to get transaction by ID", "transactionID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve transaction", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve transaction")
} }
res := convertTransaction(transaction) res := convertTransaction(transaction)
return response.WriteJSON(c, fiber.StatusOK, "Transaction retrieved successfully", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Transaction retrieved successfully", res, nil)
} }
}
type UpdateTransactionVerifiedReq struct {
Verified bool
}
// UpdateTransactionVerified godoc // UpdateTransactionVerified godoc
// @Summary Updates the cashed out field // @Summary Updates the verified field of a transaction
// @Description Updates the cashed out field // @Description Updates the verified status of a transaction
// @Tags transaction // @Tags transaction
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path int true "Transaction ID" // @Param id path int true "Transaction ID"
// @Param updateCashOut body UpdateTransactionVerifiedReq true "Updates Transaction Verification" // @Param updateVerified body UpdateTransactionVerifiedReq true "Updates Transaction Verification"
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /transaction/{id} [patch] // @Router /transaction/{id} [patch]
func UpdateTransactionVerified(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) UpdateTransactionVerified(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type UpdateTransactionVerifiedReq struct {
Verified bool `json:"verified" validate:"required" example:"true"`
}
transactionID := c.Params("id") transactionID := c.Params("id")
id, err := strconv.ParseInt(transactionID, 10, 64) id, err := strconv.ParseInt(transactionID, 10, 64)
if err != nil { if err != nil {
logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err) h.logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid transaction ID")
} }
var req UpdateTransactionVerifiedReq var req UpdateTransactionVerifiedReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
logger.Error("UpdateTransactionVerifiedReq failed", "error", err) h.logger.Error("Failed to parse UpdateTransactionVerified request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
"error": "Invalid request",
})
} }
valErrs, ok := validator.Validate(c, req) if valErrs, ok := h.validator.Validate(c, req); !ok {
if !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
err = transactionSvc.UpdateTransactionVerified(c.Context(), id, req.Verified) err = h.transactionSvc.UpdateTransactionVerified(c.Context(), id, req.Verified)
if err != nil { if err != nil {
logger.Error("Failed to update transaction verification", "transactionID", id, "error", err) h.logger.Error("Failed to update transaction verification", "transactionID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update transaction verification", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transaction verification")
} }
return response.WriteJSON(c, fiber.StatusOK, "Transaction updated successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Transaction updated successfully", nil, nil)
}
} }

View File

@ -2,27 +2,14 @@ package handlers
import ( import (
"errors" "errors"
"log/slog"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type CheckPhoneEmailExistReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
type CheckPhoneEmailExistRes struct {
EmailExist bool `json:"email_exist"`
PhoneNumberExist bool `json:"phone_number_exist"`
}
// CheckPhoneEmailExist godoc // CheckPhoneEmailExist godoc
// @Summary Check if phone number or email exist // @Summary Check if phone number or email exist
// @Description Check if phone number or email exist // @Description Check if phone number or email exist
@ -34,39 +21,37 @@ type CheckPhoneEmailExistRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/checkPhoneEmailExist [post] // @Router /user/checkPhoneEmailExist [post]
func CheckPhoneEmailExist(logger *slog.Logger, userSvc *user.Service, func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type CheckPhoneEmailExistReq struct {
return func(c *fiber.Ctx) error { Email string `json:"email" validate:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required" example:"1234567890"`
}
type CheckPhoneEmailExistRes struct {
EmailExist bool `json:"email_exist"`
PhoneNumberExist bool `json:"phone_number_exist"`
}
var req CheckPhoneEmailExistReq var req CheckPhoneEmailExistReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
logger.Error("CheckPhoneEmailExist failed", "error", err) h.logger.Error("Failed to parse CheckPhoneEmailExist request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
"error": "Invalid request",
})
} }
valErrs, ok := validator.Validate(c, req)
if !ok { if valErrs, ok := h.validator.Validate(c, req); !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
emailExist, phoneExist, err := userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email)
emailExist, phoneExist, err := h.userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email)
if err != nil { if err != nil {
logger.Error("CheckPhoneEmailExist failed", "error", err) h.logger.Error("Failed to check phone/email existence", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return fiber.NewError(fiber.StatusInternalServerError, "Failed to check phone/email existence")
"error": "Internal server error",
})
} }
res := CheckPhoneEmailExistRes{ res := CheckPhoneEmailExistRes{
EmailExist: emailExist, EmailExist: emailExist,
PhoneNumberExist: phoneExist, PhoneNumberExist: phoneExist,
} }
return response.WriteJSON(c, fiber.StatusOK, "Check Success", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Check successful", res, nil)
}
}
type RegisterCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
} }
// SendRegisterCode godoc // SendRegisterCode godoc
@ -80,52 +65,41 @@ type RegisterCodeReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/sendRegisterCode [post] // @Router /user/sendRegisterCode [post]
func SendRegisterCode(logger *slog.Logger, userSvc *user.Service, func (h *Handler) SendRegisterCode(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type RegisterCodeReq struct {
return func(c *fiber.Ctx) error { Email string `json:"email" validate:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
}
var req RegisterCodeReq var req RegisterCodeReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ h.logger.Error("Failed to parse SendRegisterCode request", "error", err)
"error": "Invalid request", return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
})
} }
valErrs, ok := validator.Validate(c, req)
if !ok { if valErrs, ok := h.validator.Validate(c, req); !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
var sentTo string var sentTo string
var medium domain.OtpMedium var medium domain.OtpMedium
if req.Email != "" { if req.Email != "" {
sentTo = req.Email sentTo = req.Email
medium = domain.OtpMediumEmail medium = domain.OtpMediumEmail
} } else if req.PhoneNumber != "" {
if req.PhoneNumber != "" {
sentTo = req.PhoneNumber sentTo = req.PhoneNumber
medium = domain.OtpMediumSms medium = domain.OtpMediumSms
} else {
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
} }
if err := userSvc.SendRegisterCode(c.Context(), medium, sentTo); err != nil {
logger.Error("SendRegisterCode failed", "error", err) if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ h.logger.Error("Failed to send register code", "error", err)
"error": "Internal server error", return fiber.NewError(fiber.StatusInternalServerError, "Failed to send register code")
})
} }
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
} }
}
type RegisterUserReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
//Role string
Otp string `json:"otp" example:"123456"`
ReferalCode string `json:"referal_code" example:"ABC123"`
//
}
// RegisterUser godoc // RegisterUser godoc
// @Summary Register user // @Summary Register user
@ -138,20 +112,25 @@ type RegisterUserReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/register [post] // @Router /user/register [post]
func RegisterUser(logger *slog.Logger, userSvc *user.Service, walletSvc *wallet.Service, func (h *Handler) RegisterUser(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type RegisterUserReq struct {
return func(c *fiber.Ctx) error { FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
Otp string `json:"otp" example:"123456"`
ReferalCode string `json:"referal_code" example:"ABC123"`
}
var req RegisterUserReq var req RegisterUserReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
logger.Error("RegisterUser failed", "error", err) h.logger.Error("Failed to parse RegisterUser request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
"error": "Invalid request",
})
} }
valErrs, ok := validator.Validate(c, req)
if !ok { if valErrs, ok := h.validator.Validate(c, req); !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
user := domain.RegisterUserReq{ user := domain.RegisterUserReq{
FirstName: req.FirstName, FirstName: req.FirstName,
@ -160,18 +139,20 @@ func RegisterUser(logger *slog.Logger, userSvc *user.Service, walletSvc *wallet.
PhoneNumber: req.PhoneNumber, PhoneNumber: req.PhoneNumber,
Password: req.Password, Password: req.Password,
Otp: req.Otp, Otp: req.Otp,
ReferalCode: req.ReferalCode, ReferralCode: req.ReferalCode,
OtpMedium: domain.OtpMediumEmail, OtpMedium: domain.OtpMediumEmail,
} }
medium, err := getMedium(req.Email, req.PhoneNumber) medium, err := getMedium(req.Email, req.PhoneNumber)
if err != nil { if err != nil {
logger.Error("RegisterUser failed", "error", err) h.logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
user.OtpMedium = medium user.OtpMedium = medium
newUser, err := userSvc.RegisterUser(c.Context(), user)
newUser, err := h.userSvc.RegisterUser(c.Context(), user)
if err != nil { if err != nil {
if errors.Is(err, domain.ErrOtpAlreadyUsed) { if errors.Is(err, domain.ErrOtpAlreadyUsed) {
return response.WriteJSON(c, fiber.StatusBadRequest, "Otp already used", nil, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Otp already used", nil, nil)
@ -185,28 +166,31 @@ func RegisterUser(logger *slog.Logger, userSvc *user.Service, walletSvc *wallet.
if errors.Is(err, domain.ErrOtpNotFound) { if errors.Is(err, domain.ErrOtpNotFound) {
return response.WriteJSON(c, fiber.StatusBadRequest, "User already exist", nil, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "User already exist", nil, nil)
} }
logger.Error("RegisterUser failed", "error", err) h.logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error", "error": "Internal server error",
}) })
} }
// TODO: Integrate company when we move to multi-vendor system _, err = h.walletSvc.CreateWallet(c.Context(), domain.CreateWallet{
_, err = walletSvc.CreateCustomerWallet(c.Context(), newUser.ID, 0) UserID: newUser.ID,
IsWithdraw: true,
IsBettable: true,
})
if err != nil { if err != nil {
logger.Error("CreateCustomerWallet failed", "error", err) h.logger.Error("Failed to create wallet for user", "userID", newUser.ID, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create customer wallet for user", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create user wallet")
}
if req.ReferalCode != "" {
err = h.referralSvc.ProcessReferral(c.Context(), req.PhoneNumber, req.ReferalCode)
if err != nil {
h.logger.Warn("Failed to process referral during registration", "phone", req.PhoneNumber, "code", req.ReferalCode, "error", err)
}
} }
return response.WriteJSON(c, fiber.StatusOK, "Registration successful", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Registration successful", nil, nil)
} }
}
type ResetCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
// SendResetCode godoc // SendResetCode godoc
// @Summary Send reset code // @Summary Send reset code
@ -219,46 +203,40 @@ type ResetCodeReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/sendResetCode [post] // @Router /user/sendResetCode [post]
func SendResetCode(logger *slog.Logger, userSvc *user.Service, func (h *Handler) SendResetCode(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type ResetCodeReq struct {
return func(c *fiber.Ctx) error { Email string `json:"email" validate:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
}
var req ResetCodeReq var req ResetCodeReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ h.logger.Error("Failed to parse SendResetCode request", "error", err)
"error": "Invalid request", return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
})
} }
valErrs, ok := validator.Validate(c, req)
if !ok { if valErrs, ok := h.validator.Validate(c, req); !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
var sentTo string var sentTo string
var medium domain.OtpMedium var medium domain.OtpMedium
if req.Email != "" { if req.Email != "" {
sentTo = req.Email sentTo = req.Email
medium = domain.OtpMediumEmail medium = domain.OtpMediumEmail
} } else if req.PhoneNumber != "" {
if req.PhoneNumber != "" {
sentTo = req.PhoneNumber sentTo = req.PhoneNumber
medium = domain.OtpMediumSms medium = domain.OtpMediumSms
} else {
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
} }
if err := userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil {
logger.Error("SendResetCode failed", "error", err) if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ h.logger.Error("Failed to send reset code", "error", err)
"error": "Internal server error", return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code")
})
} }
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
}
}
type ResetPasswordReq struct {
Email string
PhoneNumber string
Password string
Otp string
} }
// ResetPassword godoc // ResetPassword godoc
@ -272,45 +250,58 @@ type ResetPasswordReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/resetPassword [post] // @Router /user/resetPassword [post]
func ResetPassword(logger *slog.Logger, userSvc *user.Service, func (h *Handler) ResetPassword(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type ResetPasswordReq struct {
return func(c *fiber.Ctx) error { Email string `json:"email" validate:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Password string `json:"password" validate:"required,min=8" example:"newpassword123"`
Otp string `json:"otp" validate:"required" example:"123456"`
}
var req ResetPasswordReq var req ResetPasswordReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
logger.Error("ResetPassword failed", "error", err) h.logger.Error("Failed to parse ResetPassword request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
"error": "Invalid request",
})
} }
valErrs, ok := validator.Validate(c, req)
if !ok { if valErrs, ok := h.validator.Validate(c, req); !ok {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
} }
user := domain.ResetPasswordReq{
medium, err := getMedium(req.Email, req.PhoneNumber)
if err != nil {
h.logger.Error("Failed to determine medium for ResetPassword", "error", err)
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
resetReq := domain.ResetPasswordReq{
Email: req.Email, Email: req.Email,
PhoneNumber: req.PhoneNumber, PhoneNumber: req.PhoneNumber,
Password: req.Password, Password: req.Password,
Otp: req.Otp, Otp: req.Otp,
} OtpMedium: medium,
medium, err := getMedium(req.Email, req.PhoneNumber)
if err != nil {
logger.Error("ResetPassword failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
user.OtpMedium = medium
if err := userSvc.ResetPassword(c.Context(), user); err != nil {
logger.Error("ResetPassword failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
return response.WriteJSON(c, fiber.StatusOK, "Password reset successful", nil, nil)
}
} }
if err := h.userSvc.ResetPassword(c.Context(), resetReq); err != nil {
h.logger.Error("Failed to reset password", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset password")
}
return response.WriteJSON(c, fiber.StatusOK, "Password reset successful", nil, nil)
}
// UserProfile godoc
// @Summary Get user profile
// @Description Get user profile
// @Tags user
// @Accept json
// @Produce json
// @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /user/profile [get]
func (h *Handler) UserProfile(c *fiber.Ctx) error {
type UserProfileRes struct { type UserProfileRes struct {
ID int64 `json:"id"` ID int64 `json:"id"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
@ -326,26 +317,16 @@ type UserProfileRes struct {
Suspended bool `json:"suspended"` Suspended bool `json:"suspended"`
} }
// UserProfile godoc userID, ok := c.Locals("user_id").(int64)
// @Summary Get user profile if !ok || userID == 0 {
// @Description Get user profile h.logger.Error("Invalid user ID in context")
// @Tags user return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
// @Accept json }
// @Produce json
// @Success 200 {object} UserProfileRes user, err := h.userSvc.GetUserByID(c.Context(), userID)
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /user/profile [get]
func UserProfile(logger *slog.Logger, userSvc *user.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
userId := c.Locals("user_id").(int64)
user, err := userSvc.GetUserByID(c.Context(), userId)
if err != nil { if err != nil {
logger.Error("GetUserProfile failed", "error", err) h.logger.Error("Failed to get user profile", "userID", userID, "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile")
"error": "Internal server error",
})
} }
res := UserProfileRes{ res := UserProfileRes{
@ -364,7 +345,8 @@ func UserProfile(logger *slog.Logger, userSvc *user.Service) fiber.Handler {
} }
return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil) return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil)
} }
}
// Helper function (unchanged)
func getMedium(email, phoneNumber string) (domain.OtpMedium, error) { func getMedium(email, phoneNumber string) (domain.OtpMedium, error) {
if email != "" { if email != "" {
return domain.OtpMediumEmail, nil return domain.OtpMediumEmail, nil

View File

@ -1,28 +1,14 @@
package handlers package handlers
import ( import (
"log/slog"
"strconv" "strconv"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type WalletRes struct {
ID int64 `json:"id" example:"1"`
Balance float32 `json:"amount" example:"100.0"`
IsWithdraw bool `json:"is_withdraw" example:"true"`
IsBettable bool `json:"is_bettable" example:"true"`
IsActive bool `json:"is_active" example:"true"`
UserID int64 `json:"user_id" example:"1"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
}
// GetWalletByID godoc // GetWalletByID godoc
// @Summary Get wallet by ID // @Summary Get wallet by ID
// @Description Retrieve wallet details by wallet ID // @Description Retrieve wallet details by wallet ID
@ -34,21 +20,29 @@ type WalletRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /wallet/{id} [get] // @Router /wallet/{id} [get]
func GetWalletByID(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetWalletByID(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type WalletRes struct {
walletID := c.Params("id") ID int64 `json:"id" example:"1"`
Balance float32 `json:"amount" example:"100.0"`
id, err := strconv.ParseInt(walletID, 10, 64) IsWithdraw bool `json:"is_withdraw" example:"true"`
IsBettable bool `json:"is_bettable" example:"true"`
if err != nil { IsActive bool `json:"is_active" example:"true"`
logger.Error("Invalid wallet ID", "walletID", walletID, "error", err) UserID int64 `json:"user_id" example:"1"`
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
} }
wallet, err := walletSvc.GetWalletByID(c.Context(), id) walletID := c.Params("id")
id, err := strconv.ParseInt(walletID, 10, 64)
if err != nil { if err != nil {
logger.Error("Failed to get wallet by ID", "walletID", id, "error", err) h.logger.Error("Invalid wallet ID", "walletID", walletID, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallet", err, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid wallet ID")
}
wallet, err := h.walletSvc.GetWalletByID(c.Context(), id)
if err != nil {
h.logger.Error("Failed to get wallet by ID", "walletID", id, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve wallet")
} }
res := WalletRes{ res := WalletRes{
@ -64,7 +58,6 @@ func GetWalletByID(logger *slog.Logger, walletSvc *wallet.Service, validator *cu
return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil)
} }
}
// GetAllWallets godoc // GetAllWallets godoc
// @Summary Get all wallets // @Summary Get all wallets
@ -76,18 +69,27 @@ func GetWalletByID(logger *slog.Logger, walletSvc *wallet.Service, validator *cu
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /wallet [get] // @Router /wallet [get]
func GetAllWallets(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetAllWallets(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type WalletRes struct {
wallets, err := walletSvc.GetAllWallets(c.Context()) ID int64 `json:"id" example:"1"`
Balance float32 `json:"amount" example:"100.0"`
if err != nil { IsWithdraw bool `json:"is_withdraw" example:"true"`
logger.Error("Failed to get wallets", "error", err) IsBettable bool `json:"is_bettable" example:"true"`
IsActive bool `json:"is_active" example:"true"`
UserID int64 `json:"user_id" example:"1"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
} }
var res []WalletRes = make([]WalletRes, len(wallets)) wallets, err := h.walletSvc.GetAllWallets(c.Context())
if err != nil {
h.logger.Error("Failed to get wallets", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve wallets")
}
for _, wallet := range wallets { res := make([]WalletRes, len(wallets))
res = append(res, WalletRes{ for i, wallet := range wallets {
res[i] = WalletRes{
ID: wallet.ID, ID: wallet.ID,
Balance: wallet.Balance.Float64(), Balance: wallet.Balance.Float64(),
IsWithdraw: wallet.IsWithdraw, IsWithdraw: wallet.IsWithdraw,
@ -96,15 +98,10 @@ func GetAllWallets(logger *slog.Logger, walletSvc *wallet.Service, validator *cu
UserID: wallet.UserID, UserID: wallet.UserID,
UpdatedAt: wallet.UpdatedAt, UpdatedAt: wallet.UpdatedAt,
CreatedAt: wallet.CreatedAt, CreatedAt: wallet.CreatedAt,
})
}
return response.WriteJSON(c, fiber.StatusOK, "All Wallets retrieved", res, nil)
} }
} }
type UpdateWalletActiveReq struct { return response.WriteJSON(c, fiber.StatusOK, "All wallets retrieved successfully", res, nil)
IsActive bool
} }
// UpdateWalletActive godoc // UpdateWalletActive godoc
@ -119,47 +116,36 @@ type UpdateWalletActiveReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /wallet/{id} [patch] // @Router /wallet/{id} [patch]
func UpdateWalletActive(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) UpdateWalletActive(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type UpdateWalletActiveReq struct {
IsActive bool `json:"is_active" validate:"required" example:"true"`
}
walletID := c.Params("id") walletID := c.Params("id")
id, err := strconv.ParseInt(walletID, 10, 64) id, err := strconv.ParseInt(walletID, 10, 64)
if err != nil { if err != nil {
logger.Error("Invalid bet ID", "walletID", walletID, "error", err) h.logger.Error("Invalid wallet ID", "walletID", walletID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) return fiber.NewError(fiber.StatusBadRequest, "Invalid wallet ID")
} }
var req UpdateWalletActiveReq var req UpdateWalletActiveReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
logger.Error("UpdateWalletActiveReq failed", "error", err) h.logger.Error("Failed to parse UpdateWalletActive request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
"error": "Invalid request",
})
} }
err = walletSvc.UpdateWalletActive(c.Context(), id, req.IsActive) if valErrs, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
err = h.walletSvc.UpdateWalletActive(c.Context(), id, req.IsActive)
if err != nil { if err != nil {
logger.Error("Failed to update", "walletID", id, "error", err) h.logger.Error("Failed to update wallet active status", "walletID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update wallet", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update wallet")
} }
return response.WriteJSON(c, fiber.StatusOK, "Wallet successfully updated", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Wallet successfully updated", nil, nil)
} }
}
type CustomerWalletRes struct {
ID int64 `json:"id" example:"1"`
RegularID int64 `json:"regular_id" example:"1"`
RegularBalance float32 `json:"regular_balance" example:"100.0"`
StaticID int64 `json:"static_id" example:"1"`
StaticBalance float32 `json:"static_balance" example:"100.0"`
CustomerID int64 `json:"customer_id" example:"1"`
CompanyID int64 `json:"company_id" example:"1"`
RegularUpdatedAt time.Time `json:"regular_updated_at"`
StaticUpdatedAt time.Time `json:"static_updated_at"`
CreatedAt time.Time `json:"created_at"`
}
// GetCustomerWallet godoc // GetCustomerWallet godoc
// @Summary Get customer wallet // @Summary Get customer wallet
@ -173,27 +159,51 @@ type CustomerWalletRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/wallet [get] // @Router /user/wallet [get]
func GetCustomerWallet(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type CustomerWalletRes struct {
ID int64 `json:"id" example:"1"`
RegularID int64 `json:"regular_id" example:"1"`
RegularBalance float32 `json:"regular_balance" example:"100.0"`
StaticID int64 `json:"static_id" example:"1"`
StaticBalance float32 `json:"static_balance" example:"100.0"`
CustomerID int64 `json:"customer_id" example:"1"`
CompanyID int64 `json:"company_id" example:"1"`
RegularUpdatedAt time.Time `json:"regular_updated_at"`
StaticUpdatedAt time.Time `json:"static_updated_at"`
CreatedAt time.Time `json:"created_at"`
}
userId := c.Locals("user_id").(int64) userID, ok := c.Locals("user_id").(int64)
role := string(c.Locals("role").(domain.Role)) if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
role, ok := c.Locals("role").(domain.Role)
if !ok {
h.logger.Error("Invalid role in context", "userID", userID)
return fiber.NewError(fiber.StatusUnauthorized, "Invalid role")
}
if role != domain.RoleCustomer {
h.logger.Error("Unauthorized access", "userID", userID, "role", role)
return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized access")
}
companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64) companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64)
if err != nil { if err != nil {
return c.Status(fiber.StatusBadRequest).SendString("Invalid company_id") h.logger.Error("Invalid company_id header", "value", c.Get("company_id"), "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid company_id")
} }
logger.Info("Company ID: " + strconv.FormatInt(companyID, 10))
if role != string(domain.RoleCustomer) { h.logger.Info("Fetching customer wallet", "userID", userID, "companyID", companyID)
logger.Error("Unauthorized access", "userId", userId, "role", role)
return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID, companyID)
}
wallet, err := walletSvc.GetCustomerWallet(c.Context(), userId, companyID)
if err != nil { if err != nil {
logger.Error("Failed to get customer wallet", "userId", userId, "error", err) h.logger.Error("Failed to get customer wallet", "userID", userID, "companyID", companyID, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallet", err, nil) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve wallet")
} }
res := CustomerWalletRes{ res := CustomerWalletRes{
ID: wallet.ID, ID: wallet.ID,
RegularID: wallet.RegularID, RegularID: wallet.RegularID,
@ -206,7 +216,6 @@ func GetCustomerWallet(logger *slog.Logger, walletSvc *wallet.Service, validator
StaticUpdatedAt: wallet.StaticUpdatedAt, StaticUpdatedAt: wallet.StaticUpdatedAt,
CreatedAt: wallet.CreatedAt, CreatedAt: wallet.CreatedAt,
} }
return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil)
} return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil)
} }

View File

@ -12,68 +12,97 @@ import (
) )
func (a *App) initAppRoutes() { func (a *App) initAppRoutes() {
handler := handlers.New(a.logger, a.NotidicationStore, a.validator) h := handlers.New(
a.logger,
a.NotidicationStore,
a.validator,
a.walletSvc,
a.referralSvc,
a.userSvc,
a.transactionSvc,
a.ticketSvc,
a.betSvc,
a.authSvc,
a.JwtConfig,
)
a.fiber.Post("/auth/login", handlers.LoginCustomer(a.logger, a.authSvc, a.validator, a.JwtConfig)) // Auth Routes
a.fiber.Post("/auth/refresh", a.authMiddleware, handlers.RefreshToken(a.logger, a.authSvc, a.validator, a.JwtConfig)) a.fiber.Post("/auth/login", h.LoginCustomer)
a.fiber.Post("/auth/logout", a.authMiddleware, handlers.LogOutCustomer(a.logger, a.authSvc, a.validator)) a.fiber.Post("/auth/refresh", a.authMiddleware, h.RefreshToken)
a.fiber.Post("/auth/logout", a.authMiddleware, h.LogOutCustomer)
a.fiber.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error { a.fiber.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error {
userId := c.Locals("user_id").(int64) userID, ok := c.Locals("user_id").(int64)
role := string(c.Locals("role").(domain.Role)) if !ok {
refreshToken := (c.Locals("refresh_token").(string)) return fiber.NewError(fiber.StatusUnauthorized, "Invalid user ID")
}
role, ok := c.Locals("role").(domain.Role)
if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid role")
}
refreshToken, ok := c.Locals("refresh_token").(string)
if !ok {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid refresh token")
}
companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64) companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64)
if err != nil { if err != nil {
return c.Status(fiber.StatusBadRequest).SendString("Invalid company_id") return fiber.NewError(fiber.StatusBadRequest, "Invalid company_id")
} }
// a.logger.Info("User ID: " + string(userId.(string))) //panic: interface conversion: interface {} is int64, not string
a.logger.Info("User ID: " + strconv.FormatInt(userId, 10)) a.logger.Info("User ID: " + strconv.FormatInt(userID, 10))
fmt.Printf("User ID: %d\n", userId) fmt.Printf("User ID: %d\n", userID)
a.logger.Info("Role: " + role) a.logger.Info("Role: " + string(role))
a.logger.Info("Refresh Token: " + refreshToken) a.logger.Info("Refresh Token: " + refreshToken)
a.logger.Info("Company ID: " + strconv.FormatInt(companyID, 10)) a.logger.Info("Company ID: " + strconv.FormatInt(companyID, 10))
return c.SendString("Test endpoint") return c.SendString("Test endpoint")
}) })
a.fiber.Post("/user/resetPassword", handlers.ResetPassword(a.logger, a.userSvc, a.validator))
a.fiber.Post("/user/sendResetCode", handlers.SendResetCode(a.logger, a.userSvc, a.validator))
a.fiber.Post("/user/register", handlers.RegisterUser(a.logger, a.userSvc, a.walletSvc, a.validator))
a.fiber.Post("/user/sendRegisterCode", handlers.SendRegisterCode(a.logger, a.userSvc, a.validator))
a.fiber.Post("/user/checkPhoneEmailExist", handlers.CheckPhoneEmailExist(a.logger, a.userSvc, a.validator))
a.fiber.Get("/user/profile", a.authMiddleware, handlers.UserProfile(a.logger, a.userSvc))
a.fiber.Get("/user/wallet", a.authMiddleware, handlers.GetCustomerWallet(a.logger, a.walletSvc, a.validator)) // User Routes
a.fiber.Post("/user/resetPassword", h.ResetPassword)
a.fiber.Post("/user/sendResetCode", h.SendResetCode)
a.fiber.Post("/user/register", h.RegisterUser)
a.fiber.Post("/user/sendRegisterCode", h.SendRegisterCode)
a.fiber.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist)
a.fiber.Get("/user/profile", a.authMiddleware, h.UserProfile)
// Wallet Routes
a.fiber.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet)
a.fiber.Get("/wallet", h.GetAllWallets)
a.fiber.Get("/wallet/:id", h.GetWalletByID)
a.fiber.Patch("/wallet/:id", h.UpdateWalletActive)
// Referral Routes
a.fiber.Post("/referral/create", a.authMiddleware, h.CreateReferral)
a.fiber.Get("/referral/stats", a.authMiddleware, h.GetReferralStats)
a.fiber.Get("/referral/settings", h.GetReferralSettings)
a.fiber.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings)
// Swagger // Swagger
a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())
// Ticket // Ticket Routes
a.fiber.Post("/ticket", handlers.CreateTicket(a.logger, a.ticketSvc, a.validator)) a.fiber.Post("/ticket", h.CreateTicket)
a.fiber.Get("/ticket", handlers.GetAllTickets(a.logger, a.ticketSvc, a.validator)) a.fiber.Get("/ticket", h.GetAllTickets)
a.fiber.Get("/ticket/:id", handlers.GetTicketByID(a.logger, a.ticketSvc, a.validator)) a.fiber.Get("/ticket/:id", h.GetTicketByID)
// Bet // Bet Routes
a.fiber.Post("/bet", handlers.CreateBet(a.logger, a.betSvc, a.validator)) a.fiber.Post("/bet", h.CreateBet)
a.fiber.Get("/bet", handlers.GetAllBet(a.logger, a.betSvc, a.validator)) a.fiber.Get("/bet", h.GetAllBet)
a.fiber.Get("/bet/:id", handlers.GetBetByID(a.logger, a.betSvc, a.validator)) a.fiber.Get("/bet/:id", h.GetBetByID)
a.fiber.Patch("/bet/:id", handlers.UpdateCashOut(a.logger, a.betSvc, a.validator)) a.fiber.Patch("/bet/:id", h.UpdateCashOut)
a.fiber.Delete("/bet/:id", handlers.DeleteBet(a.logger, a.betSvc, a.validator)) a.fiber.Delete("/bet/:id", h.DeleteBet)
// Wallet // Transaction Routes
a.fiber.Get("/wallet", handlers.GetAllWallets(a.logger, a.walletSvc, a.validator)) a.fiber.Post("/transaction", h.CreateTransaction)
a.fiber.Get("/wallet/:id", handlers.GetWalletByID(a.logger, a.walletSvc, a.validator)) a.fiber.Get("/transaction", h.GetAllTransactions)
a.fiber.Put("/wallet/:id", handlers.UpdateWalletActive(a.logger, a.walletSvc, a.validator)) a.fiber.Get("/transaction/:id", h.GetTransactionByID)
a.fiber.Patch("/transaction/:id", h.UpdateTransactionVerified)
// Transactions /transactions // Notification Routes
a.fiber.Post("/transaction", handlers.CreateTransaction(a.logger, a.transactionSvc, a.validator)) a.fiber.Get("/notifications/ws/connect/:recipientID", h.ConnectSocket)
a.fiber.Get("/transaction", handlers.GetAllTransactions(a.logger, a.transactionSvc, a.validator)) a.fiber.Post("/notifications/mark-as-read", h.MarkNotificationAsRead)
a.fiber.Get("/transaction/:id", handlers.GetTransactionByID(a.logger, a.transactionSvc, a.validator)) a.fiber.Post("/notifications/create", h.CreateAndSendNotification)
a.fiber.Patch("/transaction/:id", handlers.UpdateTransactionVerified(a.logger, a.transactionSvc, a.validator))
a.fiber.Get("/notifications/ws/connect/:recipientID", handler.ConnectSocket)
a.fiber.Post("/notifications/mark-as-read", handler.MarkNotificationAsRead)
a.fiber.Post("/notifications/create", handler.CreateAndSendNotification)
} }
///user/profile get
// @Router /user/resetPassword [post] // @Router /user/resetPassword [post]
// @Router /user/sendResetCode [post] // @Router /user/sendResetCode [post]
// @Router /user/register [post] // @Router /user/register [post]