diff --git a/README.md b/README.md index 353769b..7f428e7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# FortuneBet-Backend + + # Directory Structure ├── cmd @@ -9,13 +10,19 @@ │ │ ├── 000001_fortune.up.sql │ │ ├── 000002_notification.down.sql │ │ ├── 000002_notification.up.sql +│ │ ├── 000003_referal.down.sql +│ │ ├── 000003_referal.up.sql │ └── query │ ├── auth.sql │ ├── bet.sql │ ├── notification.sql │ ├── otp.sql +│ ├── referal.sql │ ├── ticket.sql +│ ├── transactions.sql +│ ├── transfer.sql │ ├── user.sql +│ ├── wallet.sql ├── docs │ ├── docs.go │ ├── swagger.json @@ -28,8 +35,12 @@ │ ├── models.go │ ├── notification.sql.go │ ├── otp.sql.go +│ ├── referal.sql.go │ ├── ticket.sql.go +│ ├── transactions.sql.go +│ ├── transfer.sql.go │ ├── user.sql.go +│ ├── wallet.sql.go └── internal ├── config │ ├── config.go @@ -41,9 +52,13 @@ │ ├── event.go │ ├── notification.go │ ├── otp.go + │ ├── referal.go │ ├── role.go │ ├── ticket.go + │ ├── transaction.go + │ ├── transfer.go │ ├── user.go + │ ├── wallet.go ├── logger │ ├── logger.go ├── mocks @@ -59,9 +74,13 @@ │ ├── bet.go │ ├── notification.go │ ├── otp.go + │ ├── referal.go │ ├── store.go │ ├── ticket.go + │ ├── transaction.go + │ ├── transfer.go │ ├── user.go + │ ├── wallet.go ├── services │ ├── authentication │ │ ├── impl.go @@ -73,6 +92,9 @@ │ ├── notfication │ │ ├── port.go │ │ ├── service.go + │ ├── referal + │ │ ├── port.go + │ │ ├── service.go │ ├── sportsbook │ │ ├── events.go │ │ ├── odds.go @@ -80,20 +102,33 @@ │ ├── ticket │ │ ├── port.go │ │ ├── service.go - │ └── user - │ ├── common.go + │ ├── transaction + │ │ ├── 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 - │ ├── register.go - │ ├── reset.go │ ├── service.go - │ ├── user.go └── web_server ├── handlers │ ├── auth_handler.go │ ├── bet_handler.go + │ ├── handlers.go │ ├── notification_handler.go │ ├── ticket_handler.go + │ ├── transaction_handler.go │ ├── user.go + │ ├── wallet_handler.go ├── jwt │ ├── jwt.go │ ├── jwt_test.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index ed91221..622b60a 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS users ( -- suspended_at TIMESTAMPTZ NULL, -- this can be NULL if the user is not suspended suspended BOOLEAN NOT NULL DEFAULT FALSE, - CHECK (email IS NOT NULL OR phone_number IS NOT NULL) + CHECK (email IS NOT NULL OR phone_number IS NOT NULL) ); CREATE TABLE refresh_tokens ( id BIGSERIAL PRIMARY KEY, @@ -28,13 +28,13 @@ CREATE TABLE refresh_tokens ( CREATE TABLE otps ( id BIGSERIAL PRIMARY KEY, sent_to VARCHAR(255) NOT NULL, - medium VARCHAR(50) NOT NULL, - otp_for VARCHAR(50) NOT NULL, - otp VARCHAR(10) NOT NULL, + medium VARCHAR(50) NOT NULL, + otp_for VARCHAR(50) NOT NULL, + otp VARCHAR(10) NOT NULL, used BOOLEAN NOT NULL DEFAULT FALSE, - used_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMPTZ NOT NULL + used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS bets ( @@ -132,23 +132,20 @@ CREATE TABLE IF NOT EXISTS transactions ( CREATE EXTENSION IF NOT EXISTS pgcrypto; INSERT INTO users ( - first_name, last_name, email, phone_number, password, role, - email_verified, phone_verified, created_at, updated_at, + first_name, last_name, email, phone_number, password, role, + email_verified, phone_verified, created_at, updated_at, suspended_at, suspended ) VALUES ( - 'John', - 'Doe', - 'john.doe@example.com', - NULL, - crypt('password123', gen_salt('bf'))::bytea, - 'customer', - TRUE, - FALSE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - NULL, - FALSE + 'John', + 'Doe', + 'john.doe@example.com', + NULL, + crypt('password123', gen_salt('bf'))::bytea, + 'customer', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + NULL, + FALSE ); - - - diff --git a/db/migrations/000003_referal.down.sql b/db/migrations/000003_referal.down.sql new file mode 100644 index 0000000..911d3ed --- /dev/null +++ b/db/migrations/000003_referal.down.sql @@ -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; diff --git a/db/migrations/000003_referal.up.sql b/db/migrations/000003_referal.up.sql new file mode 100644 index 0000000..4f8a181 --- /dev/null +++ b/db/migrations/000003_referal.up.sql @@ -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); diff --git a/db/query/referal.sql b/db/query/referal.sql new file mode 100644 index 0000000..8f0adaa --- /dev/null +++ b/db/query/referal.sql @@ -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; diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index c826c36..e9f0597 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -55,7 +55,7 @@ func (q *Queries) GetRefreshToken(ctx context.Context, token string) (RefreshTok } 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 ` @@ -81,6 +81,8 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho &i.UpdatedAt, &i.SuspendedAt, &i.Suspended, + &i.ReferralCode, + &i.ReferredBy, ) return i, err } diff --git a/gen/db/models.go b/gen/db/models.go index 0dd3704..63f96fe 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -5,9 +5,56 @@ package dbgen import ( + "database/sql/driver" + "fmt" + "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 { ID int64 Amount int64 @@ -62,6 +109,32 @@ type Otp struct { 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 { ID int64 UserID int64 @@ -112,17 +185,21 @@ type User struct { UpdatedAt pgtype.Timestamptz SuspendedAt pgtype.Timestamptz Suspended bool + ReferralCode pgtype.Text + ReferredBy pgtype.Text } type Wallet struct { - ID int64 - Balance int64 - IsWithdraw bool - IsBettable bool - UserID int64 - IsActive bool - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + ID int64 + Balance int64 + IsWithdraw bool + IsBettable bool + UserID int64 + IsActive bool + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + BonusBalance pgtype.Numeric + CashBalance pgtype.Numeric } type WalletTransfer struct { diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go new file mode 100644 index 0000000..a2e6306 --- /dev/null +++ b/gen/db/referal.sql.go @@ -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 +} diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 39f0a5c..b725021 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -193,7 +193,7 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUse } 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 WHERE id = $1 ` @@ -215,6 +215,8 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { &i.UpdatedAt, &i.SuspendedAt, &i.Suspended, + &i.ReferralCode, + &i.ReferredBy, ) return i, err } diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index de555d9..7c57b08 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -43,7 +43,7 @@ func (q *Queries) CreateCustomerWallet(ctx context.Context, arg CreateCustomerWa } 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 { @@ -64,12 +64,14 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.BonusBalance, + &i.CashBalance, ) return i, err } 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) { @@ -90,6 +92,8 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.BonusBalance, + &i.CashBalance, ); err != nil { return nil, err } @@ -156,7 +160,7 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, arg GetCustomerWalletPa } 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) { @@ -171,12 +175,14 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.BonusBalance, + &i.CashBalance, ) return i, err } 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) { @@ -197,6 +203,8 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.BonusBalance, + &i.CashBalance, ); err != nil { return nil, err } diff --git a/internal/domain/referal.go b/internal/domain/referal.go new file mode 100644 index 0000000..9923806 --- /dev/null +++ b/internal/domain/referal.go @@ -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 +} diff --git a/internal/domain/user.go b/internal/domain/user.go index ea44cc8..b47928a 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -34,10 +34,9 @@ type RegisterUserReq struct { PhoneNumber string Password string //Role string - Otp string - ReferalCode string - // - OtpMedium OtpMedium + Otp string + ReferralCode string `json:"referral_code"` + OtpMedium OtpMedium } type ResetPasswordReq struct { Email string diff --git a/internal/repository/referal.go b/internal/repository/referal.go new file mode 100644 index 0000000..022b827 --- /dev/null +++ b/internal/repository/referal.go @@ -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, + } +} diff --git a/internal/services/referal/port.go b/internal/services/referal/port.go new file mode 100644 index 0000000..a51c4b5 --- /dev/null +++ b/internal/services/referal/port.go @@ -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 +} diff --git a/internal/services/referal/service.go b/internal/services/referal/service.go new file mode 100644 index 0000000..c821b18 --- /dev/null +++ b/internal/services/referal/service.go @@ -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) +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index bdf1b33..34fdd5b 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -6,6 +6,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "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/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -22,13 +23,14 @@ type App struct { fiber *fiber.App logger *slog.Logger NotidicationStore notificationservice.NotificationStore + referralSvc referralservice.ReferralStore port int authSvc *authentication.Service userSvc *user.Service - ticketSvc *ticket.Service betSvc *bet.Service walletSvc *wallet.Service transactionSvc *transaction.Service + ticketSvc *ticket.Service validator *customvalidator.CustomValidator JwtConfig jwtutil.JwtConfig Logger *slog.Logger @@ -45,6 +47,7 @@ func NewApp( walletSvc *wallet.Service, transactionSvc *transaction.Service, notidicationStore notificationservice.NotificationStore, + referralSvc referralservice.ReferralStore, ) *App { app := fiber.New(fiber.Config{ CaseSensitive: true, @@ -65,6 +68,7 @@ func NewApp( walletSvc: walletSvc, transactionSvc: transactionSvc, NotidicationStore: notidicationStore, + referralSvc: referralSvc, Logger: logger, } diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 0022827..88ca2f1 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -2,26 +2,13 @@ package handlers import ( "errors" - "log/slog" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" - customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "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 // @Summary Login customer // @Description Login customer @@ -34,49 +21,50 @@ type loginCustomerRes struct { // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /auth/login [post] -func LoginCustomer( - logger *slog.Logger, authSvc *authentication.Service, - validator *customvalidator.CustomValidator, JwtConfig jwtutil.JwtConfig) fiber.Handler { - return func(c *fiber.Ctx) error { - var req loginCustomerReq - if err := c.BodyParser(&req); err != nil { - logger.Error("Login failed", "error", err) - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) - } - valErrs, ok := validator.Validate(c, req) - if !ok { - - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - successRes, err := authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password) - if err != nil { - logger.Info("Login failed", "error", err) - if errors.Is(err, authentication.ErrInvalidPassword) { - response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid password or not registered", nil, nil) - return nil - } - 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, JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry) - res := loginCustomerRes{ - AccessToken: accessToken, - RefreshToken: successRes.RfToken, - } - return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) +func (h *Handler) LoginCustomer(c *fiber.Ctx) error { + type loginCustomerReq struct { + 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" example:"password123"` + } + type loginCustomerRes struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` } -} -type refreshToken struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` + var req loginCustomerReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse LoginCustomer request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password) + if err != nil { + h.logger.Info("Login attempt failed", "email", req.Email, "phone", req.PhoneNumber, "error", err) + switch { + case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): + return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials") + default: + h.logger.Error("Login failed", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") + } + } + + 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") + } + + res := loginCustomerRes{ + AccessToken: accessToken, + RefreshToken: successRes.RfToken, + } + return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) } // RefreshToken godoc @@ -91,50 +79,52 @@ type refreshToken struct { // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /auth/refresh [post] -func RefreshToken(logger *slog.Logger, authSvc *authentication.Service, - validator *customvalidator.CustomValidator, JwtConfig jwtutil.JwtConfig) fiber.Handler { - return func(c *fiber.Ctx) error { - var req refreshToken - if err := c.BodyParser(&req); err != nil { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) - } - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - rf, err := authSvc.RefreshToken(c.Context(), req.RefreshToken) - if err != nil { - logger.Info("Refresh token failed", "error", err) - if errors.Is(err, authentication.ErrExpiredToken) { - response.WriteJSON(c, fiber.StatusUnauthorized, "The refresh token has expired", nil, nil) - return nil - } - 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) - return nil - } - accessToken, err := jwtutil.CreateJwt(0, "", JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry) - if err != nil { - logger.Error("Create jwt failed", "error", err) - response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil) - return nil - } - - res := loginCustomerRes{ - AccessToken: accessToken, - RefreshToken: rf, - } - return response.WriteJSON(c, fiber.StatusOK, "refresh successful", res, nil) +func (h *Handler) RefreshToken(c *fiber.Ctx) error { + type refreshTokenReq struct { + AccessToken string `json:"access_token" validate:"required" example:""` + RefreshToken string `json:"refresh_token" validate:"required" example:""` + } + type loginCustomerRes struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` } -} -type logoutReq struct { - RefreshToken string `json:"refresh_token"` + var req refreshTokenReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse RefreshToken request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + rf, err := h.authSvc.RefreshToken(c.Context(), req.RefreshToken) + if err != nil { + h.logger.Info("Refresh token attempt failed", "refreshToken", req.RefreshToken, "error", err) + switch { + case errors.Is(err, authentication.ErrExpiredToken): + 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") + } + } + + // Assuming the refreshed token includes userID and role info; adjust if needed + accessToken, err := jwtutil.CreateJwt(0, "", h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) + if err != nil { + h.logger.Error("Failed to create new access token", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") + } + + res := loginCustomerRes{ + AccessToken: accessToken, + RefreshToken: rf, + } + return response.WriteJSON(c, fiber.StatusOK, "Refresh successful", res, nil) } // LogOutCustomer godoc @@ -149,34 +139,34 @@ type logoutReq struct { // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /auth/logout [post] -func LogOutCustomer( - logger *slog.Logger, authSvc *authentication.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - var req logoutReq - if err := c.BodyParser(&req); err != nil { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) - } - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - err := authSvc.Logout(c.Context(), req.RefreshToken) - if err != nil { - logger.Info("Logout failed", "error", err) - if errors.Is(err, authentication.ErrExpiredToken) { - response.WriteJSON(c, fiber.StatusUnauthorized, "The refresh token has expired", nil, nil) - return nil - } - 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) +func (h *Handler) LogOutCustomer(c *fiber.Ctx) error { + type logoutReq struct { + RefreshToken string `json:"refresh_token" validate:"required" example:""` } + + var req logoutReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse LogOutCustomer request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + err := h.authSvc.Logout(c.Context(), req.RefreshToken) + if err != nil { + h.logger.Info("Logout attempt failed", "refreshToken", req.RefreshToken, "error", err) + switch { + case errors.Is(err, authentication.ErrExpiredToken): + 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") + } + } + + return response.WriteJSON(c, fiber.StatusOK, "Logout successful", nil, nil) } diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 9d83592..4492714 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -1,26 +1,13 @@ package handlers import ( - "log/slog" "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" - customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "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 { ID int64 `json:"id" example:"1"` Outcomes []domain.Outcome `json:"outcomes"` @@ -34,20 +21,6 @@ type BetRes struct { 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 // @Summary Create a bet // @Description Creates a bet @@ -59,62 +32,67 @@ func convertBet(bet domain.Bet) BetRes { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet [post] -func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - - // TODO: Check the token, and find the role and get the branch id from there - - // TODO Reduce amount from the branch wallet - - var isShopBet bool = true - var branchID int64 = 1 - var userID int64 - - var req CreateBetReq - - if err := c.BodyParser(&req); err != nil { - logger.Error("CreateBetReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - - // TODO Validate Outcomes Here and make sure they didn't expire - - bet, err := betSvc.CreateBet(c.Context(), domain.CreateBet{ - Outcomes: req.Outcomes, - Amount: domain.Currency(req.Amount), - TotalOdds: req.TotalOdds, - Status: req.Status, - FullName: req.FullName, - PhoneNumber: req.PhoneNumber, - - BranchID: domain.ValidInt64{ - Value: branchID, - Valid: isShopBet, - }, - UserID: domain.ValidInt64{ - Value: userID, - Valid: !isShopBet, - }, - IsShopBet: req.IsShopBet, - }) - - if err != nil { - logger.Error("CreateBetReq failed", "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) - } - - res := convertBet(bet) - - return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) +func (h *Handler) CreateBet(c *fiber.Ctx) error { + type CreateBetReq struct { + 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"` + Status domain.BetStatus `json:"status" validate:"required" example:"1"` + FullName string `json:"full_name" example:"John"` + PhoneNumber string `json:"phone_number" validate:"required" example:"1234567890"` + IsShopBet bool `json:"is_shop_bet" example:"false"` } + + var req CreateBetReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse CreateBet request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + // TODO: Check the token, find the role, and get the branch ID from there + isShopBet := true + branchID := int64(1) + var userID int64 + + // TODO: Validate Outcomes Here and make sure they didn't expire + + bet, err := h.betSvc.CreateBet(c.Context(), domain.CreateBet{ + Outcomes: req.Outcomes, + Amount: domain.Currency(req.Amount), + TotalOdds: req.TotalOdds, + Status: req.Status, + FullName: req.FullName, + PhoneNumber: req.PhoneNumber, + BranchID: domain.ValidInt64{ + Value: branchID, + Valid: isShopBet, + }, + UserID: domain.ValidInt64{ + Value: userID, + Valid: !isShopBet, + }, + IsShopBet: req.IsShopBet, + }) + if err != nil { + h.logger.Error("Failed to create bet", "error", err) + 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) + return response.WriteJSON(c, fiber.StatusOK, "Bet created successfully", res, nil) } // GetAllBet godoc @@ -127,22 +105,19 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet [get] -func GetAllBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - bets, err := betSvc.GetAllBets(c.Context()) - - if err != nil { - logger.Error("Failed to get bets", "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bets", err, nil) - } - - var res []BetRes = make([]BetRes, len(bets)) - for _, bet := range bets { - res = append(res, convertBet(bet)) - } - - return response.WriteJSON(c, fiber.StatusOK, "All Bets Retrieved", res, nil) +func (h *Handler) GetAllBet(c *fiber.Ctx) error { + bets, err := h.betSvc.GetAllBets(c.Context()) + if err != nil { + h.logger.Error("Failed to get bets", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets") } + + res := make([]BetRes, len(bets)) + for i, bet := range bets { + res[i] = convertBet(bet) + } + + return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil) } // GetBetByID godoc @@ -156,32 +131,35 @@ func GetAllBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet/{id} [get] -func GetBetByID(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - betID := c.Params("id") - id, err := strconv.ParseInt(betID, 10, 64) - - if err != nil { - logger.Error("Invalid bet ID", "betID", betID, "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) - } - - bet, err := betSvc.GetBetByID(c.Context(), id) - - if err != nil { - logger.Error("Failed to get bet by ID", "betID", id, "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil) - } - - res := convertBet(bet) - - return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) - +func (h *Handler) GetBetByID(c *fiber.Ctx) error { + type BetRes 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"` + Status domain.BetStatus `json:"status" example:"1"` + 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"` } -} -type UpdateCashOutReq struct { - CashedOut bool + betID := c.Params("id") + id, err := strconv.ParseInt(betID, 10, 64) + if err != nil { + h.logger.Error("Invalid bet ID", "betID", betID, "error", err) + 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) + return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) } // UpdateCashOut godoc @@ -196,40 +174,35 @@ type UpdateCashOutReq struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet/{id} [patch] -func UpdateCashOut(logger *slog.Logger, betSvc *bet.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - betID := c.Params("id") - id, err := strconv.ParseInt(betID, 10, 64) - - if err != nil { - logger.Error("Invalid bet ID", "betID", betID, "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) - } - - var req UpdateCashOutReq - if err := c.BodyParser(&req); err != nil { - logger.Error("UpdateCashOutReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - - err = betSvc.UpdateCashOut(c.Context(), id, req.CashedOut) - - if err != nil { - 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 response.WriteJSON(c, fiber.StatusOK, "Bet updated successfully", nil, nil) +func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { + type UpdateCashOutReq struct { + CashedOut bool `json:"cashed_out" validate:"required" example:"true"` } + + betID := c.Params("id") + id, err := strconv.ParseInt(betID, 10, 64) + if err != nil { + h.logger.Error("Invalid bet ID", "betID", betID, "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") + } + + var req UpdateCashOutReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse UpdateCashOut request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + err = h.betSvc.UpdateCashOut(c.Context(), id, req.CashedOut) + if err != nil { + h.logger.Error("Failed to update cash out bet", "betID", id, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update cash out bet") + } + + return response.WriteJSON(c, fiber.StatusOK, "Bet updated successfully", nil, nil) } // DeleteBet godoc @@ -243,24 +216,34 @@ func UpdateCashOut(logger *slog.Logger, betSvc *bet.Service, // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet/{id} [delete] -func DeleteBet(logger *slog.Logger, betSvc *bet.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - betID := c.Params("id") - id, err := strconv.ParseInt(betID, 10, 64) +func (h *Handler) DeleteBet(c *fiber.Ctx) error { + betID := c.Params("id") + id, err := strconv.ParseInt(betID, 10, 64) + if err != nil { + h.logger.Error("Invalid bet ID", "betID", betID, "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") + } - if err != nil { - logger.Error("Invalid bet ID", "betID", betID, "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) - } + err = h.betSvc.DeleteBet(c.Context(), id) + if err != nil { + h.logger.Error("Failed to delete bet by ID", "betID", id, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete bet") + } - err = betSvc.DeleteBet(c.Context(), id) + return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil) +} - if err != nil { - logger.Error("Failed to delete by ID", "betID", id, "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to delete bet", err, 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, } } diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 4c3c770..6e3eb50 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -3,20 +3,45 @@ package handlers import ( "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" + 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" ) type Handler struct { logger *slog.Logger 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 } -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{ logger: logger, notificationSvc: notificationSvc, + walletSvc: walletSvc, + referralSvc: referralSvc, validator: validator, + userSvc: userSvc, + transactionSvc: transactionSvc, + ticketSvc: ticketSvc, + betSvc: betSvc, + authSvc: authSvc, + jwtConfig: jwtConfig, } } diff --git a/internal/web_server/handlers/referal_handlers.go b/internal/web_server/handlers/referal_handlers.go new file mode 100644 index 0000000..91fdf64 --- /dev/null +++ b/internal/web_server/handlers/referal_handlers.go @@ -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) +} diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index 68bba4b..310c149 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -1,25 +1,13 @@ package handlers import ( - "log/slog" "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" - customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "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 // @Summary Create a temporary ticket // @Description Creates a temporary ticket @@ -31,48 +19,43 @@ type CreateTicketRes struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /ticket [post] -func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - var req CreateTicketReq - if err := c.BodyParser(&req); err != nil { - logger.Error("CreateTicketReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - - // TODO Validate Outcomes Here and make sure they didn't expire - - ticket, err := ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ - Outcomes: req.Outcomes, - Amount: domain.Currency(req.Amount), - TotalOdds: req.TotalOdds, - }) - if err != nil { - logger.Error("CreateTicketReq failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Internal server error", - }) - } - res := CreateTicketRes{ - FastCode: ticket.ID, - } - return response.WriteJSON(c, fiber.StatusOK, "Ticket Created", res, nil) +func (h *Handler) CreateTicket(c *fiber.Ctx) error { + type CreateTicketReq struct { + 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 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"` + type CreateTicketRes struct { + FastCode int64 `json:"fast_code" example:"1234"` + } + + var req CreateTicketReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse CreateTicket request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + // TODO: Validate Outcomes Here and make sure they didn't expire + + ticket, err := h.ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ + Outcomes: req.Outcomes, + Amount: domain.Currency(req.Amount), + TotalOdds: req.TotalOdds, + }) + if err != nil { + h.logger.Error("Failed to create ticket", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create ticket") + } + + res := CreateTicketRes{ + FastCode: ticket.ID, + } + return response.WriteJSON(c, fiber.StatusOK, "Ticket created successfully", res, nil) } // GetTicketByID godoc @@ -86,34 +69,34 @@ type TicketRes struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /ticket/{id} [get] -func GetTicketByID(logger *slog.Logger, ticketSvc *ticket.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - ticketID := c.Params("id") - - id, err := strconv.ParseInt(ticketID, 10, 64) - 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) - - if err != nil { - logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve ticket", err, nil) - } - - res := TicketRes{ - ID: ticket.ID, - Outcomes: ticket.Outcomes, - Amount: ticket.Amount.Float64(), - TotalOdds: ticket.TotalOdds, - } - - return response.WriteJSON(c, fiber.StatusOK, "Ticket retrieved successfully", res, nil) - +func (h *Handler) GetTicketByID(c *fiber.Ctx) error { + 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"` } + + ticketID := c.Params("id") + id, err := strconv.ParseInt(ticketID, 10, 64) + if err != nil { + h.logger.Error("Invalid ticket ID", "ticketID", ticketID, "error", err) + 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{ + ID: ticket.ID, + Outcomes: ticket.Outcomes, + Amount: ticket.Amount.Float64(), + TotalOdds: ticket.TotalOdds, + } + return response.WriteJSON(c, fiber.StatusOK, "Ticket retrieved successfully", res, nil) } // GetAllTickets godoc @@ -126,28 +109,29 @@ func GetTicketByID(logger *slog.Logger, ticketSvc *ticket.Service, // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /ticket [get] -func GetAllTickets(logger *slog.Logger, ticketSvc *ticket.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - tickets, err := ticketSvc.GetAllTickets(c.Context()) - - if err != nil { - 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)) - - for _, ticket := range tickets { - res = append(res, TicketRes{ - ID: ticket.ID, - Outcomes: ticket.Outcomes, - Amount: ticket.Amount.Float64(), - TotalOdds: ticket.TotalOdds, - }) - } - - return response.WriteJSON(c, fiber.StatusOK, "All Tickets retrieved", res, nil) - +func (h *Handler) GetAllTickets(c *fiber.Ctx) error { + 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"` } + + 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") + } + + res := make([]TicketRes, len(tickets)) + for i, ticket := range tickets { + res[i] = TicketRes{ + ID: ticket.ID, + Outcomes: ticket.Outcomes, + Amount: ticket.Amount.Float64(), + TotalOdds: ticket.TotalOdds, + } + } + + return response.WriteJSON(c, fiber.StatusOK, "All tickets retrieved successfully", res, nil) } diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index 1d143c7..9e3519e 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -1,13 +1,10 @@ package handlers import ( - "log/slog" "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" - customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "github.com/gofiber/fiber/v2" ) @@ -75,46 +72,62 @@ func convertTransaction(transaction domain.Transaction) TransactionRes { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /transaction [post] -func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - var req CreateTransactionReq - if err := c.BodyParser(&req); err != nil { - logger.Error("CreateTransactionReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - - transaction, err := transactionSvc.CreateTransaction(c.Context(), domain.CreateTransaction{ - Amount: domain.Currency(req.Amount), - BranchID: req.BranchID, - CashierID: req.CashierID, - BetID: req.BetID, - PaymentOption: domain.PaymentOption(req.PaymentOption), - FullName: req.FullName, - PhoneNumber: req.PhoneNumber, - BankCode: req.BankCode, - BeneficiaryName: req.BeneficiaryName, - AccountName: req.AccountName, - AccountNumber: req.AccountNumber, - ReferenceNumber: req.ReferenceNumber, - }) - - if err != nil { - logger.Error("CreateTransactionReq failed", "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) - } - - res := convertTransaction(transaction) - - return response.WriteJSON(c, fiber.StatusOK, "Transaction Created", res, nil) +// Update transaction handler to include deposit bonus +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 + if err := c.BodyParser(&req); err != nil { + h.logger.Error("CreateTransaction failed to parse request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + isDeposit := req.PaymentOption == domain.BANK + + transaction, err := h.transactionSvc.CreateTransaction(c.Context(), domain.CreateTransaction{ + Amount: domain.Currency(req.Amount), + BranchID: req.BranchID, + CashierID: req.CashierID, + BetID: req.BetID, + PaymentOption: req.PaymentOption, + FullName: req.FullName, + PhoneNumber: req.PhoneNumber, + BankCode: req.BankCode, + BeneficiaryName: req.BeneficiaryName, + AccountName: req.AccountName, + AccountNumber: req.AccountNumber, + ReferenceNumber: req.ReferenceNumber, + }) + if err != nil { + h.logger.Error("CreateTransaction failed", "error", err) + 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) + return response.WriteJSON(c, fiber.StatusOK, "Transaction created successfully", res, nil) } // GetAllTransactions godoc @@ -127,22 +140,19 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /transaction [get] -func GetAllTransactions(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - transactions, err := transactionSvc.GetAllTransactions(c.Context()) - - if err != nil { - logger.Error("Failed to get transaction", "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve transaction", err, nil) - } - - var res []TransactionRes = make([]TransactionRes, len(transactions)) - for _, transaction := range transactions { - res = append(res, convertTransaction(transaction)) - } - - return response.WriteJSON(c, fiber.StatusOK, "All Transactions Retrieved", res, nil) +func (h *Handler) GetAllTransactions(c *fiber.Ctx) error { + transactions, err := h.transactionSvc.GetAllTransactions(c.Context()) + if err != nil { + h.logger.Error("Failed to get transactions", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve transactions") } + + res := make([]TransactionRes, len(transactions)) + for i, transaction := range transactions { + res[i] = convertTransaction(transaction) + } + + return response.WriteJSON(c, fiber.StatusOK, "All transactions retrieved", res, nil) } // GetTransactionByID godoc @@ -156,77 +166,63 @@ func GetAllTransactions(logger *slog.Logger, transactionSvc *transaction.Service // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /transaction/{id} [get] -func GetTransactionByID(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - transactionID := c.Params("id") - id, err := strconv.ParseInt(transactionID, 10, 64) - - if err != nil { - logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) - } - - transaction, err := transactionSvc.GetTransactionByID(c.Context(), id) - - if err != nil { - logger.Error("Failed to get transaction by ID", "transactionID", id, "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve transaction", err, nil) - } - - res := convertTransaction(transaction) - - return response.WriteJSON(c, fiber.StatusOK, "Transaction retrieved successfully", res, nil) +func (h *Handler) GetTransactionByID(c *fiber.Ctx) error { + transactionID := c.Params("id") + id, err := strconv.ParseInt(transactionID, 10, 64) + if err != nil { + h.logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid transaction ID") } -} -type UpdateTransactionVerifiedReq struct { - Verified bool + transaction, err := h.transactionSvc.GetTransactionByID(c.Context(), id) + if err != nil { + h.logger.Error("Failed to get transaction by ID", "transactionID", id, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve transaction") + } + + res := convertTransaction(transaction) + return response.WriteJSON(c, fiber.StatusOK, "Transaction retrieved successfully", res, nil) } // UpdateTransactionVerified godoc -// @Summary Updates the cashed out field -// @Description Updates the cashed out field +// @Summary Updates the verified field of a transaction +// @Description Updates the verified status of a transaction // @Tags transaction // @Accept json // @Produce json // @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 // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /transaction/{id} [patch] -func UpdateTransactionVerified(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - transactionID := c.Params("id") - id, err := strconv.ParseInt(transactionID, 10, 64) - - if err != nil { - logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) - } - - var req UpdateTransactionVerifiedReq - if err := c.BodyParser(&req); err != nil { - logger.Error("UpdateTransactionVerifiedReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - - err = transactionSvc.UpdateTransactionVerified(c.Context(), id, req.Verified) - - if err != nil { - 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 response.WriteJSON(c, fiber.StatusOK, "Transaction updated successfully", nil, nil) - +func (h *Handler) UpdateTransactionVerified(c *fiber.Ctx) error { + type UpdateTransactionVerifiedReq struct { + Verified bool `json:"verified" validate:"required" example:"true"` } + + transactionID := c.Params("id") + id, err := strconv.ParseInt(transactionID, 10, 64) + if err != nil { + h.logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid transaction ID") + } + + var req UpdateTransactionVerifiedReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse UpdateTransactionVerified request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + err = h.transactionSvc.UpdateTransactionVerified(c.Context(), id, req.Verified) + if err != nil { + h.logger.Error("Failed to update transaction verification", "transactionID", id, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transaction verification") + } + + return response.WriteJSON(c, fiber.StatusOK, "Transaction updated successfully", nil, nil) } diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 665a1ee..8565d65 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -2,27 +2,14 @@ package handlers import ( "errors" - "log/slog" "time" "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" - customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "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 // @Summary 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 500 {object} response.APIResponse // @Router /user/checkPhoneEmailExist [post] -func CheckPhoneEmailExist(logger *slog.Logger, userSvc *user.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - var req CheckPhoneEmailExistReq - if err := c.BodyParser(&req); err != nil { - logger.Error("CheckPhoneEmailExist failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - emailExist, phoneExist, err := userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email) - if err != nil { - logger.Error("CheckPhoneEmailExist failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Internal server error", - }) - } - res := CheckPhoneEmailExistRes{ - EmailExist: emailExist, - PhoneNumberExist: phoneExist, - } - return response.WriteJSON(c, fiber.StatusOK, "Check Success", res, nil) +func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error { + type CheckPhoneEmailExistReq struct { + 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"` } -} -type RegisterCodeReq struct { - Email string `json:"email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number" example:"1234567890"` + var req CheckPhoneEmailExistReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse CheckPhoneEmailExist request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + emailExist, phoneExist, err := h.userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email) + if err != nil { + h.logger.Error("Failed to check phone/email existence", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check phone/email existence") + } + + res := CheckPhoneEmailExistRes{ + EmailExist: emailExist, + PhoneNumberExist: phoneExist, + } + return response.WriteJSON(c, fiber.StatusOK, "Check successful", res, nil) } // SendRegisterCode godoc @@ -80,51 +65,40 @@ type RegisterCodeReq struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/sendRegisterCode [post] -func SendRegisterCode(logger *slog.Logger, userSvc *user.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - var req RegisterCodeReq - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - var sentTo string - var medium domain.OtpMedium - if req.Email != "" { - sentTo = req.Email - medium = domain.OtpMediumEmail - } - if req.PhoneNumber != "" { - sentTo = req.PhoneNumber - medium = domain.OtpMediumSms - } - if err := userSvc.SendRegisterCode(c.Context(), medium, sentTo); err != nil { - logger.Error("SendRegisterCode failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Internal server error", - }) - } - return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) +func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { + type RegisterCodeReq struct { + Email string `json:"email" validate:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` } -} -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"` - // + var req RegisterCodeReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse SendRegisterCode request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + var sentTo string + var medium domain.OtpMedium + if req.Email != "" { + sentTo = req.Email + medium = domain.OtpMediumEmail + } else if req.PhoneNumber != "" { + sentTo = req.PhoneNumber + medium = domain.OtpMediumSms + } else { + return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") + } + + if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo); err != nil { + h.logger.Error("Failed to send register code", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to send register code") + } + + return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) } // RegisterUser godoc @@ -138,74 +112,84 @@ type RegisterUserReq struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/register [post] -func RegisterUser(logger *slog.Logger, userSvc *user.Service, walletSvc *wallet.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - var req RegisterUserReq - if err := c.BodyParser(&req); err != nil { - logger.Error("RegisterUser failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - user := domain.RegisterUserReq{ - FirstName: req.FirstName, - LastName: req.LastName, - Email: req.Email, - PhoneNumber: req.PhoneNumber, - Password: req.Password, - Otp: req.Otp, - ReferalCode: req.ReferalCode, - OtpMedium: domain.OtpMediumEmail, - } - medium, err := getMedium(req.Email, req.PhoneNumber) - if err != nil { - logger.Error("RegisterUser failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": err.Error(), - }) - } - user.OtpMedium = medium - newUser, err := userSvc.RegisterUser(c.Context(), user) - if err != nil { - if errors.Is(err, domain.ErrOtpAlreadyUsed) { - return response.WriteJSON(c, fiber.StatusBadRequest, "Otp already used", nil, nil) - } - if errors.Is(err, domain.ErrOtpExpired) { - return response.WriteJSON(c, fiber.StatusBadRequest, "Otp expired", nil, nil) - } - if errors.Is(err, domain.ErrInvalidOtp) { - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid otp", nil, nil) - } - if errors.Is(err, domain.ErrOtpNotFound) { - return response.WriteJSON(c, fiber.StatusBadRequest, "User already exist", nil, nil) - } - logger.Error("RegisterUser failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Internal server error", - }) - } - - // TODO: Integrate company when we move to multi-vendor system - _, err = walletSvc.CreateCustomerWallet(c.Context(), newUser.ID, 0) - - if err != nil { - logger.Error("CreateCustomerWallet failed", "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create customer wallet for user", err, nil) - } - - return response.WriteJSON(c, fiber.StatusOK, "Registration successful", nil, nil) +func (h *Handler) RegisterUser(c *fiber.Ctx) error { + 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"` + Otp string `json:"otp" example:"123456"` + ReferalCode string `json:"referal_code" example:"ABC123"` } -} -type ResetCodeReq struct { - Email string `json:"email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number" example:"1234567890"` + var req RegisterUserReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse RegisterUser request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + user := domain.RegisterUserReq{ + FirstName: req.FirstName, + LastName: req.LastName, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Password: req.Password, + Otp: req.Otp, + ReferralCode: req.ReferalCode, + OtpMedium: domain.OtpMediumEmail, + } + medium, err := getMedium(req.Email, req.PhoneNumber) + if err != nil { + h.logger.Error("RegisterUser failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + user.OtpMedium = medium + + newUser, err := h.userSvc.RegisterUser(c.Context(), user) + if err != nil { + if errors.Is(err, domain.ErrOtpAlreadyUsed) { + return response.WriteJSON(c, fiber.StatusBadRequest, "Otp already used", nil, nil) + } + if errors.Is(err, domain.ErrOtpExpired) { + return response.WriteJSON(c, fiber.StatusBadRequest, "Otp expired", nil, nil) + } + if errors.Is(err, domain.ErrInvalidOtp) { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid otp", nil, nil) + } + if errors.Is(err, domain.ErrOtpNotFound) { + return response.WriteJSON(c, fiber.StatusBadRequest, "User already exist", nil, nil) + } + h.logger.Error("RegisterUser failed", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Internal server error", + }) + } + + _, err = h.walletSvc.CreateWallet(c.Context(), domain.CreateWallet{ + UserID: newUser.ID, + IsWithdraw: true, + IsBettable: true, + }) + if err != nil { + h.logger.Error("Failed to create wallet for user", "userID", newUser.ID, "error", err) + 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) } // SendResetCode godoc @@ -219,46 +203,40 @@ type ResetCodeReq struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/sendResetCode [post] -func SendResetCode(logger *slog.Logger, userSvc *user.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - var req ResetCodeReq - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - var sentTo string - var medium domain.OtpMedium - if req.Email != "" { - sentTo = req.Email - medium = domain.OtpMediumEmail - } - if req.PhoneNumber != "" { - sentTo = req.PhoneNumber - medium = domain.OtpMediumSms - } - if err := userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil { - logger.Error("SendResetCode failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Internal server error", - }) - } - return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) - +func (h *Handler) SendResetCode(c *fiber.Ctx) error { + type ResetCodeReq struct { + Email string `json:"email" validate:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` } -} -type ResetPasswordReq struct { - Email string - PhoneNumber string - Password string - Otp string + var req ResetCodeReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse SendResetCode request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + var sentTo string + var medium domain.OtpMedium + if req.Email != "" { + sentTo = req.Email + medium = domain.OtpMediumEmail + } else if req.PhoneNumber != "" { + sentTo = req.PhoneNumber + medium = domain.OtpMediumSms + } else { + return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") + } + + if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil { + h.logger.Error("Failed to send reset code", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code") + } + + return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) } // ResetPassword godoc @@ -272,58 +250,44 @@ type ResetPasswordReq struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/resetPassword [post] -func ResetPassword(logger *slog.Logger, userSvc *user.Service, - validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - var req ResetPasswordReq - if err := c.BodyParser(&req); err != nil { - logger.Error("ResetPassword failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - valErrs, ok := validator.Validate(c, req) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - user := domain.ResetPasswordReq{ - Email: req.Email, - PhoneNumber: req.PhoneNumber, - Password: req.Password, - Otp: req.Otp, - } - 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) +func (h *Handler) ResetPassword(c *fiber.Ctx) error { + type ResetPasswordReq struct { + 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"` } -} -type UserProfileRes struct { - ID int64 `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - Role domain.Role `json:"role"` - EmailVerified bool `json:"email_verified"` - PhoneVerified bool `json:"phone_verified"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - SuspendedAt time.Time `json:"suspended_at"` - Suspended bool `json:"suspended"` + var req ResetPasswordReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse ResetPassword request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + 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, + PhoneNumber: req.PhoneNumber, + Password: req.Password, + Otp: req.Otp, + OtpMedium: medium, + } + + 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 @@ -337,34 +301,52 @@ type UserProfileRes struct { // @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 { - logger.Error("GetUserProfile failed", "error", err) - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Internal server error", - }) - } - - res := UserProfileRes{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email, - PhoneNumber: user.PhoneNumber, - Role: user.Role, - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - SuspendedAt: user.SuspendedAt, - Suspended: user.Suspended, - } - return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil) +func (h *Handler) UserProfile(c *fiber.Ctx) error { + type UserProfileRes struct { + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + Role domain.Role `json:"role"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + SuspendedAt time.Time `json:"suspended_at"` + Suspended bool `json:"suspended"` } + + 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 profile", "userID", userID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile") + } + + res := UserProfileRes{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + SuspendedAt: user.SuspendedAt, + Suspended: user.Suspended, + } + return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil) } + +// Helper function (unchanged) func getMedium(email, phoneNumber string) (domain.OtpMedium, error) { if email != "" { return domain.OtpMediumEmail, nil diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index 3f6cb67..4a88cf1 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -1,28 +1,14 @@ package handlers import ( - "log/slog" "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" - customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "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 // @Summary Get wallet by ID // @Description Retrieve wallet details by wallet ID @@ -34,36 +20,43 @@ type WalletRes struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /wallet/{id} [get] -func GetWalletByID(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - walletID := c.Params("id") - - id, err := strconv.ParseInt(walletID, 10, 64) - - if err != nil { - logger.Error("Invalid wallet ID", "walletID", walletID, "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) - } - - wallet, err := walletSvc.GetWalletByID(c.Context(), id) - if err != nil { - logger.Error("Failed to get wallet by ID", "walletID", id, "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallet", err, nil) - } - - res := WalletRes{ - ID: wallet.ID, - Balance: wallet.Balance.Float64(), - IsWithdraw: wallet.IsWithdraw, - IsBettable: wallet.IsBettable, - IsActive: wallet.IsActive, - UserID: wallet.UserID, - UpdatedAt: wallet.UpdatedAt, - CreatedAt: wallet.CreatedAt, - } - - return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) +func (h *Handler) GetWalletByID(c *fiber.Ctx) error { + 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"` } + + walletID := c.Params("id") + id, err := strconv.ParseInt(walletID, 10, 64) + if err != nil { + h.logger.Error("Invalid wallet ID", "walletID", walletID, "error", err) + 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{ + ID: wallet.ID, + Balance: wallet.Balance.Float64(), + IsWithdraw: wallet.IsWithdraw, + IsBettable: wallet.IsBettable, + IsActive: wallet.IsActive, + UserID: wallet.UserID, + UpdatedAt: wallet.UpdatedAt, + CreatedAt: wallet.CreatedAt, + } + + return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) } // GetAllWallets godoc @@ -76,35 +69,39 @@ func GetWalletByID(logger *slog.Logger, walletSvc *wallet.Service, validator *cu // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /wallet [get] -func GetAllWallets(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - wallets, err := walletSvc.GetAllWallets(c.Context()) - - if err != nil { - logger.Error("Failed to get wallets", "error", err) - } - - var res []WalletRes = make([]WalletRes, len(wallets)) - - for _, wallet := range wallets { - res = append(res, WalletRes{ - ID: wallet.ID, - Balance: wallet.Balance.Float64(), - IsWithdraw: wallet.IsWithdraw, - IsBettable: wallet.IsBettable, - IsActive: wallet.IsActive, - UserID: wallet.UserID, - UpdatedAt: wallet.UpdatedAt, - CreatedAt: wallet.CreatedAt, - }) - } - - return response.WriteJSON(c, fiber.StatusOK, "All Wallets retrieved", res, nil) +func (h *Handler) GetAllWallets(c *fiber.Ctx) error { + 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"` } -} -type UpdateWalletActiveReq struct { - IsActive bool + 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") + } + + res := make([]WalletRes, len(wallets)) + for i, wallet := range wallets { + res[i] = WalletRes{ + ID: wallet.ID, + Balance: wallet.Balance.Float64(), + IsWithdraw: wallet.IsWithdraw, + IsBettable: wallet.IsBettable, + IsActive: wallet.IsActive, + UserID: wallet.UserID, + UpdatedAt: wallet.UpdatedAt, + CreatedAt: wallet.CreatedAt, + } + } + + return response.WriteJSON(c, fiber.StatusOK, "All wallets retrieved successfully", res, nil) } // UpdateWalletActive godoc @@ -119,46 +116,35 @@ type UpdateWalletActiveReq struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /wallet/{id} [patch] -func UpdateWalletActive(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - walletID := c.Params("id") - id, err := strconv.ParseInt(walletID, 10, 64) - - if err != nil { - logger.Error("Invalid bet ID", "walletID", walletID, "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) - } - - var req UpdateWalletActiveReq - if err := c.BodyParser(&req); err != nil { - logger.Error("UpdateWalletActiveReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) - } - - err = walletSvc.UpdateWalletActive(c.Context(), id, req.IsActive) - - if err != nil { - logger.Error("Failed to update", "walletID", id, "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update wallet", err, nil) - } - - return response.WriteJSON(c, fiber.StatusOK, "Wallet successfully updated", nil, nil) +func (h *Handler) UpdateWalletActive(c *fiber.Ctx) error { + type UpdateWalletActiveReq struct { + IsActive bool `json:"is_active" validate:"required" example:"true"` } -} -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"` + walletID := c.Params("id") + id, err := strconv.ParseInt(walletID, 10, 64) + if err != nil { + h.logger.Error("Invalid wallet ID", "walletID", walletID, "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid wallet ID") + } + + var req UpdateWalletActiveReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse UpdateWalletActive request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + 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 { + h.logger.Error("Failed to update wallet active status", "walletID", id, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update wallet") + } + + return response.WriteJSON(c, fiber.StatusOK, "Wallet successfully updated", nil, nil) } // GetCustomerWallet godoc @@ -173,40 +159,63 @@ type CustomerWalletRes struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/wallet [get] -func GetCustomerWallet(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - - userId := c.Locals("user_id").(int64) - role := string(c.Locals("role").(domain.Role)) - - companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64) - if err != nil { - return c.Status(fiber.StatusBadRequest).SendString("Invalid company_id") - } - logger.Info("Company ID: " + strconv.FormatInt(companyID, 10)) - - if role != string(domain.RoleCustomer) { - logger.Error("Unauthorized access", "userId", userId, "role", role) - return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) - } - wallet, err := walletSvc.GetCustomerWallet(c.Context(), userId, companyID) - if err != nil { - logger.Error("Failed to get customer wallet", "userId", userId, "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallet", err, nil) - } - res := CustomerWalletRes{ - ID: wallet.ID, - RegularID: wallet.RegularID, - RegularBalance: wallet.RegularBalance.Float64(), - StaticID: wallet.StaticID, - StaticBalance: wallet.StaticBalance.Float64(), - CustomerID: wallet.CustomerID, - CompanyID: wallet.CompanyID, - RegularUpdatedAt: wallet.RegularUpdatedAt, - StaticUpdatedAt: wallet.StaticUpdatedAt, - CreatedAt: wallet.CreatedAt, - } - return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) - +func (h *Handler) GetCustomerWallet(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, 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") + } + + 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) + if err != nil { + h.logger.Error("Invalid company_id header", "value", c.Get("company_id"), "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid company_id") + } + + h.logger.Info("Fetching customer wallet", "userID", userID, "companyID", companyID) + + wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID, companyID) + if err != nil { + h.logger.Error("Failed to get customer wallet", "userID", userID, "companyID", companyID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve wallet") + } + + res := CustomerWalletRes{ + ID: wallet.ID, + RegularID: wallet.RegularID, + RegularBalance: wallet.RegularBalance.Float64(), + StaticID: wallet.StaticID, + StaticBalance: wallet.StaticBalance.Float64(), + CustomerID: wallet.CustomerID, + CompanyID: wallet.CompanyID, + RegularUpdatedAt: wallet.RegularUpdatedAt, + StaticUpdatedAt: wallet.StaticUpdatedAt, + CreatedAt: wallet.CreatedAt, + } + + return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 204b3c5..b393abd 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -12,68 +12,97 @@ import ( ) 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)) - a.fiber.Post("/auth/refresh", a.authMiddleware, handlers.RefreshToken(a.logger, a.authSvc, a.validator, a.JwtConfig)) - a.fiber.Post("/auth/logout", a.authMiddleware, handlers.LogOutCustomer(a.logger, a.authSvc, a.validator)) + // Auth Routes + a.fiber.Post("/auth/login", h.LoginCustomer) + 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 { - userId := c.Locals("user_id").(int64) - role := string(c.Locals("role").(domain.Role)) - refreshToken := (c.Locals("refresh_token").(string)) + userID, ok := c.Locals("user_id").(int64) + if !ok { + 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) 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)) - fmt.Printf("User ID: %d\n", userId) - a.logger.Info("Role: " + role) + + a.logger.Info("User ID: " + strconv.FormatInt(userID, 10)) + fmt.Printf("User ID: %d\n", userID) + a.logger.Info("Role: " + string(role)) a.logger.Info("Refresh Token: " + refreshToken) a.logger.Info("Company ID: " + strconv.FormatInt(companyID, 10)) 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 a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) - // Ticket - a.fiber.Post("/ticket", handlers.CreateTicket(a.logger, a.ticketSvc, a.validator)) - a.fiber.Get("/ticket", handlers.GetAllTickets(a.logger, a.ticketSvc, a.validator)) - a.fiber.Get("/ticket/:id", handlers.GetTicketByID(a.logger, a.ticketSvc, a.validator)) + // Ticket Routes + a.fiber.Post("/ticket", h.CreateTicket) + a.fiber.Get("/ticket", h.GetAllTickets) + a.fiber.Get("/ticket/:id", h.GetTicketByID) - // Bet - a.fiber.Post("/bet", handlers.CreateBet(a.logger, a.betSvc, a.validator)) - a.fiber.Get("/bet", handlers.GetAllBet(a.logger, a.betSvc, a.validator)) - a.fiber.Get("/bet/:id", handlers.GetBetByID(a.logger, a.betSvc, a.validator)) - a.fiber.Patch("/bet/:id", handlers.UpdateCashOut(a.logger, a.betSvc, a.validator)) - a.fiber.Delete("/bet/:id", handlers.DeleteBet(a.logger, a.betSvc, a.validator)) + // Bet Routes + a.fiber.Post("/bet", h.CreateBet) + a.fiber.Get("/bet", h.GetAllBet) + a.fiber.Get("/bet/:id", h.GetBetByID) + a.fiber.Patch("/bet/:id", h.UpdateCashOut) + a.fiber.Delete("/bet/:id", h.DeleteBet) - // Wallet - a.fiber.Get("/wallet", handlers.GetAllWallets(a.logger, a.walletSvc, a.validator)) - a.fiber.Get("/wallet/:id", handlers.GetWalletByID(a.logger, a.walletSvc, a.validator)) - a.fiber.Put("/wallet/:id", handlers.UpdateWalletActive(a.logger, a.walletSvc, a.validator)) + // Transaction Routes + a.fiber.Post("/transaction", h.CreateTransaction) + a.fiber.Get("/transaction", h.GetAllTransactions) + a.fiber.Get("/transaction/:id", h.GetTransactionByID) + a.fiber.Patch("/transaction/:id", h.UpdateTransactionVerified) - // Transactions /transactions - a.fiber.Post("/transaction", handlers.CreateTransaction(a.logger, a.transactionSvc, a.validator)) - a.fiber.Get("/transaction", handlers.GetAllTransactions(a.logger, a.transactionSvc, a.validator)) - a.fiber.Get("/transaction/:id", handlers.GetTransactionByID(a.logger, a.transactionSvc, a.validator)) - 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) + // Notification Routes + a.fiber.Get("/notifications/ws/connect/:recipientID", h.ConnectSocket) + a.fiber.Post("/notifications/mark-as-read", h.MarkNotificationAsRead) + a.fiber.Post("/notifications/create", h.CreateAndSendNotification) } -///user/profile get // @Router /user/resetPassword [post] // @Router /user/sendResetCode [post] // @Router /user/register [post]