Merge branch 'main' into ticket-bet

This commit is contained in:
Samuel Tariku 2025-06-17 20:36:12 +03:00
commit 76518fcbd1
32 changed files with 389 additions and 157 deletions

8
.gitignore vendored
View File

@ -1,7 +1,7 @@
bin
coverage.out
# coverage
# .env
# tmp
# build
coverage
.env
tmp
build
*.log

View File

BIN
bin/web

Binary file not shown.

View File

@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS bets (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_shop_bet BOOLEAN NOT NULL,
outcomes_hash TEXT NOT NULL,
UNIQUE(cashout_id),
CHECK (
user_id IS NOT NULL
@ -308,7 +309,7 @@ ALTER TABLE bets
ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id);
ALTER TABLE wallets
ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB';
ALTER TABLE customer_wallets
ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id),

View File

@ -1,18 +1,18 @@
-- CREATE TABLE virtual_games (
-- id BIGSERIAL PRIMARY KEY,
-- name VARCHAR(255) NOT NULL,
-- provider VARCHAR(100) NOT NULL,
-- category VARCHAR(100) NOT NULL,
-- min_bet DECIMAL(15,2) NOT NULL,
-- max_bet DECIMAL(15,2) NOT NULL,
-- volatility VARCHAR(50) NOT NULL,
-- rtp DECIMAL(5,2) NOT NULL,
-- is_featured BOOLEAN DEFAULT false,
-- popularity_score INTEGER DEFAULT 0,
-- thumbnail_url TEXT,
-- created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
-- updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
-- );
CREATE TABLE IF NOT EXISTS virtual_games (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
provider VARCHAR(100) NOT NULL,
category VARCHAR(100) NOT NULL,
min_bet DECIMAL(15,2) NOT NULL,
max_bet DECIMAL(15,2) NOT NULL,
volatility VARCHAR(50) NOT NULL,
rtp DECIMAL(5,2) NOT NULL,
is_featured BOOLEAN DEFAULT false,
popularity_score INTEGER DEFAULT 0,
thumbnail_url TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_game_interactions (
id BIGSERIAL PRIMARY KEY,

View File

@ -9,9 +9,10 @@ INSERT INTO bets (
user_id,
is_shop_bet,
cashout_id,
company_id
company_id,
outcomes_hash
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING *;
-- name: CreateBetOutcome :copyfrom
INSERT INTO bet_outcomes (
@ -87,6 +88,11 @@ WHERE event_id = $1;
SELECT *
FROM bet_outcomes
WHERE bet_id = $1;
-- name: GetBetCount :one
SELECT COUNT(*)
FROM bets
where user_id = $1
AND outcomes_hash = $2;
-- name: UpdateCashOut :exec
UPDATE bets
SET cashed_out = $2,

View File

@ -69,7 +69,8 @@ SET name = COALESCE(sqlc.narg(name), name),
location = COALESCE(sqlc.narg(location), location),
branch_manager_id = COALESCE(sqlc.narg(branch_manager_id), branch_manager_id),
company_id = COALESCE(sqlc.narg(company_id), company_id),
is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned)
is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned),
is_active = COALESCE(sqlc.narg(is_active), is_active)
WHERE id = $1
RETURNING *;
-- name: DeleteBranch :exec

View File

@ -22,23 +22,25 @@ INSERT INTO bets (
user_id,
is_shop_bet,
cashout_id,
company_id
company_id,
outcomes_hash
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash
`
type CreateBetParams struct {
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
IsShopBet bool `json:"is_shop_bet"`
CashoutID string `json:"cashout_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
IsShopBet bool `json:"is_shop_bet"`
CashoutID string `json:"cashout_id"`
CompanyID pgtype.Int8 `json:"company_id"`
OutcomesHash string `json:"outcomes_hash"`
}
func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, error) {
@ -53,6 +55,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro
arg.IsShopBet,
arg.CashoutID,
arg.CompanyID,
arg.OutcomesHash,
)
var i Bet
err := row.Scan(
@ -70,6 +73,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
)
return i, err
}
@ -111,7 +115,7 @@ func (q *Queries) DeleteBetOutcome(ctx context.Context, betID int64) error {
}
const GetAllBets = `-- name: GetAllBets :many
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes
FROM bet_with_outcomes
wHERE (
branch_id = $1
@ -167,6 +171,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.Outcomes,
); err != nil {
return nil, err
@ -180,7 +185,7 @@ func (q *Queries) GetAllBets(ctx context.Context, arg GetAllBetsParams) ([]BetWi
}
const GetBetByBranchID = `-- name: GetBetByBranchID :many
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes
FROM bet_with_outcomes
WHERE branch_id = $1
`
@ -209,6 +214,7 @@ func (q *Queries) GetBetByBranchID(ctx context.Context, branchID pgtype.Int8) ([
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.Outcomes,
); err != nil {
return nil, err
@ -222,7 +228,7 @@ func (q *Queries) GetBetByBranchID(ctx context.Context, branchID pgtype.Int8) ([
}
const GetBetByCashoutID = `-- name: GetBetByCashoutID :one
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes
FROM bet_with_outcomes
WHERE cashout_id = $1
`
@ -245,13 +251,14 @@ func (q *Queries) GetBetByCashoutID(ctx context.Context, cashoutID string) (BetW
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.Outcomes,
)
return i, err
}
const GetBetByID = `-- name: GetBetByID :one
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes
FROM bet_with_outcomes
WHERE id = $1
`
@ -274,13 +281,14 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.Outcomes,
)
return i, err
}
const GetBetByUserID = `-- name: GetBetByUserID :many
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes
SELECT id, amount, total_odds, status, full_name, phone_number, company_id, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes_hash, outcomes
FROM bet_with_outcomes
WHERE user_id = $1
`
@ -309,6 +317,7 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID pgtype.Int8) ([]Bet
&i.CreatedAt,
&i.UpdatedAt,
&i.IsShopBet,
&i.OutcomesHash,
&i.Outcomes,
); err != nil {
return nil, err
@ -321,6 +330,25 @@ func (q *Queries) GetBetByUserID(ctx context.Context, userID pgtype.Int8) ([]Bet
return items, nil
}
const GetBetCount = `-- name: GetBetCount :one
SELECT COUNT(*)
FROM bets
where user_id = $1
AND outcomes_hash = $2
`
type GetBetCountParams struct {
UserID pgtype.Int8 `json:"user_id"`
OutcomesHash string `json:"outcomes_hash"`
}
func (q *Queries) GetBetCount(ctx context.Context, arg GetBetCountParams) (int64, error) {
row := q.db.QueryRow(ctx, GetBetCount, arg.UserID, arg.OutcomesHash)
var count int64
err := row.Scan(&count)
return count, err
}
const GetBetOutcomeByBetID = `-- name: GetBetOutcomeByBetID :many
SELECT id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
FROM bet_outcomes

