diff --git a/.gitignore b/.gitignore index 036c1c6..d96cd29 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ build logs/ app_logs/ backup/ - +reports/ diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 5a74346..a5ebe1c 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -216,7 +216,11 @@ CREATE TABLE IF NOT EXISTS branches ( is_self_owned BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(wallet_id) + UNIQUE(wallet_id), + CONSTRAINT profit_percentage_check CHECK ( + profit_percent >= 0 + AND profit_percent < 1 + ) ); CREATE TABLE IF NOT EXISTS branch_operations ( id BIGSERIAL PRIMARY KEY, @@ -258,7 +262,8 @@ CREATE TABLE events ( status TEXT, fetched_at TIMESTAMP DEFAULT now(), source TEXT DEFAULT 'b365api', - flagged BOOLEAN NOT NULL DEFAULT false + is_featured BOOLEAN NOT NULL DEFAULT FALSE, + is_active BOOLEAN NOT NULL DEFAULT TRUE ); CREATE TABLE odds ( id SERIAL PRIMARY KEY, @@ -289,7 +294,11 @@ CREATE TABLE companies ( deducted_percentage REAL NOT NULL, is_active BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT deducted_percentage_check CHECK ( + deducted_percentage >= 0 + AND deducted_percentage < 1 + ) ); CREATE TABLE leagues ( id BIGINT PRIMARY KEY, diff --git a/db/query/branch.sql b/db/query/branch.sql index 4c38e0c..1e09f40 100644 --- a/db/query/branch.sql +++ b/db/query/branch.sql @@ -5,9 +5,10 @@ INSERT INTO branches ( wallet_id, branch_manager_id, company_id, - is_self_owned + is_self_owned, + profit_percent ) -VALUES ($1, $2, $3, $4, $5, $6) +VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; -- name: CreateSupportedOperation :one INSERT INTO supported_operations (name, description) @@ -88,6 +89,7 @@ SET name = COALESCE(sqlc.narg(name), name), company_id = COALESCE(sqlc.narg(company_id), company_id), is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned), is_active = COALESCE(sqlc.narg(is_active), is_active), + profit_percent = COALESCE(sqlc.narg(profit_percent), profit_percent), updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING *; diff --git a/db/query/events.sql b/db/query/events.sql index 14750c8..b88f6d8 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -157,6 +157,11 @@ WHERE is_live = false events.sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) + AND ( + match_name ILIKE '%' || sqlc.narg('query') || '%' + OR league_name ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ) AND ( start_time < sqlc.narg('last_start_time') OR sqlc.narg('last_start_time') IS NULL @@ -170,8 +175,8 @@ WHERE is_live = false OR sqlc.narg('country_code') IS NULL ) AND ( - flagged = sqlc.narg('flagged') - OR sqlc.narg('flagged') IS NULL + events.is_featured = sqlc.narg('is_featured') + OR sqlc.narg('is_featured') IS NULL ); -- name: GetPaginatedUpcomingEvents :many SELECT events.*, @@ -189,6 +194,11 @@ WHERE start_time > now() events.sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) + AND ( + match_name ILIKE '%' || sqlc.narg('query') || '%' + OR league_name ILIKE '%' || sqlc.narg('query') || '%' + OR sqlc.narg('query') IS NULL + ) AND ( start_time < sqlc.narg('last_start_time') OR sqlc.narg('last_start_time') IS NULL @@ -202,8 +212,8 @@ WHERE start_time > now() OR sqlc.narg('country_code') IS NULL ) AND ( - flagged = sqlc.narg('flagged') - OR sqlc.narg('flagged') IS NULL + events.is_featured = sqlc.narg('is_featured') + OR sqlc.narg('is_featured') IS NULL ) ORDER BY start_time ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); @@ -219,9 +229,9 @@ UPDATE events SET score = $1, status = $2 WHERE id = $3; --- name: UpdateFlagged :exec +-- name: UpdateFeatured :exec UPDATE events -SET flagged = $1 +SET is_featured = $1 WHERE id = $2; -- name: DeleteEvent :exec DELETE FROM events diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 88a96db..a9a57b8 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -18,19 +18,21 @@ INSERT INTO branches ( wallet_id, branch_manager_id, company_id, - is_self_owned + is_self_owned, + profit_percent ) -VALUES ($1, $2, $3, $4, $5, $6) +VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at ` type CreateBranchParams struct { - Name string `json:"name"` - Location string `json:"location"` - WalletID int64 `json:"wallet_id"` - BranchManagerID int64 `json:"branch_manager_id"` - CompanyID int64 `json:"company_id"` - IsSelfOwned bool `json:"is_self_owned"` + Name string `json:"name"` + Location string `json:"location"` + WalletID int64 `json:"wallet_id"` + BranchManagerID int64 `json:"branch_manager_id"` + CompanyID int64 `json:"company_id"` + IsSelfOwned bool `json:"is_self_owned"` + ProfitPercent float32 `json:"profit_percent"` } func (q *Queries) CreateBranch(ctx context.Context, arg CreateBranchParams) (Branch, error) { @@ -41,6 +43,7 @@ func (q *Queries) CreateBranch(ctx context.Context, arg CreateBranchParams) (Bra arg.BranchManagerID, arg.CompanyID, arg.IsSelfOwned, + arg.ProfitPercent, ) var i Branch err := row.Scan( @@ -498,19 +501,21 @@ SET name = COALESCE($2, name), company_id = COALESCE($5, company_id), is_self_owned = COALESCE($6, is_self_owned), is_active = COALESCE($7, is_active), + profit_percent = COALESCE($8, profit_percent), updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at ` type UpdateBranchParams struct { - ID int64 `json:"id"` - Name pgtype.Text `json:"name"` - Location pgtype.Text `json:"location"` - 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"` + ID int64 `json:"id"` + Name pgtype.Text `json:"name"` + Location pgtype.Text `json:"location"` + 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"` + ProfitPercent pgtype.Float4 `json:"profit_percent"` } func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Branch, error) { @@ -522,6 +527,7 @@ func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Bra arg.CompanyID, arg.IsSelfOwned, arg.IsActive, + arg.ProfitPercent, ) var i Branch err := row.Scan( diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 9e11418..101c705 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -22,7 +22,7 @@ func (q *Queries) DeleteEvent(ctx context.Context, id string) error { } const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many -SELECT id, sport_id, match_name, home_team, away_team, home_team_id, away_team_id, home_kit_image, away_kit_image, league_id, league_name, league_cc, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, flagged +SELECT id, sport_id, match_name, home_team, away_team, home_team_id, away_team_id, home_kit_image, away_kit_image, league_id, league_name, league_cc, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, is_featured, is_active FROM events WHERE start_time > now() AND is_live = false @@ -62,7 +62,8 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { &i.Status, &i.FetchedAt, &i.Source, - &i.Flagged, + &i.IsFeatured, + &i.IsActive, ); err != nil { return nil, err } @@ -75,7 +76,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]Event, error) { } const GetExpiredUpcomingEvents = `-- name: GetExpiredUpcomingEvents :many -SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.flagged, +SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_active, leagues.country_code as league_cc FROM events LEFT JOIN leagues ON leagues.id = league_id @@ -110,7 +111,8 @@ type GetExpiredUpcomingEventsRow struct { Status pgtype.Text `json:"status"` FetchedAt pgtype.Timestamp `json:"fetched_at"` Source pgtype.Text `json:"source"` - Flagged bool `json:"flagged"` + IsFeatured bool `json:"is_featured"` + IsActive bool `json:"is_active"` LeagueCc_2 pgtype.Text `json:"league_cc_2"` } @@ -146,7 +148,8 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te &i.Status, &i.FetchedAt, &i.Source, - &i.Flagged, + &i.IsFeatured, + &i.IsActive, &i.LeagueCc_2, ); err != nil { return nil, err @@ -160,7 +163,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te } const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many -SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.flagged, +SELECT events.id, events.sport_id, events.match_name, events.home_team, events.away_team, events.home_team_id, events.away_team_id, events.home_kit_image, events.away_kit_image, events.league_id, events.league_name, events.league_cc, events.start_time, events.score, events.match_minute, events.timer_status, events.added_time, events.match_period, events.is_live, events.status, events.fetched_at, events.source, events.is_featured, events.is_active, leagues.country_code as league_cc FROM events LEFT JOIN leagues ON leagues.id = league_id @@ -176,32 +179,38 @@ WHERE start_time > now() OR $2 IS NULL ) AND ( - start_time < $3 + match_name ILIKE '%' || $3 || '%' + OR league_name ILIKE '%' || $3 || '%' OR $3 IS NULL ) AND ( - start_time > $4 + start_time < $4 OR $4 IS NULL ) AND ( - leagues.country_code = $5 + start_time > $5 OR $5 IS NULL ) AND ( - flagged = $6 + leagues.country_code = $6 OR $6 IS NULL ) + AND ( + events.is_featured = $7 + OR $7 IS NULL + ) ORDER BY start_time ASC -LIMIT $8 OFFSET $7 +LIMIT $9 OFFSET $8 ` type GetPaginatedUpcomingEventsParams struct { LeagueID pgtype.Int4 `json:"league_id"` SportID pgtype.Int4 `json:"sport_id"` + Query pgtype.Text `json:"query"` LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` CountryCode pgtype.Text `json:"country_code"` - Flagged pgtype.Bool `json:"flagged"` + IsFeatured pgtype.Bool `json:"is_featured"` Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"` } @@ -229,7 +238,8 @@ type GetPaginatedUpcomingEventsRow struct { Status pgtype.Text `json:"status"` FetchedAt pgtype.Timestamp `json:"fetched_at"` Source pgtype.Text `json:"source"` - Flagged bool `json:"flagged"` + IsFeatured bool `json:"is_featured"` + IsActive bool `json:"is_active"` LeagueCc_2 pgtype.Text `json:"league_cc_2"` } @@ -237,10 +247,11 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, arg.LeagueID, arg.SportID, + arg.Query, arg.LastStartTime, arg.FirstStartTime, arg.CountryCode, - arg.Flagged, + arg.IsFeatured, arg.Offset, arg.Limit, ) @@ -274,7 +285,8 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat &i.Status, &i.FetchedAt, &i.Source, - &i.Flagged, + &i.IsFeatured, + &i.IsActive, &i.LeagueCc_2, ); err != nil { return nil, err @@ -302,40 +314,47 @@ WHERE is_live = false OR $2 IS NULL ) AND ( - start_time < $3 + match_name ILIKE '%' || $3 || '%' + OR league_name ILIKE '%' || $3 || '%' OR $3 IS NULL ) AND ( - start_time > $4 + start_time < $4 OR $4 IS NULL ) AND ( - leagues.country_code = $5 + start_time > $5 OR $5 IS NULL ) AND ( - flagged = $6 + leagues.country_code = $6 OR $6 IS NULL ) + AND ( + events.is_featured = $7 + OR $7 IS NULL + ) ` type GetTotalEventsParams struct { LeagueID pgtype.Int4 `json:"league_id"` SportID pgtype.Int4 `json:"sport_id"` + Query pgtype.Text `json:"query"` LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` CountryCode pgtype.Text `json:"country_code"` - Flagged pgtype.Bool `json:"flagged"` + IsFeatured pgtype.Bool `json:"is_featured"` } func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) (int64, error) { row := q.db.QueryRow(ctx, GetTotalEvents, arg.LeagueID, arg.SportID, + arg.Query, arg.LastStartTime, arg.FirstStartTime, arg.CountryCode, - arg.Flagged, + arg.IsFeatured, ) var count int64 err := row.Scan(&count) @@ -343,7 +362,7 @@ func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) } const GetUpcomingByID = `-- name: GetUpcomingByID :one -SELECT id, sport_id, match_name, home_team, away_team, home_team_id, away_team_id, home_kit_image, away_kit_image, league_id, league_name, league_cc, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, flagged +SELECT id, sport_id, match_name, home_team, away_team, home_team_id, away_team_id, home_kit_image, away_kit_image, league_id, league_name, league_cc, start_time, score, match_minute, timer_status, added_time, match_period, is_live, status, fetched_at, source, is_featured, is_active FROM events WHERE id = $1 AND is_live = false @@ -377,7 +396,8 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (Event, error) &i.Status, &i.FetchedAt, &i.Source, - &i.Flagged, + &i.IsFeatured, + &i.IsActive, ) return i, err } @@ -623,19 +643,19 @@ func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) { return items, nil } -const UpdateFlagged = `-- name: UpdateFlagged :exec +const UpdateFeatured = `-- name: UpdateFeatured :exec UPDATE events -SET flagged = $1 +SET is_featured = $1 WHERE id = $2 ` -type UpdateFlaggedParams struct { - Flagged bool `json:"flagged"` - ID string `json:"id"` +type UpdateFeaturedParams struct { + IsFeatured bool `json:"is_featured"` + ID string `json:"id"` } -func (q *Queries) UpdateFlagged(ctx context.Context, arg UpdateFlaggedParams) error { - _, err := q.db.Exec(ctx, UpdateFlagged, arg.Flagged, arg.ID) +func (q *Queries) UpdateFeatured(ctx context.Context, arg UpdateFeaturedParams) error { + _, err := q.db.Exec(ctx, UpdateFeatured, arg.IsFeatured, arg.ID) return err } diff --git a/gen/db/models.go b/gen/db/models.go index cccf340..15f24e5 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -257,7 +257,8 @@ type Event struct { Status pgtype.Text `json:"status"` FetchedAt pgtype.Timestamp `json:"fetched_at"` Source pgtype.Text `json:"source"` - Flagged bool `json:"flagged"` + IsFeatured bool `json:"is_featured"` + IsActive bool `json:"is_active"` } type ExchangeRate struct { diff --git a/internal/domain/bet.go b/internal/domain/bet.go index a3f0c2b..53522ab 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -93,16 +93,16 @@ type CreateBetOutcomeReq struct { } type CreateBetReq struct { - Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"` - Amount float32 `json:"amount" validate:"required,gt=0" example:"100.0"` - BranchID *int64 `json:"branch_id,omitempty" validate:"required" example:"1"` + Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"` + Amount float32 `json:"amount" validate:"required,gt=0" example:"100.0"` + BranchID *int64 `json:"branch_id,omitempty" example:"1"` } -type CreateBetWithFastCodeReq struct { +type CreateBetWithFastCodeReq struct { FastCode string `json:"fast_code"` Amount float32 `json:"amount"` BranchID *int64 `json:"branch_id"` -} +} type RandomBetReq struct { BranchID int64 `json:"branch_id" validate:"required" example:"1"` @@ -117,6 +117,7 @@ type CreateBetRes struct { UserID int64 `json:"user_id" example:"2"` IsShopBet bool `json:"is_shop_bet" example:"false"` CreatedNumber int64 `json:"created_number" example:"2"` + FastCode string `json:"fast_code"` } type BetRes struct { ID int64 `json:"id" example:"1"` @@ -140,6 +141,8 @@ func ConvertCreateBet(bet Bet, createdNumber int64) CreateBetRes { Status: bet.Status, UserID: bet.UserID, CreatedNumber: createdNumber, + IsShopBet: bet.IsShopBet, + FastCode: bet.FastCode, } } diff --git a/internal/domain/branch.go b/internal/domain/branch.go index 8064410..ded005f 100644 --- a/internal/domain/branch.go +++ b/internal/domain/branch.go @@ -1,14 +1,20 @@ package domain +import ( + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/jackc/pgx/v5/pgtype" +) + type Branch struct { - ID int64 - Name string - Location string - WalletID int64 - BranchManagerID int64 - CompanyID int64 - IsActive bool - IsSelfOwned bool + ID int64 + Name string + Location string + WalletID int64 + BranchManagerID int64 + CompanyID int64 + IsActive bool + IsSelfOwned bool + ProfitPercentage float32 } type BranchLocation struct { @@ -38,6 +44,7 @@ type BranchDetail struct { ManagerName string ManagerPhoneNumber string WalletIsActive bool + ProfitPercentage float32 } type SupportedOperation struct { @@ -53,22 +60,24 @@ type BranchOperation struct { } type CreateBranch struct { - Name string - Location string - WalletID int64 - BranchManagerID int64 - CompanyID int64 - IsSelfOwned bool + Name string + Location string + WalletID int64 + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool + ProfitPercentage float32 } type UpdateBranch struct { - ID int64 - Name *string - Location *string - BranchManagerID *int64 - CompanyID *int64 - IsSelfOwned *bool - IsActive *bool + ID int64 + Name *string + Location *string + BranchManagerID *int64 + CompanyID *int64 + IsSelfOwned *bool + IsActive *bool + ProfitPercentage *float32 } type CreateSupportedOperation struct { @@ -81,21 +90,23 @@ type CreateBranchOperation struct { } type CreateBranchReq struct { - Name string `json:"name" validate:"required,min=3,max=100" example:"4-kilo Branch"` - Location string `json:"location" validate:"required,min=3,max=100" example:"Addis Ababa"` - BranchManagerID int64 `json:"branch_manager_id" validate:"required,gt=0" example:"1"` - CompanyID *int64 `json:"company_id,omitempty" example:"1"` - IsSelfOwned *bool `json:"is_self_owned,omitempty" example:"false"` - Operations []int64 `json:"operations" validate:"required,dive,gt=0"` + Name string `json:"name" validate:"required,min=3,max=100" example:"4-kilo Branch"` + Location string `json:"location" validate:"required,min=3,max=100" example:"Addis Ababa"` + BranchManagerID int64 `json:"branch_manager_id" validate:"required,gt=0" example:"1"` + ProfitPercentage float32 `json:"profit_percentage" example:"0.1" validate:"lt=1" ` + CompanyID *int64 `json:"company_id,omitempty" example:"1"` + IsSelfOwned *bool `json:"is_self_owned,omitempty" example:"false"` + Operations []int64 `json:"operations" validate:"required,dive,gt=0"` } type UpdateBranchReq struct { - Name *string `json:"name,omitempty" example:"4-kilo Branch"` - Location *string `json:"location,omitempty" example:"Addis Ababa"` - BranchManagerID *int64 `json:"branch_manager_id,omitempty" example:"1"` - CompanyID *int64 `json:"company_id,omitempty" example:"1"` - IsSelfOwned *bool `json:"is_self_owned,omitempty" example:"false"` - IsActive *bool `json:"is_active,omitempty" example:"false"` + Name *string `json:"name,omitempty" example:"4-kilo Branch"` + Location *string `json:"location,omitempty" example:"Addis Ababa"` + BranchManagerID *int64 `json:"branch_manager_id,omitempty" example:"1"` + CompanyID *int64 `json:"company_id,omitempty" example:"1"` + IsSelfOwned *bool `json:"is_self_owned,omitempty" example:"false"` + IsActive *bool `json:"is_active,omitempty" example:"false"` + ProfitPercentage *float32 `json:"profit_percentage,omitempty" example:"0.1" validate:"lt=1" ` } type CreateSupportedOperationReq struct { @@ -120,14 +131,15 @@ type BranchOperationRes struct { } type BranchRes struct { - ID int64 `json:"id" example:"1"` - Name string `json:"name" example:"4-kilo Branch"` - Location string `json:"location" example:"Addis Ababa"` - WalletID int64 `json:"wallet_id" example:"1"` - BranchManagerID int64 `json:"branch_manager_id" example:"1"` - CompanyID int64 `json:"company_id" example:"1"` - IsSelfOwned bool `json:"is_self_owned" example:"false"` - IsActive bool `json:"is_active" example:"false"` + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"4-kilo Branch"` + Location string `json:"location" example:"Addis Ababa"` + WalletID int64 `json:"wallet_id" example:"1"` + BranchManagerID int64 `json:"branch_manager_id" example:"1"` + CompanyID int64 `json:"company_id" example:"1"` + IsSelfOwned bool `json:"is_self_owned" example:"false"` + IsActive bool `json:"is_active" example:"false"` + ProfitPercentage float32 `json:"profit_percentage" example:"0.1"` } type BranchDetailRes struct { @@ -143,18 +155,20 @@ type BranchDetailRes struct { Balance float32 `json:"balance" example:"100.5"` IsActive bool `json:"is_active" example:"false"` WalletIsActive bool `json:"is_wallet_active" example:"false"` + ProfitPercentage float32 `json:"profit_percentage" example:"0.1"` } func ConvertBranch(branch Branch) BranchRes { return BranchRes{ - ID: branch.ID, - Name: branch.Name, - Location: branch.Location, - WalletID: branch.WalletID, - BranchManagerID: branch.BranchManagerID, - CompanyID: branch.CompanyID, - IsSelfOwned: branch.IsSelfOwned, - IsActive: branch.IsActive, + ID: branch.ID, + Name: branch.Name, + Location: branch.Location, + WalletID: branch.WalletID, + BranchManagerID: branch.BranchManagerID, + CompanyID: branch.CompanyID, + IsSelfOwned: branch.IsSelfOwned, + IsActive: branch.IsActive, + ProfitPercentage: branch.ProfitPercentage, } } @@ -172,5 +186,103 @@ func ConvertBranchDetail(branch BranchDetail) BranchDetailRes { Balance: branch.Balance.Float32(), IsActive: branch.IsActive, WalletIsActive: branch.WalletIsActive, + ProfitPercentage: branch.ProfitPercentage, } } + +func ConvertCreateBranch(branch CreateBranch) dbgen.CreateBranchParams { + return dbgen.CreateBranchParams{ + Name: branch.Name, + Location: branch.Location, + WalletID: branch.WalletID, + BranchManagerID: branch.BranchManagerID, + CompanyID: branch.CompanyID, + IsSelfOwned: branch.IsSelfOwned, + ProfitPercent: branch.ProfitPercentage, + } +} + +func ConvertDBBranchDetail(dbBranch dbgen.BranchDetail) BranchDetail { + return BranchDetail{ + ID: dbBranch.ID, + Name: dbBranch.Name, + Location: dbBranch.Location, + WalletID: dbBranch.WalletID, + BranchManagerID: dbBranch.BranchManagerID, + CompanyID: dbBranch.CompanyID, + IsSelfOwned: dbBranch.IsSelfOwned, + ManagerName: dbBranch.ManagerName.(string), + ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String, + Balance: Currency(dbBranch.Balance.Int64), + IsActive: dbBranch.IsActive, + WalletIsActive: dbBranch.WalletIsActive.Bool, + ProfitPercentage: dbBranch.ProfitPercent, + } +} + +func ConvertDBBranch(dbBranch dbgen.Branch) Branch { + return Branch{ + ID: dbBranch.ID, + Name: dbBranch.Name, + Location: dbBranch.Location, + WalletID: dbBranch.WalletID, + BranchManagerID: dbBranch.BranchManagerID, + CompanyID: dbBranch.CompanyID, + IsSelfOwned: dbBranch.IsSelfOwned, + IsActive: dbBranch.IsActive, + ProfitPercentage: dbBranch.ProfitPercent, + } +} + +func ConvertUpdateBranch(updateBranch UpdateBranch) dbgen.UpdateBranchParams { + + var newUpdateBranch dbgen.UpdateBranchParams + + newUpdateBranch.ID = updateBranch.ID + + if updateBranch.Name != nil { + newUpdateBranch.Name = pgtype.Text{ + String: *updateBranch.Name, + Valid: true, + } + } + if updateBranch.Location != nil { + newUpdateBranch.Location = pgtype.Text{ + String: *updateBranch.Location, + Valid: true, + } + } + if updateBranch.BranchManagerID != nil { + newUpdateBranch.BranchManagerID = pgtype.Int8{ + Int64: *updateBranch.BranchManagerID, + Valid: true, + } + } + if updateBranch.CompanyID != nil { + newUpdateBranch.CompanyID = pgtype.Int8{ + Int64: *updateBranch.CompanyID, + Valid: true, + } + } + if updateBranch.IsSelfOwned != nil { + newUpdateBranch.IsSelfOwned = pgtype.Bool{ + Bool: *updateBranch.IsSelfOwned, + Valid: true, + } + } + if updateBranch.IsActive != nil { + newUpdateBranch.IsActive = pgtype.Bool{ + Bool: *updateBranch.IsActive, + Valid: true, + } + } + + if updateBranch.ProfitPercentage != nil { + newUpdateBranch.ProfitPercent = pgtype.Float4{ + Float32: *updateBranch.ProfitPercentage, + Valid: true, + } + } + + return newUpdateBranch +} diff --git a/internal/domain/event.go b/internal/domain/event.go index 431d998..7e4e43f 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -101,7 +101,8 @@ type UpcomingEvent struct { StartTime time.Time `json:"start_time"` // Converted from "time" field in UNIX format Source string `json:"source"` // bet api provider (bet365, betfair) Status EventStatus `json:"status"` //Match Status for event - Flagged bool `json:"flagged"` //Whether the event is flagged or not + IsFeatured bool `json:"is_featured"` //Whether the event is featured or not + IsActive bool `json:"is_active"` //Whether the event is featured or not } type MatchResult struct { EventID string @@ -120,6 +121,7 @@ type Odds struct { } type EventFilter struct { + Query ValidString SportID ValidInt32 LeagueID ValidInt32 CountryCode ValidString @@ -128,5 +130,5 @@ type EventFilter struct { Limit ValidInt64 Offset ValidInt64 MatchStatus ValidString // e.g., "upcoming", "in_play", "ended" - Flagged ValidBool + Featured ValidBool } diff --git a/internal/repository/branch.go b/internal/repository/branch.go index b816e05..f7a4f7a 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -9,100 +9,15 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func convertCreateBranch(branch domain.CreateBranch) dbgen.CreateBranchParams { - return dbgen.CreateBranchParams{ - Name: branch.Name, - Location: branch.Location, - WalletID: branch.WalletID, - BranchManagerID: branch.BranchManagerID, - CompanyID: branch.CompanyID, - IsSelfOwned: branch.IsSelfOwned, - } -} - -func convertDBBranchDetail(dbBranch dbgen.BranchDetail) domain.BranchDetail { - return domain.BranchDetail{ - ID: dbBranch.ID, - Name: dbBranch.Name, - Location: dbBranch.Location, - WalletID: dbBranch.WalletID, - BranchManagerID: dbBranch.BranchManagerID, - CompanyID: dbBranch.CompanyID, - IsSelfOwned: dbBranch.IsSelfOwned, - ManagerName: dbBranch.ManagerName.(string), - ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String, - Balance: domain.Currency(dbBranch.Balance.Int64), - IsActive: dbBranch.IsActive, - WalletIsActive: dbBranch.WalletIsActive.Bool, - } -} - -func convertDBBranch(dbBranch dbgen.Branch) domain.Branch { - return domain.Branch{ - ID: dbBranch.ID, - Name: dbBranch.Name, - Location: dbBranch.Location, - WalletID: dbBranch.WalletID, - BranchManagerID: dbBranch.BranchManagerID, - CompanyID: dbBranch.CompanyID, - IsSelfOwned: dbBranch.IsSelfOwned, - } -} - -func convertUpdateBranch(updateBranch domain.UpdateBranch) dbgen.UpdateBranchParams { - - var newUpdateBranch dbgen.UpdateBranchParams - - newUpdateBranch.ID = updateBranch.ID - - if updateBranch.Name != nil { - newUpdateBranch.Name = pgtype.Text{ - String: *updateBranch.Name, - Valid: true, - } - } - if updateBranch.Location != nil { - newUpdateBranch.Location = pgtype.Text{ - String: *updateBranch.Location, - Valid: true, - } - } - if updateBranch.BranchManagerID != nil { - newUpdateBranch.BranchManagerID = pgtype.Int8{ - Int64: *updateBranch.BranchManagerID, - Valid: true, - } - } - if updateBranch.CompanyID != nil { - newUpdateBranch.CompanyID = pgtype.Int8{ - Int64: *updateBranch.CompanyID, - Valid: true, - } - } - if updateBranch.IsSelfOwned != nil { - newUpdateBranch.IsSelfOwned = pgtype.Bool{ - Bool: *updateBranch.IsSelfOwned, - Valid: true, - } - } - if updateBranch.IsActive != nil { - newUpdateBranch.IsActive = pgtype.Bool{ - Bool: *updateBranch.IsActive, - Valid: true, - } - } - - return newUpdateBranch -} func (s *Store) CreateBranch(ctx context.Context, branch domain.CreateBranch) (domain.Branch, error) { - dbBranch, err := s.queries.CreateBranch(ctx, convertCreateBranch(branch)) + dbBranch, err := s.queries.CreateBranch(ctx, domain.ConvertCreateBranch(branch)) if err != nil { return domain.Branch{}, err } - return convertDBBranch(dbBranch), nil + return domain.ConvertDBBranch(dbBranch), nil } func (s *Store) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) { @@ -110,7 +25,7 @@ func (s *Store) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetai if err != nil { return domain.BranchDetail{}, err } - return convertDBBranchDetail(dbBranch), nil + return domain.ConvertDBBranchDetail(dbBranch), nil } func (s *Store) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) { @@ -120,7 +35,7 @@ func (s *Store) GetBranchByManagerID(ctx context.Context, branchManagerID int64) } var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) for _, dbBranch := range dbBranches { - branches = append(branches, convertDBBranchDetail(dbBranch)) + branches = append(branches, domain.ConvertDBBranchDetail(dbBranch)) } return branches, nil } @@ -131,7 +46,7 @@ func (s *Store) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]do } var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) for _, dbBranch := range dbBranches { - branches = append(branches, convertDBBranchDetail(dbBranch)) + branches = append(branches, domain.ConvertDBBranchDetail(dbBranch)) } return branches, nil } @@ -164,7 +79,7 @@ func (s *Store) GetAllBranches(ctx context.Context, filter domain.BranchFilter) } var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) for _, dbBranch := range dbBranches { - branches = append(branches, convertDBBranchDetail(dbBranch)) + branches = append(branches, domain.ConvertDBBranchDetail(dbBranch)) } return branches, nil } @@ -177,18 +92,18 @@ func (s *Store) SearchBranchByName(ctx context.Context, name string) ([]domain.B var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) for _, dbBranch := range dbBranches { - branches = append(branches, convertDBBranchDetail(dbBranch)) + branches = append(branches, domain.ConvertDBBranchDetail(dbBranch)) } return branches, nil } func (s *Store) UpdateBranch(ctx context.Context, branch domain.UpdateBranch) (domain.Branch, error) { - dbBranch, err := s.queries.UpdateBranch(ctx, convertUpdateBranch(branch)) + dbBranch, err := s.queries.UpdateBranch(ctx, domain.ConvertUpdateBranch(branch)) if err != nil { return domain.Branch{}, err } - return convertDBBranch(dbBranch), nil + return domain.ConvertDBBranch(dbBranch), nil } func (s *Store) DeleteBranch(ctx context.Context, id int64) error { @@ -272,7 +187,7 @@ func (s *Store) GetBranchByCashier(ctx context.Context, userID int64) (domain.Br return domain.Branch{}, err } - return convertDBBranch(branch), err + return domain.ConvertDBBranch(branch), err } func (s *Store) DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error { diff --git a/internal/repository/event.go b/internal/repository/event.go index 0d266b3..58951ad 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -89,7 +89,7 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven StartTime: e.StartTime.Time.UTC(), Source: e.Source.String, Status: domain.EventStatus(e.Status.String), - Flagged: e.Flagged, + IsFeatured: e.IsFeatured, } } return upcomingEvents, nil @@ -122,7 +122,8 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.Even StartTime: e.StartTime.Time.UTC(), Source: e.Source.String, Status: domain.EventStatus(e.Status.String), - Flagged: e.Flagged, + IsFeatured: e.IsFeatured, + IsActive: e.IsActive, } } return upcomingEvents, nil @@ -139,6 +140,10 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev Int32: int32(filter.SportID.Value), Valid: filter.SportID.Valid, }, + Query: pgtype.Text{ + String: filter.Query.Value, + Valid: filter.Query.Valid, + }, Limit: pgtype.Int4{ Int32: int32(filter.Limit.Value), Valid: filter.Limit.Valid, @@ -159,9 +164,9 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev String: filter.CountryCode.Value, Valid: filter.CountryCode.Valid, }, - Flagged: pgtype.Bool{ - Bool: filter.Flagged.Valid, - Valid: filter.Flagged.Valid, + IsFeatured: pgtype.Bool{ + Bool: filter.Featured.Valid, + Valid: filter.Featured.Valid, }, }) @@ -186,7 +191,8 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev StartTime: e.StartTime.Time.UTC(), Source: e.Source.String, Status: domain.EventStatus(e.Status.String), - Flagged: e.Flagged, + IsFeatured: e.IsFeatured, + IsActive: e.IsActive, } } totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ @@ -198,6 +204,10 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev Int32: int32(filter.SportID.Value), Valid: filter.SportID.Valid, }, + Query: pgtype.Text{ + String: filter.Query.Value, + Valid: filter.Query.Valid, + }, FirstStartTime: pgtype.Timestamp{ Time: filter.FirstStartTime.Value.UTC(), Valid: filter.FirstStartTime.Valid, @@ -210,9 +220,9 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev String: filter.CountryCode.Value, Valid: filter.CountryCode.Valid, }, - Flagged: pgtype.Bool{ - Bool: filter.Flagged.Valid, - Valid: filter.Flagged.Valid, + IsFeatured: pgtype.Bool{ + Bool: filter.Featured.Valid, + Valid: filter.Featured.Valid, }, }) if err != nil { @@ -244,7 +254,7 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc StartTime: event.StartTime.Time.UTC(), Source: event.Source.String, Status: domain.EventStatus(event.Status.String), - Flagged: event.Flagged, + IsFeatured: event.IsFeatured, }, nil } func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { @@ -280,10 +290,10 @@ func (s *Store) UpdateEventStatus(ctx context.Context, eventID string, status do } -func (s *Store) UpdateFlagged(ctx context.Context, eventID string, flagged bool) error { - return s.queries.UpdateFlagged(ctx, dbgen.UpdateFlaggedParams{ +func (s *Store) UpdateFeatured(ctx context.Context, eventID string, isFeatured bool) error { + return s.queries.UpdateFeatured(ctx, dbgen.UpdateFeaturedParams{ ID: eventID, - Flagged: flagged, + IsFeatured: isFeatured, }) } diff --git a/internal/services/event/port.go b/internal/services/event/port.go index fafb8e8..c95b516 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -16,5 +16,5 @@ type Service interface { // GetAndStoreMatchResult(ctx context.Context, eventID string) error UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error - UpdateFlagged(ctx context.Context, eventID string, flagged bool) error + UpdateFeatured(ctx context.Context, eventID string, flagged bool) error } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 7315b45..23ccc9b 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -369,8 +369,8 @@ func (s *service) UpdateEventStatus(ctx context.Context, eventID string, status return s.store.UpdateEventStatus(ctx, eventID, status) } -func (s *service) UpdateFlagged(ctx context.Context, eventID string, flagged bool) error { - return s.store.UpdateFlagged(ctx, eventID, flagged) +func (s *service) UpdateFeatured(ctx context.Context, eventID string, flagged bool) error { + return s.store.UpdateFeatured(ctx, eventID, flagged) } // func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error { diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 9507669..46ef873 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -37,7 +38,7 @@ type loginCustomerRes struct { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /api/v1/auth/login [post] +// @Router /api/v1/auth/customer-login [post] func (h *Handler) LoginCustomer(c *fiber.Ctx) error { var req loginCustomerReq if err := c.BodyParser(&req); err != nil { @@ -59,7 +60,6 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error { successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password) if err != nil { - switch { case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", @@ -89,6 +89,133 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error { } } + if successRes.Role != domain.RoleCustomer { + h.mongoLoggerSvc.Info("Login attempt: customer login of other role", + zap.Int("status_code", fiber.StatusForbidden), + zap.String("role", string(successRes.Role)), + zap.String("email", req.Email), + zap.String("phone", req.PhoneNumber), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusForbidden, "Only customers are allowed to login ") + } + + accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) + if err != nil { + h.mongoLoggerSvc.Error("Failed to create access token", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Int64("user_id", successRes.UserId), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token") + } + + res := loginCustomerRes{ + AccessToken: accessToken, + RefreshToken: successRes.RfToken, + Role: string(successRes.Role), + } + + h.mongoLoggerSvc.Info("Login successful", + zap.Int("status_code", fiber.StatusOK), + zap.Int64("user_id", successRes.UserId), + zap.String("role", string(successRes.Role)), + zap.Time("timestamp", time.Now()), + ) + + return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) +} + +// loginAdminReq represents the request body for the LoginAdmin endpoint. +type loginAdminReq struct { + Email string `json:"email" validate:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` + Password string `json:"password" validate:"required" example:"password123"` +} + +// loginAdminRes represents the response body for the LoginAdmin endpoint. +type loginAdminRes struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + Role string `json:"role"` +} + +// LoginAdmin godoc +// @Summary Login customer +// @Description Login customer +// @Tags auth +// @Accept json +// @Produce json +// @Param login body loginAdminReq true "Login admin" +// @Success 200 {object} loginAdminRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/auth/admin-login [post] +func (h *Handler) LoginAdmin(c *fiber.Ctx) error { + var req loginAdminReq + if err := c.BodyParser(&req); err != nil { + h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request", + zap.Int("status_code", fiber.StatusBadRequest), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body"+err.Error()) + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password) + if err != nil { + switch { + case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound): + h.mongoLoggerSvc.Info("Login attempt failed: Invalid credentials", + zap.Int("status_code", fiber.StatusUnauthorized), + zap.String("email", req.Email), + zap.String("phone", req.PhoneNumber), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials") + case errors.Is(err, authentication.ErrUserSuspended): + h.mongoLoggerSvc.Info("Login attempt failed: User login has been locked", + zap.Int("status_code", fiber.StatusUnauthorized), + zap.String("email", req.Email), + zap.String("phone", req.PhoneNumber), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked") + default: + h.mongoLoggerSvc.Error("Login failed", + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Internal server error") + } + } + + if successRes.Role == domain.RoleCustomer { + h.mongoLoggerSvc.Warn("Login attempt: admin login of customer", + zap.Int("status_code", fiber.StatusForbidden), + zap.String("role", string(successRes.Role)), + zap.String("email", req.Email), + zap.String("phone", req.PhoneNumber), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusForbidden, "Only admin roles are allowed") + } + accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) if err != nil { h.mongoLoggerSvc.Error("Failed to create access token", diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 0c335a6..a323560 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -45,6 +45,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("user_id", userID), zap.String("role", string(role)), + zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create bet:"+err.Error()) @@ -97,6 +98,15 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "failed to get bet with fast code:"+err.Error()) } + if bet.UserID == userID { + h.mongoLoggerSvc.Info("User cannot refer himself", + zap.Int64("bet_id", bet.ID), + zap.Int("status_code", fiber.StatusBadRequest), + zap.Time("timestamp", time.Now()), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "User cannot use his own referral code") + } outcomes, err := h.betSvc.GetBetOutcomeByBetID(c.Context(), bet.ID) if err != nil { h.mongoLoggerSvc.Info("failed to get BetOutcomes by BetID", @@ -118,7 +128,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { } // This can be for both online and offline bets - // If bet is an online bet (if the customer role creates the bet on their own) + // If bet is an online bet (if the customer role creates the bet on their own) // then the branchID is null newReq := domain.CreateBetReq{ Amount: req.Amount, @@ -190,7 +200,7 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI zap.Error(err), zap.Time("timestamp", time.Now()), ) - return domain.CreateBetRes{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) + return domain.CreateBetRes{}, err } h.mongoLoggerSvc.Error("PlaceBet failed", @@ -202,7 +212,7 @@ func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userI zap.Time("timestamp", time.Now()), ) - return domain.CreateBetRes{}, fiber.NewError(fiber.StatusInternalServerError, "Unable to create bet") + return domain.CreateBetRes{}, err } return res, nil @@ -490,6 +500,42 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) } +// GetBetByFastCode godoc +// @Summary Gets bet by fast_code +// @Description Gets a single bet by fast_code +// @Tags bet +// @Accept json +// @Produce json +// @Param fast_code path int true "Bet ID" +// @Success 200 {object} domain.BetRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /api/v1/sport/bet/fastcode/{fast_code} [get] +func (h *Handler) GetBetByFastCode(c *fiber.Ctx) error { + fastCode := c.Params("fast_code") + + bet, err := h.betSvc.GetBetByFastCode(c.Context(), fastCode) + if err != nil { + h.mongoLoggerSvc.Info("Failed to get bet by fast code", + zap.String("fast_code", fastCode), + zap.Int("status_code", fiber.StatusNotFound), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusNotFound, "Failed to find bet by fast code") + } + + res := domain.ConvertBet(bet) + + // h.mongoLoggerSvc.Info("Bet retrieved successfully", + // zap.Int64("betID", id), + // zap.Int("status_code", fiber.StatusOK), + // zap.Time("timestamp", time.Now()), + // ) + + return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) +} + type UpdateCashOutReq struct { CashedOut bool } diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index 2695332..9fa7940 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -74,6 +74,13 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { Valid: true, } } + + searchQuery := c.Query("query") + searchString := domain.ValidString{ + Value: searchQuery, + Valid: searchQuery != "", + } + firstStartTimeQuery := c.Query("first_start_time") var firstStartTime domain.ValidTime if firstStartTimeQuery != "" { @@ -98,7 +105,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { if lastStartTimeQuery != "" { lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) if err != nil { - h.mongoLoggerSvc.Info("invalid start_time format", + h.mongoLoggerSvc.Info("invalid last_start_time format", zap.String("last_start_time", lastStartTimeQuery), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), @@ -118,12 +125,12 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { Valid: countryCodeQuery != "", } - flaggedQuery := c.Query("flagged") - var flagged domain.ValidBool - if flaggedQuery != "" { - flaggedParsed, err := strconv.ParseBool(flaggedQuery) + isFeaturedQuery := c.Query("is_featured") + var isFeatured domain.ValidBool + if isFeaturedQuery != "" { + isFeaturedParsed, err := strconv.ParseBool(isFeaturedQuery) if err != nil { - h.mongoLoggerSvc.Error("Failed to parse flagged", + h.mongoLoggerSvc.Error("Failed to parse isFeatured", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), @@ -131,8 +138,8 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet") } - flagged = domain.ValidBool{ - Value: flaggedParsed, + isFeatured = domain.ValidBool{ + Value: isFeaturedParsed, Valid: true, } } @@ -141,12 +148,13 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { c.Context(), domain.EventFilter{ SportID: sportID, LeagueID: leagueID, + Query: searchString, FirstStartTime: firstStartTime, LastStartTime: lastStartTime, Limit: limit, Offset: offset, CountryCode: countryCode, - Flagged: flagged, + Featured: isFeatured, }) // fmt.Printf("League ID: %v", leagueID) @@ -299,13 +307,13 @@ func (h *Handler) SetEventStatusToRemoved(c *fiber.Ctx) error { } -type UpdateEventFlaggedReq struct { - Flagged bool `json:"flagged" example:"true"` +type UpdateEventFeaturedReq struct { + Featured bool `json:"is_featured" example:"true"` } -// UpdateEventFlagged godoc -// @Summary update the event flagged -// @Description Update the event flagged +// UpdateEventFeatured godoc +// @Summary update the event featured +// @Description Update the event featured // @Tags event // @Accept json // @Produce json @@ -314,10 +322,10 @@ type UpdateEventFlaggedReq struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/events/{id}/flag [put] -func (h *Handler) UpdateEventFlagged(c *fiber.Ctx) error { +func (h *Handler) UpdateEventFeatured(c *fiber.Ctx) error { eventID := c.Params("id") - var req UpdateEventFlaggedReq + var req UpdateEventFeaturedReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse user id", @@ -335,17 +343,17 @@ func (h *Handler) UpdateEventFlagged(c *fiber.Ctx) error { for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } - h.mongoLoggerSvc.Error("Failed to update event flagged", + h.mongoLoggerSvc.Error("Failed to update event featured", zap.Any("request", req), zap.Int("status_code", fiber.StatusInternalServerError), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } - err := h.eventSvc.UpdateFlagged(c.Context(), eventID, req.Flagged) + err := h.eventSvc.UpdateFeatured(c.Context(), eventID, req.Featured) if err != nil { - h.mongoLoggerSvc.Error("Failed to update event flagged", + h.mongoLoggerSvc.Error("Failed to update event featured", zap.String("eventID", eventID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index ba725df..232490b 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -69,7 +69,8 @@ func (a *App) initAppRoutes() { }) }) // Auth Routes - groupV1.Post("/auth/login", h.LoginCustomer) + groupV1.Post("/auth/customer-login", h.LoginCustomer) + groupV1.Post("/auth/admin-login", h.LoginAdmin) groupV1.Post("/auth/refresh", h.RefreshToken) groupV1.Post("/auth/logout", a.authMiddleware, h.LogOutCustomer) groupV1.Get("/auth/test", a.authMiddleware, func(c *fiber.Ctx) error { @@ -153,7 +154,7 @@ func (a *App) initAppRoutes() { groupV1.Get("/events/:id", h.GetUpcomingEventByID) groupV1.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) groupV1.Get("/top-leagues", h.GetTopLeagues) - groupV1.Get("/events/:id/flag", h.UpdateEventFlagged) + groupV1.Put("/events/:id/featured", h.UpdateEventFeatured) // Leagues groupV1.Get("/leagues", h.GetAllLeagues) @@ -161,7 +162,7 @@ func (a *App) initAppRoutes() { groupV1.Put("/leagues/:id/featured", h.SetLeagueFeatured) groupV1.Get("/result/:id", h.GetResultsByEventID) - + // Branch groupV1.Post("/branch", a.authMiddleware, h.CreateBranch) groupV1.Get("/branch", a.authMiddleware, h.GetAllBranches) @@ -171,10 +172,10 @@ func (a *App) initAppRoutes() { groupV1.Put("/branch/:id/set-active", a.authMiddleware, h.UpdateBranchStatus) groupV1.Put("/branch/:id/set-inactive", a.authMiddleware, h.UpdateBranchStatus) groupV1.Delete("/branch/:id", a.authMiddleware, h.DeleteBranch) - + groupV1.Get("/search/branch", a.authMiddleware, h.SearchBranch) groupV1.Get("/branchLocation", a.authMiddleware, h.GetAllBranchLocations) - + groupV1.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers) groupV1.Get("/branchCashier", a.authMiddleware, h.GetBranchForCashier) @@ -204,6 +205,7 @@ func (a *App) initAppRoutes() { // Bet Routes groupV1.Post("/sport/bet", a.authMiddleware, h.CreateBet) groupV1.Post("/sport/bet/fastcode", a.authMiddleware, h.CreateBetWithFastCode) + groupV1.Get("/sport/bet/fastcode/:fast_code", h.GetBetByFastCode) groupV1.Get("/sport/bet", a.authMiddleware, h.GetAllBet) groupV1.Get("/sport/bet/:id", h.GetBetByID) groupV1.Patch("/sport/bet/:id", a.authMiddleware, h.UpdateCashOut)