diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6a9157e..a7d9d93 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -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 @@ -310,7 +311,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), diff --git a/db/migrations/000006_recommendation.up.sql b/db/migrations/000006_recommendation.up.sql index f7806c5..28d4cc0 100644 --- a/db/migrations/000006_recommendation.up.sql +++ b/db/migrations/000006_recommendation.up.sql @@ -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, diff --git a/db/query/bet.sql b/db/query/bet.sql index 335cf56..9b31f11 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -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 ( @@ -83,6 +84,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, diff --git a/db/query/branch.sql b/db/query/branch.sql index bb01b26..176b947 100644 --- a/db/query/branch.sql +++ b/db/query/branch.sql @@ -61,7 +61,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 diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index e4cde1d..3396e25 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -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 @@ -157,6 +161,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 @@ -170,7 +175,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 ` @@ -199,6 +204,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 @@ -212,7 +218,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 ` @@ -235,13 +241,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 ` @@ -264,13 +271,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 ` @@ -299,6 +307,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 @@ -311,6 +320,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 diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 92e7f80..ad59526 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -443,7 +443,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 ` @@ -455,6 +456,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) { @@ -465,6 +467,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( diff --git a/gen/db/models.go b/gen/db/models.go index 1da22f3..b92792d 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -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 { @@ -204,6 +206,15 @@ type Event struct { Source pgtype.Text `json:"source"` } +type ExchangeRate struct { + ID int32 `json:"id"` + FromCurrency string `json:"from_currency"` + ToCurrency string `json:"to_currency"` + Rate pgtype.Numeric `json:"rate"` + ValidUntil pgtype.Timestamp `json:"valid_until"` + CreatedAt pgtype.Timestamp `json:"created_at"` +} + type League struct { ID int64 `json:"id"` Name string `json:"name"` @@ -470,6 +481,7 @@ type Wallet struct { IsActive bool `json:"is_active"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` + Currency string `json:"currency"` BonusBalance pgtype.Numeric `json:"bonus_balance"` CashBalance pgtype.Numeric `json:"cash_balance"` } diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index e46ea0b..c0c3d3c 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -49,7 +49,7 @@ INSERT INTO wallets ( user_id ) VALUES ($1, $2, $3, $4) -RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance +RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance ` type CreateWalletParams struct { @@ -77,6 +77,7 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.Currency, &i.BonusBalance, &i.CashBalance, ) @@ -143,7 +144,7 @@ func (q *Queries) GetAllBranchWallets(ctx context.Context) ([]GetAllBranchWallet } const GetAllWallets = `-- name: GetAllWallets :many -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance FROM wallets ` @@ -166,6 +167,7 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.Currency, &i.BonusBalance, &i.CashBalance, ); err != nil { @@ -225,7 +227,7 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (GetC } const GetWalletByID = `-- name: GetWalletByID :one -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance FROM wallets WHERE id = $1 ` @@ -243,6 +245,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.Currency, &i.BonusBalance, &i.CashBalance, ) @@ -250,7 +253,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { } const GetWalletByUserID = `-- name: GetWalletByUserID :many -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, bonus_balance, cash_balance +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance FROM wallets WHERE user_id = $1 ` @@ -274,6 +277,7 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet &i.IsActive, &i.CreatedAt, &i.UpdatedAt, + &i.Currency, &i.BonusBalance, &i.CashBalance, ); err != nil { diff --git a/go.mod b/go.mod index 8728f9b..75d188f 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,12 @@ require ( ) require ( - github.com/go-resty/resty/v2 v2.16.5 // indirect + github.com/go-resty/resty/v2 v2.16.5 + github.com/twilio/twilio-go v1.26.3 +) + +require ( + github.com/golang/mock v1.6.0 // indirect + github.com/pkg/errors v0.9.1 // indirect go.uber.org/atomic v1.9.0 // indirect ) diff --git a/go.sum b/go.sum index a3fd1ae..56862fa 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -56,6 +57,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= @@ -96,6 +99,8 @@ 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 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= +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= @@ -116,6 +121,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= @@ -152,6 +159,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= @@ -172,13 +181,13 @@ 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= go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= @@ -201,6 +210,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= @@ -216,8 +226,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= @@ -236,9 +248,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= diff --git a/internal/config/config.go b/internal/config/config.go index a83da59..c2af075 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 VeliConfig `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 } diff --git a/internal/domain/bet.go b/internal/domain/bet.go index cbd904e..13828f2 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -80,16 +80,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 { @@ -173,4 +174,3 @@ func ConvertBet(bet GetBet) BetRes { CreatedAt: bet.CreatedAt, } } - diff --git a/internal/domain/branch.go b/internal/domain/branch.go index 43d2cc0..99876ed 100644 --- a/internal/domain/branch.go +++ b/internal/domain/branch.go @@ -53,6 +53,7 @@ type UpdateBranch struct { BranchManagerID *int64 CompanyID *int64 IsSelfOwned *bool + IsActive *bool } type CreateSupportedOperation struct { diff --git a/internal/domain/otp.go b/internal/domain/otp.go index a6904e4..23c8640 100644 --- a/internal/domain/otp.go +++ b/internal/domain/otp.go @@ -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 diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 560eb62..ed98a74 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -265,6 +265,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, diff --git a/internal/repository/branch.go b/internal/repository/branch.go index 51f460f..a9f9980 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -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 } diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index a249e43..753ec3c 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -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) diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 59d0bc0..ddc91b6 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -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 { @@ -321,6 +344,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", @@ -636,6 +660,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) } @@ -785,3 +813,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 +} diff --git a/internal/services/user/common.go b/internal/services/user/common.go index fd4f9aa..6cab064 100644 --- a/internal/services/user/common.go +++ b/internal/services/user/common.go @@ -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) diff --git a/internal/services/user/register.go b/internal/services/user/register.go index 3169b7b..c7e0d83 100644 --- a/internal/services/user/register.go +++ b/internal/services/user/register.go @@ -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 diff --git a/internal/services/user/reset.go b/internal/services/user/reset.go index c6d3f47..7c4e5d5 100644 --- a/internal/services/user/reset.go +++ b/internal/services/user/reset.go @@ -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) } @@ -57,7 +57,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo return err } // reset pass and mark otp as used - + err = s.userStore.UpdatePassword(ctx, sentTo, hashedPassword, otp.ID) if err != nil { return err diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 9aef6cc..5c8a104 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -46,22 +46,22 @@ 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 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() { @@ -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) } diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index a7a0706..69b10b8 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -40,8 +40,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) diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index 6f869a1..f5d5866 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -2,6 +2,7 @@ package handlers import ( "strconv" + "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" @@ -682,6 +683,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 diff --git a/internal/web_server/handlers/mongoLogger.go b/internal/web_server/handlers/mongoLogger.go index 384e3a2..f31d780 100644 --- a/internal/web_server/handlers/mongoLogger.go +++ b/internal/web_server/handlers/mongoLogger.go @@ -12,7 +12,7 @@ import ( func GetLogsHandler(appCtx context.Context) fiber.Handler { return func(c *fiber.Ctx) error { - client, err := mongo.Connect(appCtx, options.Client().ApplyURI("mongodb://root:secret@mongo:27017/?authSource=admin")) + client, err := mongo.Connect(appCtx, options.Client().ApplyURI("mongodb://root:secret@localhost:27017/?authSource=admin")) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error()) } @@ -32,7 +32,6 @@ func GetLogsHandler(appCtx context.Context) fiber.Handler { return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error()) } - return c.JSON(logs) } } diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 522551c..8ef77ce 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -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 @@ -203,8 +205,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 @@ -242,7 +245,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") diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 03ef2db..1118aaa 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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