feat: referal completed

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

View File

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

View File

@ -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_at TIMESTAMPTZ NULL, -- this can be NULL if the user is not suspended
suspended BOOLEAN NOT NULL DEFAULT FALSE, 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 ( CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
@ -28,13 +28,13 @@ CREATE TABLE refresh_tokens (
CREATE TABLE otps ( CREATE TABLE otps (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
sent_to VARCHAR(255) NOT NULL, sent_to VARCHAR(255) NOT NULL,
medium VARCHAR(50) NOT NULL, medium VARCHAR(50) NOT NULL,
otp_for VARCHAR(50) NOT NULL, otp_for VARCHAR(50) NOT NULL,
otp VARCHAR(10) NOT NULL, otp VARCHAR(10) NOT NULL,
used BOOLEAN NOT NULL DEFAULT FALSE, used BOOLEAN NOT NULL DEFAULT FALSE,
used_at TIMESTAMPTZ, used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ NOT NULL expires_at TIMESTAMPTZ NOT NULL
); );
CREATE TABLE IF NOT EXISTS bets ( CREATE TABLE IF NOT EXISTS bets (
@ -132,23 +132,20 @@ CREATE TABLE IF NOT EXISTS transactions (
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO users ( INSERT INTO users (
first_name, last_name, email, phone_number, password, role, first_name, last_name, email, phone_number, password, role,
email_verified, phone_verified, created_at, updated_at, email_verified, phone_verified, created_at, updated_at,
suspended_at, suspended suspended_at, suspended
) VALUES ( ) VALUES (
'John', 'John',
'Doe', 'Doe',
'john.doe@example.com', 'john.doe@example.com',
NULL, NULL,
crypt('password123', gen_salt('bf'))::bytea, crypt('password123', gen_salt('bf'))::bytea,
'customer', 'customer',
TRUE, TRUE,
FALSE, FALSE,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
NULL, NULL,
FALSE FALSE
); );

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -34,10 +34,9 @@ type RegisterUserReq struct {
PhoneNumber string PhoneNumber string
Password string Password string
//Role string //Role string
Otp string Otp string
ReferalCode string ReferralCode string `json:"referral_code"`
// OtpMedium OtpMedium
OtpMedium OtpMedium
} }
type ResetPasswordReq struct { type ResetPasswordReq struct {
Email string Email string

View File

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

View File

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

View File

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

View File

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

View File

@ -2,26 +2,13 @@ package handlers
import ( import (
"errors" "errors"
"log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type loginCustomerReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
}
type loginCustomerRes struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// LoginCustomer godoc // LoginCustomer godoc
// @Summary Login customer // @Summary Login customer
// @Description Login customer // @Description Login customer
@ -34,49 +21,50 @@ type loginCustomerRes struct {
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /auth/login [post] // @Router /auth/login [post]
func LoginCustomer( func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
logger *slog.Logger, authSvc *authentication.Service, type loginCustomerReq struct {
validator *customvalidator.CustomValidator, JwtConfig jwtutil.JwtConfig) fiber.Handler { Email string `json:"email" validate:"email" example:"john.doe@example.com"`
return func(c *fiber.Ctx) error { PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
var req loginCustomerReq Password string `json:"password" validate:"required" example:"password123"`
if err := c.BodyParser(&req); err != nil { }
logger.Error("Login failed", "error", err) type loginCustomerRes struct {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) AccessToken string `json:"access_token"`
} RefreshToken string `json:"refresh_token"`
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)
} }
}
type refreshToken struct { var req loginCustomerReq
AccessToken string `json:"access_token"` if err := c.BodyParser(&req); err != nil {
RefreshToken string `json:"refresh_token"` 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 // RefreshToken godoc
@ -91,50 +79,52 @@ type refreshToken struct {
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /auth/refresh [post] // @Router /auth/refresh [post]
func RefreshToken(logger *slog.Logger, authSvc *authentication.Service, func (h *Handler) RefreshToken(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator, JwtConfig jwtutil.JwtConfig) fiber.Handler { type refreshTokenReq struct {
return func(c *fiber.Ctx) error { AccessToken string `json:"access_token" validate:"required" example:"<jwt-token>"`
var req refreshToken RefreshToken string `json:"refresh_token" validate:"required" example:"<refresh-token>"`
if err := c.BodyParser(&req); err != nil { }
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) type loginCustomerRes struct {
} AccessToken string `json:"access_token"`
valErrs, ok := validator.Validate(c, req) RefreshToken string `json:"refresh_token"`
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)
} }
}
type logoutReq struct { var req refreshTokenReq
RefreshToken string `json:"refresh_token"` 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 // LogOutCustomer godoc
@ -149,34 +139,34 @@ type logoutReq struct {
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /auth/logout [post] // @Router /auth/logout [post]
func LogOutCustomer( func (h *Handler) LogOutCustomer(c *fiber.Ctx) error {
logger *slog.Logger, authSvc *authentication.Service, type logoutReq struct {
validator *customvalidator.CustomValidator) fiber.Handler { RefreshToken string `json:"refresh_token" validate:"required" example:"<refresh-token>"`
return func(c *fiber.Ctx) error {
var req logoutReq
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)
} }
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)
} }

View File

@ -1,26 +1,13 @@
package handlers package handlers
import ( import (
"log/slog"
"strconv" "strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type CreateBetReq struct {
Outcomes []int64 `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status domain.BetStatus `json:"status" example:"1"`
FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
}
type BetRes struct { type BetRes struct {
ID int64 `json:"id" example:"1"` ID int64 `json:"id" example:"1"`
Outcomes []domain.Outcome `json:"outcomes"` Outcomes []domain.Outcome `json:"outcomes"`
@ -34,20 +21,6 @@ type BetRes struct {
IsShopBet bool `json:"is_shop_bet" example:"false"` IsShopBet bool `json:"is_shop_bet" example:"false"`
} }
func convertBet(bet domain.Bet) BetRes {
return BetRes{
ID: bet.ID,
Outcomes: bet.Outcomes,
Amount: bet.Amount.Float64(),
TotalOdds: bet.TotalOdds,
Status: bet.Status,
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: bet.BranchID.Value,
UserID: bet.UserID.Value,
}
}
// CreateBet godoc // CreateBet godoc
// @Summary Create a bet // @Summary Create a bet
// @Description Creates a bet // @Description Creates a bet
@ -59,62 +32,67 @@ func convertBet(bet domain.Bet) BetRes {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [post] // @Router /bet [post]
func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) CreateBet(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type CreateBetReq struct {
Outcomes []int64 `json:"outcomes" validate:"required" example:"[1, 2, 3]"`
// TODO: Check the token, and find the role and get the branch id from there Amount float32 `json:"amount" validate:"required" example:"100.0"`
TotalOdds float32 `json:"total_odds" validate:"required" example:"4.22"`
// TODO Reduce amount from the branch wallet Status domain.BetStatus `json:"status" validate:"required" example:"1"`
FullName string `json:"full_name" example:"John"`
var isShopBet bool = true PhoneNumber string `json:"phone_number" validate:"required" example:"1234567890"`
var branchID int64 = 1 IsShopBet bool `json:"is_shop_bet" example:"false"`
var userID int64
var req CreateBetReq
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)
} }
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 // GetAllBet godoc
@ -127,22 +105,19 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [get] // @Router /bet [get]
func GetAllBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetAllBet(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { bets, err := h.betSvc.GetAllBets(c.Context())
bets, err := betSvc.GetAllBets(c.Context()) if err != nil {
h.logger.Error("Failed to get bets", "error", err)
if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets")
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)
} }
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 // GetBetByID godoc
@ -156,32 +131,35 @@ func GetAllBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [get] // @Router /bet/{id} [get]
func GetBetByID(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetBetByID(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type BetRes struct {
betID := c.Params("id") ID int64 `json:"id" example:"1"`
id, err := strconv.ParseInt(betID, 10, 64) Outcomes []domain.Outcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
if err != nil { TotalOdds float32 `json:"total_odds" example:"4.22"`
logger.Error("Invalid bet ID", "betID", betID, "error", err) Status domain.BetStatus `json:"status" example:"1"`
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) FullName string `json:"full_name" example:"John"`
} PhoneNumber string `json:"phone_number" example:"1234567890"`
BranchID int64 `json:"branch_id" example:"2"`
bet, err := betSvc.GetBetByID(c.Context(), id) UserID int64 `json:"user_id" example:"2"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
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)
} }
}
type UpdateCashOutReq struct { betID := c.Params("id")
CashedOut bool 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 // UpdateCashOut godoc
@ -196,40 +174,35 @@ type UpdateCashOutReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [patch] // @Router /bet/{id} [patch]
func UpdateCashOut(logger *slog.Logger, betSvc *bet.Service, func (h *Handler) UpdateCashOut(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type UpdateCashOutReq struct {
return func(c *fiber.Ctx) error { CashedOut bool `json:"cashed_out" validate:"required" example:"true"`
betID := c.Params("id")
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)
} }
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 // DeleteBet godoc
@ -243,24 +216,34 @@ func UpdateCashOut(logger *slog.Logger, betSvc *bet.Service,
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [delete] // @Router /bet/{id} [delete]
func DeleteBet(logger *slog.Logger, betSvc *bet.Service, func (h *Handler) DeleteBet(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { betID := c.Params("id")
return func(c *fiber.Ctx) error { id, err := strconv.ParseInt(betID, 10, 64)
betID := c.Params("id") if err != nil {
id, err := strconv.ParseInt(betID, 10, 64) h.logger.Error("Invalid bet ID", "betID", betID, "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID")
}
if err != nil { err = h.betSvc.DeleteBet(c.Context(), id)
logger.Error("Invalid bet ID", "betID", betID, "error", err) if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", 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 { func convertBet(bet domain.Bet) BetRes {
logger.Error("Failed to delete by ID", "betID", id, "error", err) return BetRes{
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to delete bet", err, nil) ID: bet.ID,
} Outcomes: bet.Outcomes,
Amount: bet.Amount.Float64(),
return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil) TotalOdds: bet.TotalOdds,
Status: bet.Status,
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: bet.BranchID.Value,
UserID: bet.UserID.Value,
IsShopBet: bet.IsShopBet,
} }
} }

View File

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

View File

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

View File

@ -1,25 +1,13 @@
package handlers package handlers
import ( import (
"log/slog"
"strconv" "strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type CreateTicketReq struct {
Outcomes []int64 `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
}
type CreateTicketRes struct {
FastCode int64 `json:"fast_code" example:"1234"`
}
// CreateTicket godoc // CreateTicket godoc
// @Summary Create a temporary ticket // @Summary Create a temporary ticket
// @Description Creates a temporary ticket // @Description Creates a temporary ticket
@ -31,48 +19,43 @@ type CreateTicketRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /ticket [post] // @Router /ticket [post]
func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, func (h *Handler) CreateTicket(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type CreateTicketReq struct {
return func(c *fiber.Ctx) error { Outcomes []int64 `json:"outcomes" validate:"required" example:"[1, 2, 3]"`
var req CreateTicketReq Amount float32 `json:"amount" validate:"required" example:"100.0"`
if err := c.BodyParser(&req); err != nil { TotalOdds float32 `json:"total_odds" validate:"required" example:"4.22"`
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)
} }
}
type TicketRes struct { type CreateTicketRes struct {
ID int64 `json:"id" example:"1"` FastCode int64 `json:"fast_code" example:"1234"`
Outcomes []domain.Outcome `json:"outcomes"` }
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"` 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 // GetTicketByID godoc
@ -86,34 +69,34 @@ type TicketRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /ticket/{id} [get] // @Router /ticket/{id} [get]
func GetTicketByID(logger *slog.Logger, ticketSvc *ticket.Service, func (h *Handler) GetTicketByID(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type TicketRes struct {
return func(c *fiber.Ctx) error { ID int64 `json:"id" example:"1"`
ticketID := c.Params("id") Outcomes []domain.Outcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
id, err := strconv.ParseInt(ticketID, 10, 64) TotalOdds float32 `json:"total_odds" example:"4.22"`
if err != nil {
logger.Error("Invalid ticket ID", "ticketID", ticketID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid ticket ID", err, nil)
}
ticket, err := ticketSvc.GetTicketByID(c.Context(), id)
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)
} }
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 // GetAllTickets godoc
@ -126,28 +109,29 @@ func GetTicketByID(logger *slog.Logger, ticketSvc *ticket.Service,
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /ticket [get] // @Router /ticket [get]
func GetAllTickets(logger *slog.Logger, ticketSvc *ticket.Service, func (h *Handler) GetAllTickets(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type TicketRes struct {
return func(c *fiber.Ctx) error { ID int64 `json:"id" example:"1"`
tickets, err := ticketSvc.GetAllTickets(c.Context()) Outcomes []domain.Outcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
if err != nil { TotalOdds float32 `json:"total_odds" example:"4.22"`
logger.Error("Failed to get tickets", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve tickets", err, nil)
}
var res []TicketRes = make([]TicketRes, len(tickets))
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)
} }
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)
} }

View File

@ -1,13 +1,10 @@
package handlers package handlers
import ( import (
"log/slog"
"strconv" "strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -75,46 +72,62 @@ func convertTransaction(transaction domain.Transaction) TransactionRes {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /transaction [post] // @Router /transaction [post]
func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { // Update transaction handler to include deposit bonus
return func(c *fiber.Ctx) error { func (h *Handler) CreateTransaction(c *fiber.Ctx) error {
var req CreateTransactionReq type CreateTransactionReq struct {
if err := c.BodyParser(&req); err != nil { Amount float32 `json:"amount" validate:"required" example:"100.0"`
logger.Error("CreateTransactionReq failed", "error", err) BranchID int64 `json:"branch_id" example:"1"`
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ CashierID int64 `json:"cashier_id" example:"1"`
"error": "Invalid request", 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"`
valErrs, ok := validator.Validate(c, req) BankCode string `json:"bank_code"`
if !ok { BeneficiaryName string `json:"beneficiary_name"`
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) AccountName string `json:"account_name"`
return nil AccountNumber string `json:"account_number"`
} ReferenceNumber string `json:"reference_number"`
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)
} }
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 // GetAllTransactions godoc
@ -127,22 +140,19 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service,
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /transaction [get] // @Router /transaction [get]
func GetAllTransactions(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetAllTransactions(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { transactions, err := h.transactionSvc.GetAllTransactions(c.Context())
transactions, err := transactionSvc.GetAllTransactions(c.Context()) if err != nil {
h.logger.Error("Failed to get transactions", "error", err)
if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve transactions")
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)
} }
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 // GetTransactionByID godoc
@ -156,77 +166,63 @@ func GetAllTransactions(logger *slog.Logger, transactionSvc *transaction.Service
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /transaction/{id} [get] // @Router /transaction/{id} [get]
func GetTransactionByID(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetTransactionByID(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { transactionID := c.Params("id")
transactionID := c.Params("id") id, err := strconv.ParseInt(transactionID, 10, 64)
id, err := strconv.ParseInt(transactionID, 10, 64) if err != nil {
h.logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err)
if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid transaction ID")
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)
} }
}
type UpdateTransactionVerifiedReq struct { transaction, err := h.transactionSvc.GetTransactionByID(c.Context(), id)
Verified bool 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 // UpdateTransactionVerified godoc
// @Summary Updates the cashed out field // @Summary Updates the verified field of a transaction
// @Description Updates the cashed out field // @Description Updates the verified status of a transaction
// @Tags transaction // @Tags transaction
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path int true "Transaction ID" // @Param id path int true "Transaction ID"
// @Param updateCashOut body UpdateTransactionVerifiedReq true "Updates Transaction Verification" // @Param updateVerified body UpdateTransactionVerifiedReq true "Updates Transaction Verification"
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /transaction/{id} [patch] // @Router /transaction/{id} [patch]
func UpdateTransactionVerified(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) UpdateTransactionVerified(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type UpdateTransactionVerifiedReq struct {
transactionID := c.Params("id") Verified bool `json:"verified" validate:"required" example:"true"`
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)
} }
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)
} }

View File

@ -2,27 +2,14 @@ package handlers
import ( import (
"errors" "errors"
"log/slog"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type CheckPhoneEmailExistReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
type CheckPhoneEmailExistRes struct {
EmailExist bool `json:"email_exist"`
PhoneNumberExist bool `json:"phone_number_exist"`
}
// CheckPhoneEmailExist godoc // CheckPhoneEmailExist godoc
// @Summary Check if phone number or email exist // @Summary Check if phone number or email exist
// @Description Check if phone number or email exist // @Description Check if phone number or email exist
@ -34,39 +21,37 @@ type CheckPhoneEmailExistRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/checkPhoneEmailExist [post] // @Router /user/checkPhoneEmailExist [post]
func CheckPhoneEmailExist(logger *slog.Logger, userSvc *user.Service, func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type CheckPhoneEmailExistReq struct {
return func(c *fiber.Ctx) error { Email string `json:"email" validate:"email" example:"john.doe@example.com"`
var req CheckPhoneEmailExistReq PhoneNumber string `json:"phone_number" validate:"required" example:"1234567890"`
if err := c.BodyParser(&req); err != nil { }
logger.Error("CheckPhoneEmailExist failed", "error", err) type CheckPhoneEmailExistRes struct {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ EmailExist bool `json:"email_exist"`
"error": "Invalid request", PhoneNumberExist bool `json:"phone_number_exist"`
})
}
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)
} }
}
type RegisterCodeReq struct { var req CheckPhoneEmailExistReq
Email string `json:"email" example:"john.doe@example.com"` if err := c.BodyParser(&req); err != nil {
PhoneNumber string `json:"phone_number" example:"1234567890"` 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 // SendRegisterCode godoc
@ -80,51 +65,40 @@ type RegisterCodeReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/sendRegisterCode [post] // @Router /user/sendRegisterCode [post]
func SendRegisterCode(logger *slog.Logger, userSvc *user.Service, func (h *Handler) SendRegisterCode(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type RegisterCodeReq struct {
return func(c *fiber.Ctx) error { Email string `json:"email" validate:"email" example:"john.doe@example.com"`
var req RegisterCodeReq PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
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)
} }
}
type RegisterUserReq struct { var req RegisterCodeReq
FirstName string `json:"first_name" example:"John"` if err := c.BodyParser(&req); err != nil {
LastName string `json:"last_name" example:"Doe"` h.logger.Error("Failed to parse SendRegisterCode request", "error", err)
Email string `json:"email" example:"john.doe@example.com"` return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
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"`
//
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 // RegisterUser godoc
@ -138,74 +112,84 @@ type RegisterUserReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/register [post] // @Router /user/register [post]
func RegisterUser(logger *slog.Logger, userSvc *user.Service, walletSvc *wallet.Service, func (h *Handler) RegisterUser(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type RegisterUserReq struct {
return func(c *fiber.Ctx) error { FirstName string `json:"first_name" example:"John"`
var req RegisterUserReq LastName string `json:"last_name" example:"Doe"`
if err := c.BodyParser(&req); err != nil { Email string `json:"email" example:"john.doe@example.com"`
logger.Error("RegisterUser failed", "error", err) PhoneNumber string `json:"phone_number" example:"1234567890"`
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ Password string `json:"password" example:"password123"`
"error": "Invalid request", Otp string `json:"otp" example:"123456"`
}) ReferalCode string `json:"referal_code" example:"ABC123"`
}
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)
} }
}
type ResetCodeReq struct { var req RegisterUserReq
Email string `json:"email" example:"john.doe@example.com"` if err := c.BodyParser(&req); err != nil {
PhoneNumber string `json:"phone_number" example:"1234567890"` 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 // SendResetCode godoc
@ -219,46 +203,40 @@ type ResetCodeReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/sendResetCode [post] // @Router /user/sendResetCode [post]
func SendResetCode(logger *slog.Logger, userSvc *user.Service, func (h *Handler) SendResetCode(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type ResetCodeReq struct {
return func(c *fiber.Ctx) error { Email string `json:"email" validate:"email" example:"john.doe@example.com"`
var req ResetCodeReq PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
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)
} }
}
type ResetPasswordReq struct { var req ResetCodeReq
Email string if err := c.BodyParser(&req); err != nil {
PhoneNumber string h.logger.Error("Failed to parse SendResetCode request", "error", err)
Password string return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
Otp string }
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 // ResetPassword godoc
@ -272,58 +250,44 @@ type ResetPasswordReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/resetPassword [post] // @Router /user/resetPassword [post]
func ResetPassword(logger *slog.Logger, userSvc *user.Service, func (h *Handler) ResetPassword(c *fiber.Ctx) error {
validator *customvalidator.CustomValidator) fiber.Handler { type ResetPasswordReq struct {
return func(c *fiber.Ctx) error { Email string `json:"email" validate:"email" example:"john.doe@example.com"`
var req ResetPasswordReq PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
if err := c.BodyParser(&req); err != nil { Password string `json:"password" validate:"required,min=8" example:"newpassword123"`
logger.Error("ResetPassword failed", "error", err) Otp string `json:"otp" validate:"required" example:"123456"`
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)
} }
}
type UserProfileRes struct { var req ResetPasswordReq
ID int64 `json:"id"` if err := c.BodyParser(&req); err != nil {
FirstName string `json:"first_name"` h.logger.Error("Failed to parse ResetPassword request", "error", err)
LastName string `json:"last_name"` return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
Email string `json:"email"` }
PhoneNumber string `json:"phone_number"`
Role domain.Role `json:"role"` if valErrs, ok := h.validator.Validate(c, req); !ok {
EmailVerified bool `json:"email_verified"` return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
PhoneVerified bool `json:"phone_verified"` }
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` medium, err := getMedium(req.Email, req.PhoneNumber)
SuspendedAt time.Time `json:"suspended_at"` if err != nil {
Suspended bool `json:"suspended"` 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 // UserProfile godoc
@ -337,34 +301,52 @@ type UserProfileRes struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Security Bearer // @Security Bearer
// @Router /user/profile [get] // @Router /user/profile [get]
func UserProfile(logger *slog.Logger, userSvc *user.Service) fiber.Handler { func (h *Handler) UserProfile(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type UserProfileRes struct {
userId := c.Locals("user_id").(int64) ID int64 `json:"id"`
user, err := userSvc.GetUserByID(c.Context(), userId) FirstName string `json:"first_name"`
if err != nil { LastName string `json:"last_name"`
logger.Error("GetUserProfile failed", "error", err) Email string `json:"email"`
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ PhoneNumber string `json:"phone_number"`
"error": "Internal server error", Role domain.Role `json:"role"`
}) EmailVerified bool `json:"email_verified"`
} PhoneVerified bool `json:"phone_verified"`
CreatedAt time.Time `json:"created_at"`
res := UserProfileRes{ UpdatedAt time.Time `json:"updated_at"`
ID: user.ID, SuspendedAt time.Time `json:"suspended_at"`
FirstName: user.FirstName, Suspended bool `json:"suspended"`
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)
} }
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) { func getMedium(email, phoneNumber string) (domain.OtpMedium, error) {
if email != "" { if email != "" {
return domain.OtpMediumEmail, nil return domain.OtpMediumEmail, nil

View File

@ -1,28 +1,14 @@
package handlers package handlers
import ( import (
"log/slog"
"strconv" "strconv"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type WalletRes struct {
ID int64 `json:"id" example:"1"`
Balance float32 `json:"amount" example:"100.0"`
IsWithdraw bool `json:"is_withdraw" example:"true"`
IsBettable bool `json:"is_bettable" example:"true"`
IsActive bool `json:"is_active" example:"true"`
UserID int64 `json:"user_id" example:"1"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
}
// GetWalletByID godoc // GetWalletByID godoc
// @Summary Get wallet by ID // @Summary Get wallet by ID
// @Description Retrieve wallet details by wallet ID // @Description Retrieve wallet details by wallet ID
@ -34,36 +20,43 @@ type WalletRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /wallet/{id} [get] // @Router /wallet/{id} [get]
func GetWalletByID(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetWalletByID(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type WalletRes struct {
walletID := c.Params("id") ID int64 `json:"id" example:"1"`
Balance float32 `json:"amount" example:"100.0"`
id, err := strconv.ParseInt(walletID, 10, 64) IsWithdraw bool `json:"is_withdraw" example:"true"`
IsBettable bool `json:"is_bettable" example:"true"`
if err != nil { IsActive bool `json:"is_active" example:"true"`
logger.Error("Invalid wallet ID", "walletID", walletID, "error", err) UserID int64 `json:"user_id" example:"1"`
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) UpdatedAt time.Time `json:"updated_at"`
} CreatedAt time.Time `json:"created_at"`
wallet, err := walletSvc.GetWalletByID(c.Context(), id)
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)
} }
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 // GetAllWallets godoc
@ -76,35 +69,39 @@ func GetWalletByID(logger *slog.Logger, walletSvc *wallet.Service, validator *cu
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /wallet [get] // @Router /wallet [get]
func GetAllWallets(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetAllWallets(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type WalletRes struct {
wallets, err := walletSvc.GetAllWallets(c.Context()) ID int64 `json:"id" example:"1"`
Balance float32 `json:"amount" example:"100.0"`
if err != nil { IsWithdraw bool `json:"is_withdraw" example:"true"`
logger.Error("Failed to get wallets", "error", err) IsBettable bool `json:"is_bettable" example:"true"`
} IsActive bool `json:"is_active" example:"true"`
UserID int64 `json:"user_id" example:"1"`
var res []WalletRes = make([]WalletRes, len(wallets)) UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
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)
} }
}
type UpdateWalletActiveReq struct { wallets, err := h.walletSvc.GetAllWallets(c.Context())
IsActive bool 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 // UpdateWalletActive godoc
@ -119,46 +116,35 @@ type UpdateWalletActiveReq struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /wallet/{id} [patch] // @Router /wallet/{id} [patch]
func UpdateWalletActive(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) UpdateWalletActive(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type UpdateWalletActiveReq struct {
walletID := c.Params("id") IsActive bool `json:"is_active" validate:"required" example:"true"`
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)
} }
}
type CustomerWalletRes struct { walletID := c.Params("id")
ID int64 `json:"id" example:"1"` id, err := strconv.ParseInt(walletID, 10, 64)
RegularID int64 `json:"regular_id" example:"1"` if err != nil {
RegularBalance float32 `json:"regular_balance" example:"100.0"` h.logger.Error("Invalid wallet ID", "walletID", walletID, "error", err)
StaticID int64 `json:"static_id" example:"1"` return fiber.NewError(fiber.StatusBadRequest, "Invalid wallet ID")
StaticBalance float32 `json:"static_balance" example:"100.0"` }
CustomerID int64 `json:"customer_id" example:"1"`
CompanyID int64 `json:"company_id" example:"1"` var req UpdateWalletActiveReq
RegularUpdatedAt time.Time `json:"regular_updated_at"` if err := c.BodyParser(&req); err != nil {
StaticUpdatedAt time.Time `json:"static_updated_at"` h.logger.Error("Failed to parse UpdateWalletActive request", "error", err)
CreatedAt time.Time `json:"created_at"` 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 // GetCustomerWallet godoc
@ -173,40 +159,63 @@ type CustomerWalletRes struct {
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /user/wallet [get] // @Router /user/wallet [get]
func GetCustomerWallet(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error { type CustomerWalletRes struct {
ID int64 `json:"id" example:"1"`
userId := c.Locals("user_id").(int64) RegularID int64 `json:"regular_id" example:"1"`
role := string(c.Locals("role").(domain.Role)) RegularBalance float32 `json:"regular_balance" example:"100.0"`
StaticID int64 `json:"static_id" example:"1"`
companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64) StaticBalance float32 `json:"static_balance" example:"100.0"`
if err != nil { CustomerID int64 `json:"customer_id" example:"1"`
return c.Status(fiber.StatusBadRequest).SendString("Invalid company_id") CompanyID int64 `json:"company_id" example:"1"`
} RegularUpdatedAt time.Time `json:"regular_updated_at"`
logger.Info("Company ID: " + strconv.FormatInt(companyID, 10)) StaticUpdatedAt time.Time `json:"static_updated_at"`
CreatedAt time.Time `json:"created_at"`
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)
} }
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)
} }

View File

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