From dc2144a91eb7a1f98adf507dda89edb2f272e423 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 16 Jun 2025 20:04:28 +0300 Subject: [PATCH 1/7] resolve conflict --- .gitignore | 14 +++---- db/migrations/000006_recommendation.up.sql | 30 +++++++-------- db/query/branch.sql | 3 +- gen/db/branch.sql.go | 5 ++- internal/domain/branch.go | 1 + internal/repository/branch.go | 6 +++ .../web_server/handlers/branch_handler.go | 37 +++++++++++++++++++ internal/web_server/routes.go | 2 + logs/app.log | 6 +++ 9 files changed, 80 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index e80176d..32a2000 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -# bin -# coverage.out -# coverage -# .env -# tmp -# build -# *.log \ No newline at end of file +bin +coverage.out +coverage +.env +tmp +build +*.log \ No newline at end of file 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/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/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/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/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/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/routes.go b/internal/web_server/routes.go index 784338a..363b577 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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 diff --git a/logs/app.log b/logs/app.log index e69de29..d95137b 100644 --- a/logs/app.log +++ b/logs/app.log @@ -0,0 +1,6 @@ +time=2025-06-15T11:01:26.408+03:00 level=INFO msg="No events were updated" service_info.env=development +time=2025-06-15T11:01:26.410+03:00 level=INFO msg="Successfully processed results" service_info.env=development removed_events=0 total_events=0 +time=2025-06-15T11:01:26.411+03:00 level=INFO msg="Starting server" service_info.env=development port=8080 +time=2025-06-15T11:07:09.604+03:00 level=INFO msg="No events were updated" service_info.env=development +time=2025-06-15T11:07:09.605+03:00 level=INFO msg="Successfully processed results" service_info.env=development removed_events=0 total_events=0 +time=2025-06-15T11:07:09.606+03:00 level=INFO msg="Starting server" service_info.env=development port=8080 From 3df12adf92dedb82928393b2e03aa06e44fe9b32 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 16 Jun 2025 23:39:27 +0300 Subject: [PATCH 2/7] fix: preparing repo for v1.0dev6 deployment --- internal/web_server/cron.go | 34 +++++++++++++++++----------------- internal/web_server/routes.go | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) 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/routes.go b/internal/web_server/routes.go index 784338a..8f10c44 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", }) }) From 503333589bcb2259c2faaece6aa5ada2f7ee4f23 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 16 Jun 2025 23:41:00 +0300 Subject: [PATCH 3/7] fix: database error --- db/migrations/000001_fortune.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6a9157e..393e2cc 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -310,7 +310,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), From bfba23bc69fc9885754d0bc941f47c40bd6c78eb Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 16 Jun 2025 23:59:24 +0300 Subject: [PATCH 4/7] fix: mongo fail issue --- internal/logger/mongoLogger/init.go | 2 +- internal/web_server/handlers/mongoLogger.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/logger/mongoLogger/init.go b/internal/logger/mongoLogger/init.go index 9d4b78b..77ef645 100644 --- a/internal/logger/mongoLogger/init.go +++ b/internal/logger/mongoLogger/init.go @@ -10,7 +10,7 @@ import ( func InitLogger() (*zap.Logger, error) { mongoCore, err := NewMongoCore( - "mongodb://root:secret@mongo:27017/?authSource=admin", + "mongodb://root:secret@localhost:27017/?authSource=admin", "logdb", "applogs", zapcore.InfoLevel, 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) } } From 78d351cae90f7bede219183a6c7f5388b34ca73d Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Tue, 17 Jun 2025 01:17:22 +0300 Subject: [PATCH 5/7] bet on same bet only twice --- db/migrations/000001_fortune.up.sql | 3 +- db/query/bet.sql | 10 ++- gen/db/bet.sql.go | 64 +++++++++++++------ gen/db/models.go | 70 ++++++++++++--------- gen/db/wallet.sql.go | 12 ++-- internal/domain/bet.go | 22 +++---- internal/repository/bet.go | 13 ++++ internal/services/bet/port.go | 1 + internal/services/bet/service.go | 61 ++++++++++++++++-- internal/web_server/handlers/bet_handler.go | 3 +- 10 files changed, 186 insertions(+), 73 deletions(-) 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/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/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/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/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/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/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/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) From 5461feaa0badf14bf8fff317e6878db71996f3e4 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 16 Jun 2025 20:04:28 +0300 Subject: [PATCH 6/7] resolve conflict --- db/migrations/000006_recommendation.up.sql | 30 +++++++-------- db/query/branch.sql | 3 +- gen/db/branch.sql.go | 5 ++- internal/domain/branch.go | 1 + internal/repository/branch.go | 6 +++ .../web_server/handlers/branch_handler.go | 37 +++++++++++++++++++ internal/web_server/routes.go | 2 + 7 files changed, 67 insertions(+), 17 deletions(-) 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/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/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/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/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/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/routes.go b/internal/web_server/routes.go index 8f10c44..e7e43a4 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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 From 808d7b9eeb9a233cd387da08b7d9c568d2151e9a Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Tue, 17 Jun 2025 12:07:12 +0300 Subject: [PATCH 7/7] twilio sms support - (trial version) --- go.mod | 7 +++- go.sum | 13 ++++++ internal/config/config.go | 62 +++++++++++++++++++--------- internal/domain/otp.go | 7 ++++ internal/services/user/common.go | 42 +++++++++++++++++-- internal/services/user/register.go | 4 +- internal/services/user/reset.go | 6 +-- internal/web_server/handlers/user.go | 29 +++++++------ 8 files changed, 128 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index cfc550d..5372953 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum index 8420e2a..6faf62c 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= @@ -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= diff --git a/internal/config/config.go b/internal/config/config.go index 802302e..362a38f 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 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 } 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/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/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")