View File

@ -456,7 +456,8 @@ SET name = COALESCE($2, name),
location = COALESCE($3, location),
branch_manager_id = COALESCE($4, branch_manager_id),
company_id = COALESCE($5, company_id),
is_self_owned = COALESCE($6, is_self_owned)
is_self_owned = COALESCE($6, is_self_owned),
is_active = COALESCE($7, is_active)
WHERE id = $1
RETURNING id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
`
@ -468,6 +469,7 @@ type UpdateBranchParams struct {
BranchManagerID pgtype.Int8 `json:"branch_manager_id"`
CompanyID pgtype.Int8 `json:"company_id"`
IsSelfOwned pgtype.Bool `json:"is_self_owned"`
IsActive pgtype.Bool `json:"is_active"`
}
func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Branch, error) {
@ -478,6 +480,7 @@ func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Bra
arg.BranchManagerID,
arg.CompanyID,
arg.IsSelfOwned,
arg.IsActive,
)
var i Branch
err := row.Scan(

View File

@ -56,20 +56,21 @@ func (ns NullReferralstatus) Value() (driver.Value, error) {
}
type Bet struct {
ID int64 `json:"id"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
CompanyID pgtype.Int8 `json:"company_id"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
CashedOut bool `json:"cashed_out"`
CashoutID string `json:"cashout_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
IsShopBet bool `json:"is_shop_bet"`
ID int64 `json:"id"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
CompanyID pgtype.Int8 `json:"company_id"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
CashedOut bool `json:"cashed_out"`
CashoutID string `json:"cashout_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
IsShopBet bool `json:"is_shop_bet"`
OutcomesHash string `json:"outcomes_hash"`
}
type BetOutcome struct {
@ -91,21 +92,22 @@ type BetOutcome struct {
}
type BetWithOutcome struct {
ID int64 `json:"id"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
CompanyID pgtype.Int8 `json:"company_id"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
CashedOut bool `json:"cashed_out"`
CashoutID string `json:"cashout_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
IsShopBet bool `json:"is_shop_bet"`
Outcomes []BetOutcome `json:"outcomes"`
ID int64 `json:"id"`
Amount int64 `json:"amount"`
TotalOdds float32 `json:"total_odds"`
Status int32 `json:"status"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
CompanyID pgtype.Int8 `json:"company_id"`
BranchID pgtype.Int8 `json:"branch_id"`
UserID pgtype.Int8 `json:"user_id"`
CashedOut bool `json:"cashed_out"`
CashoutID string `json:"cashout_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
IsShopBet bool `json:"is_shop_bet"`
OutcomesHash string `json:"outcomes_hash"`
Outcomes []BetOutcome `json:"outcomes"`
}
type Branch struct {

7
go.mod
View File

@ -77,4 +77,9 @@ require (
go.uber.org/multierr v1.10.0 // indirect
)
require go.uber.org/atomic v1.9.0 // indirect
require (
github.com/golang/mock v1.6.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/twilio/twilio-go v1.26.3 // indirect
go.uber.org/atomic v1.9.0 // indirect
)

13
go.sum
View File

@ -9,6 +9,7 @@ github.com/amanuelabay/afrosms-go v1.0.6/go.mod h1:5mzzZtWSCDdvQsA0OyYf5CtbdGpl9
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
@ -54,6 +55,8 @@ github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27X
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@ -94,6 +97,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@ -114,6 +118,8 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/resend/resend-go/v2 v2.20.0 h1:MrIrgV0aHhwRgmcRPw33Nexn6aGJvCvG2XwfFpAMBGM=
@ -150,6 +156,8 @@ github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9J
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/twilio/twilio-go v1.26.3 h1:K2mYBzbhPVyWF+Jq5Sw53edBFvkgWo4sKTvgaO7461I=
github.com/twilio/twilio-go v1.26.3/go.mod h1:FpgNWMoD8CFnmukpKq9RNpUSGXC0BwnbeKZj2YHlIkw=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
@ -170,6 +178,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
@ -199,6 +208,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -214,8 +224,10 @@ golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -237,6 +249,7 @@ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=

View File

@ -13,25 +13,28 @@ import (
)
var (
ErrInvalidDbUrl = errors.New("db url is invalid")
ErrInvalidPort = errors.New("port number is invalid")
ErrRefreshExpiry = errors.New("refresh token expiry is invalid")
ErrAccessExpiry = errors.New("access token expiry is invalid")
ErrInvalidJwtKey = errors.New("jwt key is invalid")
ErrLogLevel = errors.New("log level not set")
ErrInvalidLevel = errors.New("invalid log level")
ErrInvalidEnv = errors.New("env not set or invalid")
ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid")
ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env")
ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid")
ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid")
ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid")
ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid")
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
ErrMissingResendApiKey = errors.New("missing Resend Api key")
ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
ErrInvalidDbUrl = errors.New("db url is invalid")
ErrInvalidPort = errors.New("port number is invalid")
ErrRefreshExpiry = errors.New("refresh token expiry is invalid")
ErrAccessExpiry = errors.New("access token expiry is invalid")
ErrInvalidJwtKey = errors.New("jwt key is invalid")
ErrLogLevel = errors.New("log level not set")
ErrInvalidLevel = errors.New("invalid log level")
ErrInvalidEnv = errors.New("env not set or invalid")
ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid")
ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env")
ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid")
ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid")
ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid")
ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid")
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
ErrMissingResendApiKey = errors.New("missing Resend Api key")
ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
ErrMissingTwilioAccountSid = errors.New("missing twilio account sid")
ErrMissingTwilioAuthToken = errors.New("missing twilio auth token")
ErrMissingTwilioSenderPhoneNumber = errors.New("missing twilio sender phone number")
)
type AleaPlayConfig struct {
@ -85,6 +88,9 @@ type Config struct {
VeliGames VeliGamesConfig `mapstructure:"veli_games"`
ResendApiKey string
ResendSenderEmail string
TwilioAccountSid string
TwilioAuthToken string
TwilioSenderPhoneNumber string
}
func NewConfig() (*Config, error) {
@ -324,6 +330,24 @@ func (c *Config) loadEnv() error {
}
c.ResendSenderEmail = resendSenderEmail
twilioAccountSid := os.Getenv("TWILIO_ACCOUNT_SID")
if twilioAccountSid == "" {
return ErrMissingTwilioAccountSid
}
c.TwilioAccountSid = twilioAccountSid
twilioAuthToken := os.Getenv("TWILIO_AUTH_TOKEN")
if twilioAuthToken == "" {
return ErrMissingTwilioAuthToken
}
c.TwilioAuthToken = twilioAuthToken
twilioSenderPhoneNumber := os.Getenv("TWILIO_SENDER_PHONE_NUMBER")
if twilioSenderPhoneNumber == "" {
return ErrMissingTwilioSenderPhoneNumber
}
c.TwilioSenderPhoneNumber = twilioSenderPhoneNumber
return nil
}

View File

@ -81,16 +81,17 @@ type GetBet struct {
}
type CreateBet struct {
Amount Currency
TotalOdds float32
Status OutcomeStatus
FullName string
PhoneNumber string
CompanyID ValidInt64 // Can Be Nullable
BranchID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable
IsShopBet bool
CashoutID string
Amount Currency
TotalOdds float32
Status OutcomeStatus
FullName string
PhoneNumber string
CompanyID ValidInt64 // Can Be Nullable
BranchID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable
IsShopBet bool
CashoutID string
OutcomesHash string
}
type CreateBetOutcomeReq struct {

View File

@ -58,6 +58,7 @@ type UpdateBranch struct {
BranchManagerID *int64
CompanyID *int64
IsSelfOwned *bool
IsActive *bool
}
type CreateSupportedOperation struct {

View File

@ -26,6 +26,13 @@ const (
OtpMediumSms OtpMedium = "sms"
)
type OtpProvider string
const (
TwilioSms OtpProvider = "twilio"
AfroMessage OtpProvider = "aformessage"
)
type Otp struct {
ID int64
SentTo string

View File

@ -269,6 +269,19 @@ func (s *Store) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetB
return result, nil
}
func (s *Store) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
count, err := s.queries.GetBetCount(ctx, dbgen.GetBetCountParams{
UserID: pgtype.Int8{Int64: UserID},
OutcomesHash: outcomesHash,
})
if err != nil {
return 0, err
}
return count, nil
}
func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {
err := s.queries.UpdateCashOut(ctx, dbgen.UpdateCashOutParams{
ID: id,

View File

@ -83,6 +83,12 @@ func convertUpdateBranch(updateBranch domain.UpdateBranch) dbgen.UpdateBranchPar
Valid: true,
}
}
if updateBranch.IsActive != nil {
newUpdateBranch.IsActive = pgtype.Bool{
Bool: *updateBranch.IsActive,
Valid: true,
}
}
return newUpdateBranch
}

View File

@ -17,6 +17,7 @@ type BetStore interface {
GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error)
GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error)
GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error)
GetBetCount(ctx context.Context, userID int64, outcomesHash string) (int64, error)
UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)

View File

@ -3,13 +3,17 @@ package bet
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math/big"
random "math/rand"
"sort"
"strconv"
"strings"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -196,6 +200,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
var totalOdds float32 = 1
for _, outcomeReq := range req.Outcomes {
fmt.Println("reqq: ", outcomeReq)
newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
if err != nil {
s.mongoLogger.Error("failed to generate outcome",
@ -211,6 +216,23 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
outcomes = append(outcomes, newOutcome)
}
outcomesHash, err := generateOutcomeHash(outcomes)
if err != nil {
s.mongoLogger.Error("failed to generate outcome hash",
zap.Int64("user_id", userID),
zap.Error(err),
)
return domain.CreateBetRes{}, err
}
count, err := s.GetBetCount(ctx, userID, outcomesHash)
if err != nil {
return domain.CreateBetRes{}, err
}
if count == 2 {
return domain.CreateBetRes{}, fmt.Errorf("bet already pleaced twice")
}
cashoutID, err := s.GenerateCashoutID()
if err != nil {
s.mongoLogger.Error("failed to generate cashout ID",
@ -221,12 +243,13 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
}
newBet := domain.CreateBet{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
CashoutID: cashoutID,
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
CashoutID: cashoutID,
OutcomesHash: outcomesHash,
}
switch role {
@ -329,6 +352,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type")
}
fmt.Println("Bet is: ", newBet)
bet, err := s.CreateBet(ctx, newBet)
if err != nil {
s.mongoLogger.Error("failed to create bet",
@ -644,6 +668,10 @@ func (s *Service) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.Ge
return s.betStore.GetBetByUserID(ctx, UserID)
}
func (s *Service) GetBetCount(ctx context.Context, UserID int64, outcomesHash string) (int64, error) {
return s.betStore.GetBetCount(ctx, UserID, outcomesHash)
}
func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error {
return s.betStore.UpdateCashOut(ctx, id, cashedOut)
}
@ -794,3 +822,24 @@ func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status d
func (s *Service) DeleteBet(ctx context.Context, id int64) error {
return s.betStore.DeleteBet(ctx, id)
}
func generateOutcomeHash(outcomes []domain.CreateBetOutcome) (string, error) {
// should always be in the same order for producing the same hash
sort.Slice(outcomes, func(i, j int) bool {
if outcomes[i].EventID != outcomes[j].EventID {
return outcomes[i].EventID < outcomes[j].EventID
}
if outcomes[i].MarketID != outcomes[j].MarketID {
return outcomes[i].MarketID < outcomes[j].MarketID
}
return outcomes[i].OddID < outcomes[j].OddID
})
var sb strings.Builder
for _, o := range outcomes {
sb.WriteString(fmt.Sprintf("%d-%d-%d;", o.EventID, o.MarketID, o.OddID))
}
sum := sha256.Sum256([]byte(sb.String()))
return hex.EncodeToString(sum[:]), nil
}

View File

@ -10,18 +10,29 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
afro "github.com/amanuelabay/afrosms-go"
"github.com/resend/resend-go/v2"
"github.com/twilio/twilio-go"
twilioApi "github.com/twilio/twilio-go/rest/api/v2010"
"golang.org/x/crypto/bcrypt"
)
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium) error {
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.OtpProvider) error {
otpCode := helpers.GenerateOTP()
message := fmt.Sprintf("Welcome to Fortune bets, your OTP is %s please don't share with anyone.", otpCode)
switch medium {
case domain.OtpMediumSms:
if err := s.SendSMSOTP(ctx, sentTo, message); err != nil {
return err
switch provider {
case "twilio":
if err := s.SendTwilioSMSOTP(ctx, sentTo, message, provider); err != nil {
return err
}
case "afromessage":
if err := s.SendAfroMessageSMSOTP(ctx, sentTo, message, provider); err != nil {
return err
}
default:
return fmt.Errorf("invalid sms provider: %s", provider)
}
case domain.OtpMediumEmail:
if err := s.SendEmailOTP(ctx, sentTo, message); err != nil {
@ -51,7 +62,7 @@ func hashPassword(plaintextPassword string) ([]byte, error) {
return hash, nil
}
func (s *Service) SendSMSOTP(ctx context.Context, receiverPhone, message string) error {
func (s *Service) SendAfroMessageSMSOTP(ctx context.Context, receiverPhone, message string, provider domain.OtpProvider) error {
apiKey := s.config.AFRO_SMS_API_KEY
senderName := s.config.AFRO_SMS_SENDER_NAME
hostURL := s.config.ADRO_SMS_HOST_URL
@ -79,6 +90,29 @@ func (s *Service) SendSMSOTP(ctx context.Context, receiverPhone, message string)
}
}
func (s *Service) SendTwilioSMSOTP(ctx context.Context, receiverPhone, message string, provider domain.OtpProvider) error {
accountSid := s.config.TwilioAccountSid
authToken := s.config.TwilioAuthToken
senderPhone := s.config.TwilioSenderPhoneNumber
client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: accountSid,
Password: authToken,
})
params := &twilioApi.CreateMessageParams{}
params.SetTo(receiverPhone)
params.SetFrom(senderPhone)
params.SetBody(message)
_, err := client.Api.CreateMessage(params)
if err != nil {
return fmt.Errorf("Error sending SMS message: %s" + err.Error())
}
return nil
}
func (s *Service) SendEmailOTP(ctx context.Context, receiverEmail, message string) error {
apiKey := s.config.ResendApiKey
client := resend.NewClient(apiKey)

View File

@ -10,7 +10,7 @@ import (
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
}
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.OtpProvider) error {
var err error
// check if user exists
switch medium {
@ -26,7 +26,7 @@ func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium,
}
// send otp based on the medium
return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium)
return s.SendOtp(ctx, sentTo, domain.OtpRegister, medium, provider)
}
func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterUserReq) (domain.User, error) { // normal
// get otp

View File

@ -8,7 +8,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string) error {
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.OtpProvider) error {
var err error
// check if user exists
@ -23,7 +23,7 @@ func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, se
return err
}
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium)
return s.SendOtp(ctx, sentTo, domain.OtpReset, medium, provider)
}

View File

@ -46,26 +46,26 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
spec string
task func()
}{
// {
// spec: "0 0 * * * *", // Every 1 hour
// task: func() {
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
// log.Printf("FetchUpcomingEvents error: %v", err)
// }
// },
// },
// {
// spec: "0 0 * * * *", // Every 15 minutes
// task: func() {
// if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
// log.Printf("FetchNonLiveOdds error: %v", err)
// }
// },
// },
// {
// spec: "0 */5 * * * *", // Every 5 Minutes
// task: func() {
// log.Println("Updating expired events status...")
{
spec: "0 0 * * * *", // Every 1 hour
task: func() {
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err)
}
},
},
{
spec: "0 0 * * * *", // Every 15 minutes
task: func() {
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
log.Printf("FetchNonLiveOdds error: %v", err)
}
},
},
{
spec: "0 */5 * * * *", // Every 5 Minutes
task: func() {
log.Println("Updating expired events status...")
// if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil {
// log.Printf("Failed to update events: %v", err)
@ -89,7 +89,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
}
for _, job := range schedule {
job.task()
// job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
}

View File

@ -38,8 +38,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
res, err := h.betSvc.
PlaceBet(c.Context(), req, userID, role)
res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role)
if err != nil {
h.logger.Error("PlaceBet failed", "error", err)

View File

@ -2,6 +2,7 @@ package handlers
import (
"strconv"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
@ -697,6 +698,42 @@ func (h *Handler) UpdateBranch(c *fiber.Ctx) error {
}
func (h *Handler) UpdateBranchStatus(c *fiber.Ctx) error {
branchID := c.Params("id")
id, err := strconv.ParseInt(branchID, 10, 64)
if err != nil {
h.logger.Error("Invalid branch ID", "branchID", branchID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", err, nil)
}
var isActive bool
path := strings.Split(strings.Trim(c.Path(), "/"), "/")
if path[len(path)-1] == "set-active" {
isActive = true
} else if path[len(path)-1] == "set-inactive" {
isActive = false
} else {
h.logger.Error("Invalid branch status", "status", isActive, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch status", err, nil)
}
branch, err := h.branchSvc.UpdateBranch(c.Context(), domain.UpdateBranch{
ID: id,
IsActive: &isActive,
})
if err != nil {
h.logger.Error("Failed to update branch", "branchID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update branch", err, nil)
}
res := convertBranch(branch)
return response.WriteJSON(c, fiber.StatusOK, "Branch Updated", res, nil)
}
// DeleteBranch godoc
// @Summary Delete the branch
// @Description Delete the branch

View File

@ -59,8 +59,9 @@ func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error {
}
type RegisterCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Provider domain.OtpProvider `json:"provider" validate:"required" example:"twilio"`
}
// SendRegisterCode godoc
@ -98,7 +99,7 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
}
if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo); err != nil {
if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo, req.Provider); err != nil {
h.logger.Error("Failed to send register code", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send register code")
}
@ -107,13 +108,14 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error {
}
type RegisterUserReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
Otp string `json:"otp" example:"123456"`
ReferalCode string `json:"referal_code" example:"ABC123"`
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
Otp string `json:"otp" example:"123456"`
ReferalCode string `json:"referal_code" example:"ABC123"`
Provider domain.OtpProvider `json:"provider" validate:"required" example:"twilio"`
}
// RegisterUser godoc
@ -204,8 +206,9 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
}
type ResetCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Provider domain.OtpProvider `json:"provider" validate:"required" example:"twilio"`
}
// SendResetCode godoc
@ -243,7 +246,7 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided")
}
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil {
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, req.Provider); err != nil {
h.logger.Error("Failed to send reset code", "error", err)
fmt.Println(err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code")

View File

@ -52,7 +52,7 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"message": "Welcome to the FortuneBet API",
"version": "1.0dev3",
"version": "1.0dev6",
})
})
@ -146,6 +146,8 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/branch/:id", a.authMiddleware, h.GetBranchByID)
a.fiber.Get("/branch/:id/bets", a.authMiddleware, h.GetBetByBranchID)
a.fiber.Put("/branch/:id", a.authMiddleware, h.UpdateBranch)
a.fiber.Put("/branch/:id/set-active", a.authMiddleware, h.UpdateBranchStatus)
a.fiber.Put("/branch/:id/set-inactive", a.authMiddleware, h.UpdateBranchStatus)
a.fiber.Delete("/branch/:id", a.authMiddleware, h.DeleteBranch)
a.fiber.Get("/search/branch", a.authMiddleware, h.SearchBranch)
// /branch/search

View File

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

File diff suppressed because one or more lines are too long

BIN
tmp/main

Binary file not shown.