diff --git a/cmd/main.go b/cmd/main.go index 0484d3c..899b830 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -119,8 +119,8 @@ func main() { branchSvc := branch.NewService(store) companySvc := company.NewService(store) leagueSvc := league.New(store) - betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger) - resultSvc := result.NewService(store, cfg, logger, *betSvc) + betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger) + resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) @@ -177,8 +177,7 @@ func main() { ) walletMonitorSvc.Start() - // Start other cron jobs - httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc) + httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc) httpserver.StartTicketCrons(*ticketSvc) go httpserver.SetupReportCronJob(reportWorker) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index d9cbf8f..c43a7b9 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -65,6 +65,7 @@ CREATE TABLE IF NOT EXISTS tickets ( id BIGSERIAL PRIMARY KEY, amount BIGINT NOT NULL, total_odds REAL NOT NULL, + IP VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -240,6 +241,7 @@ CREATE TABLE leagues ( name TEXT NOT NULL, country_code TEXT, bet365_id INT, + sport_id INT NOT NULL, is_active BOOLEAN DEFAULT true ); CREATE TABLE teams ( @@ -265,9 +267,11 @@ FROM companies CREATE VIEW branch_details AS SELECT branches.*, CONCAT(users.first_name, ' ', users.last_name) AS manager_name, - users.phone_number AS manager_phone_number + users.phone_number AS manager_phone_number, + wallets.balance FROM branches - LEFT JOIN users ON branches.branch_manager_id = users.id; + LEFT JOIN users ON branches.branch_manager_id = users.id + LEFT JOin wallets ON wallets.id = branches.wallet_id; CREATE TABLE IF NOT EXISTS supported_operations ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, diff --git a/db/query/cashier.sql b/db/query/cashier.sql index dcb8dfb..ad885a0 100644 --- a/db/query/cashier.sql +++ b/db/query/cashier.sql @@ -2,14 +2,24 @@ SELECT users.* FROM branch_cashiers JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id WHERE branch_cashiers.branch_id = $1; -- name: GetAllCashiers :many SELECT users.*, - branch_id + branch_id, + branches.name AS branch_name, + branches.wallet_id AS branch_wallet, + branches.location As branch_location FROM branch_cashiers - JOIN users ON branch_cashiers.user_id = users.id; + JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id; -- name: GetCashierByID :one SELECT users.*, - branch_id + branch_id, + branches.name AS branch_name, + branches.wallet_id AS branch_wallet, + branches.location As branch_location FROM branch_cashiers - JOIN users ON branch_cashiers.user_id = $1; \ No newline at end of file + JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id +WHERE users.id = $1; \ No newline at end of file diff --git a/db/query/events.sql b/db/query/events.sql index e470aee..6056738 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -144,33 +144,40 @@ SELECT id, source, fetched_at FROM events -WHERE is_live = false +WHERE start_time > now() + AND is_live = false AND status = 'upcoming' ORDER BY start_time ASC; -- name: GetExpiredUpcomingEvents :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, - is_live, - status, - source, - fetched_at +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.start_time, + events.is_live, + events.status, + events.source, + events.fetched_at, + leagues.country_code as league_cc FROM events + LEFT JOIN leagues ON leagues.id = league_id WHERE start_time < now() + and ( + status = sqlc.narg('status') + OR sqlc.narg('status') IS NULL + ) ORDER BY start_time ASC; -- name: GetTotalEvents :one SELECT COUNT(*) FROM events + LEFT JOIN leagues ON leagues.id = league_id WHERE is_live = false AND status = 'upcoming' AND ( @@ -178,7 +185,7 @@ WHERE is_live = false OR sqlc.narg('league_id') IS NULL ) AND ( - sport_id = sqlc.narg('sport_id') + events.sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) AND ( @@ -188,34 +195,40 @@ WHERE is_live = false AND ( start_time > sqlc.narg('first_start_time') OR sqlc.narg('first_start_time') IS NULL + ) + AND ( + leagues.country_code = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL ); -- name: GetPaginatedUpcomingEvents :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, - is_live, - status, - source, - fetched_at +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.start_time, + events.is_live, + events.status, + events.source, + events.fetched_at, + leagues.country_code as league_cc FROM events -WHERE is_live = false + LEFT JOIN leagues ON leagues.id = league_id +WHERE start_time > now() + AND is_live = false AND status = 'upcoming' AND ( league_id = sqlc.narg('league_id') OR sqlc.narg('league_id') IS NULL ) AND ( - sport_id = sqlc.narg('sport_id') + events.sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) AND ( @@ -226,6 +239,10 @@ WHERE is_live = false start_time > sqlc.narg('first_start_time') OR sqlc.narg('first_start_time') IS NULL ) + AND ( + leagues.country_code = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL + ) ORDER BY start_time ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetUpcomingByID :one diff --git a/db/query/leagues.sql b/db/query/leagues.sql index b9c0e02..e8ee241 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -1,48 +1,64 @@ -- name: InsertLeague :exec INSERT INTO leagues ( - id, - name, - country_code, - bet365_id, - is_active -) VALUES ( - $1, $2, $3, $4, $5 -) -ON CONFLICT (id) DO UPDATE + id, + name, + country_code, + bet365_id, + sport_id, + is_active + ) +VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO +UPDATE SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, - is_active = EXCLUDED.is_active; --- name: GetSupportedLeagues :many -SELECT id, - name, - country_code, - bet365_id, - is_active -FROM leagues -WHERE is_active = true; + is_active = EXCLUDED.is_active, + sport_id = EXCLUDED.sport_id; -- name: GetAllLeagues :many SELECT id, - name, - country_code, - bet365_id, - is_active -FROM leagues; + name, + country_code, + bet365_id, + is_active, + sport_id +FROM leagues +WHERE ( + country_code = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL + ) + AND ( + sport_id = sqlc.narg('sport_id') + OR sqlc.narg('sport_id') IS NULL + ) + AND ( + is_active = sqlc.narg('is_active') + OR sqlc.narg('is_active') IS NULL + ) +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: CheckLeagueSupport :one SELECT EXISTS( - SELECT 1 - FROM leagues - WHERE id = $1 - AND is_active = true -); + SELECT 1 + FROM leagues + WHERE id = $1 + AND is_active = true + ); -- name: UpdateLeague :exec UPDATE leagues -SET name = $1, - country_code = $2, - bet365_id = $3, - is_active = $4 -WHERE id = $5; +SET name = COALESCE(sqlc.narg('name'), name), + country_code = COALESCE(sqlc.narg('country_code'), country_code), + bet365_id = COALESCE(sqlc.narg('bet365_id'), bet365_id), + is_active = COALESCE(sqlc.narg('is_active'), is_active), + sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) +WHERE id = $1; +-- name: UpdateLeagueByBet365ID :exec +UPDATE leagues +SET name = COALESCE(sqlc.narg('name'), name), + id = COALESCE(sqlc.narg('id'), id), + country_code = COALESCE(sqlc.narg('country_code'), country_code), + is_active = COALESCE(sqlc.narg('is_active'), is_active), + sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) +WHERE bet365_id = $1; -- name: SetLeagueActive :exec UPDATE leagues -SET is_active = true +SET is_active = $2 WHERE id = $1; \ No newline at end of file diff --git a/db/query/odds.sql b/db/query/odds.sql index 9de17b3..bdabb44 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -63,7 +63,7 @@ SELECT event_id, is_active FROM odds WHERE is_active = true - AND source = 'b365api'; + AND source = 'bet365'; -- name: GetALLPrematchOdds :many SELECT event_id, fi, @@ -82,7 +82,7 @@ SELECT event_id, is_active FROM odds WHERE is_active = true - AND source = 'b365api'; + AND source = 'bet365'; -- name: GetRawOddsByMarketID :one SELECT id, market_name, @@ -93,7 +93,7 @@ FROM odds WHERE market_id = $1 AND fi = $2 AND is_active = true - AND source = 'b365api'; + AND source = 'bet365'; -- name: GetPrematchOddsByUpcomingID :many SELECT o.* FROM odds o @@ -102,7 +102,7 @@ WHERE e.id = $1 AND e.is_live = false AND e.status = 'upcoming' AND o.is_active = true - AND o.source = 'b365api'; + AND o.source = 'bet365'; -- name: GetPaginatedPrematchOddsByUpcomingID :many SELECT o.* FROM odds o @@ -111,5 +111,8 @@ WHERE e.id = $1 AND e.is_live = false AND e.status = 'upcoming' AND o.is_active = true - AND o.source = 'b365api' -LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); \ No newline at end of file + AND o.source = 'bet365' +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: DeleteOddsForEvent :exec +DELETE FROM odds +Where fi = $1; \ No newline at end of file diff --git a/db/query/ticket.sql b/db/query/ticket.sql index 8e2daaf..d091f04 100644 --- a/db/query/ticket.sql +++ b/db/query/ticket.sql @@ -1,6 +1,6 @@ -- name: CreateTicket :one -INSERT INTO tickets (amount, total_odds) -VALUES ($1, $2) +INSERT INTO tickets (amount, total_odds, ip) +VALUES ($1, $2, $3) RETURNING *; -- name: CreateTicketOutcome :copyfrom INSERT INTO ticket_outcomes ( @@ -42,6 +42,10 @@ WHERE id = $1; SELECT * FROM ticket_outcomes WHERE ticket_id = $1; +-- name: CountTicketByIP :one +SELECT count(id) +FROM tickets +WHERE IP = $1; -- name: UpdateTicketOutcomeStatus :exec UPDATE ticket_outcomes SET status = $1 diff --git a/docs/docs.go b/docs/docs.go index e4bf621..d6a2230 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2850,6 +2850,53 @@ const docTemplate = `{ } } }, + "/result/{id}": { + "get": { + "description": "Get results for an event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "result" + ], + "summary": "Get results for an event", + "parameters": [ + { + "type": "string", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetOutcome" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/search/branch": { "get": { "description": "Search branches by name or location", diff --git a/docs/swagger.json b/docs/swagger.json index 527e4db..2438fb7 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2842,6 +2842,53 @@ } } }, + "/result/{id}": { + "get": { + "description": "Get results for an event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "result" + ], + "summary": "Get results for an event", + "parameters": [ + { + "type": "string", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetOutcome" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/search/branch": { "get": { "description": "Search branches by name or location", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index fb86ed5..bcdf305 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3318,6 +3318,37 @@ paths: summary: Get referral statistics tags: - referral + /result/{id}: + get: + consumes: + - application/json + description: Get results for an event + parameters: + - description: Event ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.BetOutcome' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get results for an event + tags: + - result /search/branch: get: consumes: diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 9c55b29..527f25c 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: auth.sql package dbgen diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index e4cde1d..40182ae 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: bet.sql package dbgen diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 57b8dad..d3ef2e5 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: branch.sql package dbgen @@ -155,7 +155,7 @@ func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOpe } const GetAllBranches = `-- name: GetAllBranches :many -SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance FROM branch_details ` @@ -181,6 +181,7 @@ func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) { &i.UpdatedAt, &i.ManagerName, &i.ManagerPhoneNumber, + &i.Balance, ); err != nil { return nil, err } @@ -243,7 +244,7 @@ func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch, } const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many -SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance FROM branch_details WHERE company_id = $1 ` @@ -270,6 +271,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([] &i.UpdatedAt, &i.ManagerName, &i.ManagerPhoneNumber, + &i.Balance, ); err != nil { return nil, err } @@ -282,7 +284,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([] } const GetBranchByID = `-- name: GetBranchByID :one -SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance FROM branch_details WHERE id = $1 ` @@ -303,12 +305,13 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er &i.UpdatedAt, &i.ManagerName, &i.ManagerPhoneNumber, + &i.Balance, ) return i, err } const GetBranchByManagerID = `-- name: GetBranchByManagerID :many -SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance FROM branch_details WHERE branch_manager_id = $1 ` @@ -335,6 +338,7 @@ func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int6 &i.UpdatedAt, &i.ManagerName, &i.ManagerPhoneNumber, + &i.Balance, ); err != nil { return nil, err } @@ -394,7 +398,7 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge } const SearchBranchByName = `-- name: SearchBranchByName :many -SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number +SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance FROM branch_details WHERE name ILIKE '%' || $1 || '%' ` @@ -421,6 +425,7 @@ func (q *Queries) SearchBranchByName(ctx context.Context, dollar_1 pgtype.Text) &i.UpdatedAt, &i.ManagerName, &i.ManagerPhoneNumber, + &i.Balance, ); err != nil { return nil, err } diff --git a/gen/db/cashier.sql.go b/gen/db/cashier.sql.go index bb71cb2..27a1ffb 100644 --- a/gen/db/cashier.sql.go +++ b/gen/db/cashier.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: cashier.sql package dbgen @@ -13,29 +13,36 @@ import ( const GetAllCashiers = `-- name: GetAllCashiers :many SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by, - branch_id + branch_id, + branches.name AS branch_name, + branches.wallet_id AS branch_wallet, + branches.location As branch_location FROM branch_cashiers JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id ` type GetAllCashiersRow struct { - ID int64 `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email pgtype.Text `json:"email"` - PhoneNumber pgtype.Text `json:"phone_number"` - Role string `json:"role"` - Password []byte `json:"password"` - EmailVerified bool `json:"email_verified"` - PhoneVerified bool `json:"phone_verified"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` - CompanyID pgtype.Int8 `json:"company_id"` - SuspendedAt pgtype.Timestamptz `json:"suspended_at"` - Suspended bool `json:"suspended"` - ReferralCode pgtype.Text `json:"referral_code"` - ReferredBy pgtype.Text `json:"referred_by"` - BranchID int64 `json:"branch_id"` + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email pgtype.Text `json:"email"` + PhoneNumber pgtype.Text `json:"phone_number"` + Role string `json:"role"` + Password []byte `json:"password"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + CompanyID pgtype.Int8 `json:"company_id"` + SuspendedAt pgtype.Timestamptz `json:"suspended_at"` + Suspended bool `json:"suspended"` + ReferralCode pgtype.Text `json:"referral_code"` + ReferredBy pgtype.Text `json:"referred_by"` + BranchID int64 `json:"branch_id"` + BranchName string `json:"branch_name"` + BranchWallet int64 `json:"branch_wallet"` + BranchLocation string `json:"branch_location"` } func (q *Queries) GetAllCashiers(ctx context.Context) ([]GetAllCashiersRow, error) { @@ -65,6 +72,9 @@ func (q *Queries) GetAllCashiers(ctx context.Context) ([]GetAllCashiersRow, erro &i.ReferralCode, &i.ReferredBy, &i.BranchID, + &i.BranchName, + &i.BranchWallet, + &i.BranchLocation, ); err != nil { return nil, err } @@ -78,33 +88,41 @@ func (q *Queries) GetAllCashiers(ctx context.Context) ([]GetAllCashiersRow, erro const GetCashierByID = `-- name: GetCashierByID :one SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by, - branch_id + branch_id, + branches.name AS branch_name, + branches.wallet_id AS branch_wallet, + branches.location As branch_location FROM branch_cashiers - JOIN users ON branch_cashiers.user_id = $1 + JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id +WHERE users.id = $1 ` type GetCashierByIDRow struct { - ID int64 `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email pgtype.Text `json:"email"` - PhoneNumber pgtype.Text `json:"phone_number"` - Role string `json:"role"` - Password []byte `json:"password"` - EmailVerified bool `json:"email_verified"` - PhoneVerified bool `json:"phone_verified"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` - CompanyID pgtype.Int8 `json:"company_id"` - SuspendedAt pgtype.Timestamptz `json:"suspended_at"` - Suspended bool `json:"suspended"` - ReferralCode pgtype.Text `json:"referral_code"` - ReferredBy pgtype.Text `json:"referred_by"` - BranchID int64 `json:"branch_id"` + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email pgtype.Text `json:"email"` + PhoneNumber pgtype.Text `json:"phone_number"` + Role string `json:"role"` + Password []byte `json:"password"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + CompanyID pgtype.Int8 `json:"company_id"` + SuspendedAt pgtype.Timestamptz `json:"suspended_at"` + Suspended bool `json:"suspended"` + ReferralCode pgtype.Text `json:"referral_code"` + ReferredBy pgtype.Text `json:"referred_by"` + BranchID int64 `json:"branch_id"` + BranchName string `json:"branch_name"` + BranchWallet int64 `json:"branch_wallet"` + BranchLocation string `json:"branch_location"` } -func (q *Queries) GetCashierByID(ctx context.Context, userID int64) (GetCashierByIDRow, error) { - row := q.db.QueryRow(ctx, GetCashierByID, userID) +func (q *Queries) GetCashierByID(ctx context.Context, id int64) (GetCashierByIDRow, error) { + row := q.db.QueryRow(ctx, GetCashierByID, id) var i GetCashierByIDRow err := row.Scan( &i.ID, @@ -124,6 +142,9 @@ func (q *Queries) GetCashierByID(ctx context.Context, userID int64) (GetCashierB &i.ReferralCode, &i.ReferredBy, &i.BranchID, + &i.BranchName, + &i.BranchWallet, + &i.BranchLocation, ) return i, err } @@ -132,6 +153,7 @@ const GetCashiersByBranch = `-- name: GetCashiersByBranch :many SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by FROM branch_cashiers JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id WHERE branch_cashiers.branch_id = $1 ` diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index 449c8fd..3c5a6b1 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: company.sql package dbgen diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index 1212253..900af58 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: copyfrom.go package dbgen diff --git a/gen/db/db.go b/gen/db/db.go index 84de07c..d892683 100644 --- a/gen/db/db.go +++ b/gen/db/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index a2a53d6..0ce862a 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: events.sql package dbgen @@ -40,7 +40,8 @@ SELECT id, source, fetched_at FROM events -WHERE is_live = false +WHERE start_time > now() + AND is_live = false AND status = 'upcoming' ORDER BY start_time ASC ` @@ -104,25 +105,30 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]GetAllUpcomingEve } const GetExpiredUpcomingEvents = `-- name: GetExpiredUpcomingEvents :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, - is_live, - status, - source, - fetched_at +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.start_time, + events.is_live, + events.status, + events.source, + events.fetched_at, + leagues.country_code as league_cc FROM events + LEFT JOIN leagues ON leagues.id = league_id WHERE start_time < now() + and ( + status = $1 + OR $1 IS NULL + ) ORDER BY start_time ASC ` @@ -138,16 +144,16 @@ type GetExpiredUpcomingEventsRow struct { AwayKitImage pgtype.Text `json:"away_kit_image"` LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` + LeagueCc pgtype.Text `json:"league_cc"` } -func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context) ([]GetExpiredUpcomingEventsRow, error) { - rows, err := q.db.Query(ctx, GetExpiredUpcomingEvents) +func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Text) ([]GetExpiredUpcomingEventsRow, error) { + rows, err := q.db.Query(ctx, GetExpiredUpcomingEvents, status) if err != nil { return nil, err } @@ -167,12 +173,12 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context) ([]GetExpiredUpc &i.AwayKitImage, &i.LeagueID, &i.LeagueName, - &i.LeagueCc, &i.StartTime, &i.IsLive, &i.Status, &i.Source, &i.FetchedAt, + &i.LeagueCc, ); err != nil { return nil, err } @@ -185,32 +191,34 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context) ([]GetExpiredUpc } const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :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, - is_live, - status, - source, - fetched_at +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.start_time, + events.is_live, + events.status, + events.source, + events.fetched_at, + leagues.country_code as league_cc FROM events -WHERE is_live = false + LEFT JOIN leagues ON leagues.id = league_id +WHERE start_time > now() + AND is_live = false AND status = 'upcoming' AND ( league_id = $1 OR $1 IS NULL ) AND ( - sport_id = $2 + events.sport_id = $2 OR $2 IS NULL ) AND ( @@ -221,8 +229,12 @@ WHERE is_live = false start_time > $4 OR $4 IS NULL ) + AND ( + leagues.country_code = $5 + OR $5 IS NULL + ) ORDER BY start_time ASC -LIMIT $6 OFFSET $5 +LIMIT $7 OFFSET $6 ` type GetPaginatedUpcomingEventsParams struct { @@ -230,6 +242,7 @@ type GetPaginatedUpcomingEventsParams struct { SportID pgtype.Int4 `json:"sport_id"` LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` + CountryCode pgtype.Text `json:"country_code"` Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"` } @@ -246,12 +259,12 @@ type GetPaginatedUpcomingEventsRow struct { AwayKitImage pgtype.Text `json:"away_kit_image"` LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` + LeagueCc pgtype.Text `json:"league_cc"` } func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) { @@ -260,6 +273,7 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat arg.SportID, arg.LastStartTime, arg.FirstStartTime, + arg.CountryCode, arg.Offset, arg.Limit, ) @@ -282,12 +296,12 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat &i.AwayKitImage, &i.LeagueID, &i.LeagueName, - &i.LeagueCc, &i.StartTime, &i.IsLive, &i.Status, &i.Source, &i.FetchedAt, + &i.LeagueCc, ); err != nil { return nil, err } @@ -302,6 +316,7 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat const GetTotalEvents = `-- name: GetTotalEvents :one SELECT COUNT(*) FROM events + LEFT JOIN leagues ON leagues.id = league_id WHERE is_live = false AND status = 'upcoming' AND ( @@ -309,7 +324,7 @@ WHERE is_live = false OR $1 IS NULL ) AND ( - sport_id = $2 + events.sport_id = $2 OR $2 IS NULL ) AND ( @@ -320,6 +335,10 @@ WHERE is_live = false start_time > $4 OR $4 IS NULL ) + AND ( + leagues.country_code = $5 + OR $5 IS NULL + ) ` type GetTotalEventsParams struct { @@ -327,6 +346,7 @@ type GetTotalEventsParams struct { SportID pgtype.Int4 `json:"sport_id"` LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` + CountryCode pgtype.Text `json:"country_code"` } func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) (int64, error) { @@ -335,6 +355,7 @@ func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) arg.SportID, arg.LastStartTime, arg.FirstStartTime, + arg.CountryCode, ) var count int64 err := row.Scan(&count) diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 49c1555..8762f82 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: leagues.sql package dbgen @@ -13,11 +13,11 @@ import ( const CheckLeagueSupport = `-- name: CheckLeagueSupport :one SELECT EXISTS( - SELECT 1 - FROM leagues - WHERE id = $1 - AND is_active = true -) + SELECT 1 + FROM leagues + WHERE id = $1 + AND is_active = true + ) ` func (q *Queries) CheckLeagueSupport(ctx context.Context, id int64) (bool, error) { @@ -29,64 +29,66 @@ func (q *Queries) CheckLeagueSupport(ctx context.Context, id int64) (bool, error const GetAllLeagues = `-- name: GetAllLeagues :many SELECT id, - name, - country_code, - bet365_id, - is_active + name, + country_code, + bet365_id, + is_active, + sport_id FROM leagues +WHERE ( + country_code = $1 + OR $1 IS NULL + ) + AND ( + sport_id = $2 + OR $2 IS NULL + ) + AND ( + is_active = $3 + OR $3 IS NULL + ) +LIMIT $5 OFFSET $4 ` -func (q *Queries) GetAllLeagues(ctx context.Context) ([]League, error) { - rows, err := q.db.Query(ctx, GetAllLeagues) - if err != nil { - return nil, err - } - defer rows.Close() - var items []League - for rows.Next() { - var i League - if err := rows.Scan( - &i.ID, - &i.Name, - &i.CountryCode, - &i.Bet365ID, - &i.IsActive, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil +type GetAllLeaguesParams struct { + CountryCode pgtype.Text `json:"country_code"` + SportID pgtype.Int4 `json:"sport_id"` + IsActive pgtype.Bool `json:"is_active"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` } -const GetSupportedLeagues = `-- name: GetSupportedLeagues :many -SELECT id, - name, - country_code, - bet365_id, - is_active -FROM leagues -WHERE is_active = true -` +type GetAllLeaguesRow struct { + ID int64 `json:"id"` + Name string `json:"name"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + IsActive pgtype.Bool `json:"is_active"` + SportID int32 `json:"sport_id"` +} -func (q *Queries) GetSupportedLeagues(ctx context.Context) ([]League, error) { - rows, err := q.db.Query(ctx, GetSupportedLeagues) +func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([]GetAllLeaguesRow, error) { + rows, err := q.db.Query(ctx, GetAllLeagues, + arg.CountryCode, + arg.SportID, + arg.IsActive, + arg.Offset, + arg.Limit, + ) if err != nil { return nil, err } defer rows.Close() - var items []League + var items []GetAllLeaguesRow for rows.Next() { - var i League + var i GetAllLeaguesRow if err := rows.Scan( &i.ID, &i.Name, &i.CountryCode, &i.Bet365ID, &i.IsActive, + &i.SportID, ); err != nil { return nil, err } @@ -100,19 +102,20 @@ func (q *Queries) GetSupportedLeagues(ctx context.Context) ([]League, error) { const InsertLeague = `-- name: InsertLeague :exec INSERT INTO leagues ( - id, - name, - country_code, - bet365_id, - is_active -) VALUES ( - $1, $2, $3, $4, $5 -) -ON CONFLICT (id) DO UPDATE + id, + name, + country_code, + bet365_id, + sport_id, + is_active + ) +VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO +UPDATE SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, - is_active = EXCLUDED.is_active + is_active = EXCLUDED.is_active, + sport_id = EXCLUDED.sport_id ` type InsertLeagueParams struct { @@ -120,6 +123,7 @@ type InsertLeagueParams struct { Name string `json:"name"` CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` + SportID int32 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` } @@ -129,6 +133,7 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro arg.Name, arg.CountryCode, arg.Bet365ID, + arg.SportID, arg.IsActive, ) return err @@ -136,39 +141,78 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro const SetLeagueActive = `-- name: SetLeagueActive :exec UPDATE leagues -SET is_active = true +SET is_active = $2 WHERE id = $1 ` -func (q *Queries) SetLeagueActive(ctx context.Context, id int64) error { - _, err := q.db.Exec(ctx, SetLeagueActive, id) +type SetLeagueActiveParams struct { + ID int64 `json:"id"` + IsActive pgtype.Bool `json:"is_active"` +} + +func (q *Queries) SetLeagueActive(ctx context.Context, arg SetLeagueActiveParams) error { + _, err := q.db.Exec(ctx, SetLeagueActive, arg.ID, arg.IsActive) return err } const UpdateLeague = `-- name: UpdateLeague :exec UPDATE leagues -SET name = $1, - country_code = $2, - bet365_id = $3, - is_active = $4 -WHERE id = $5 +SET name = COALESCE($2, name), + country_code = COALESCE($3, country_code), + bet365_id = COALESCE($4, bet365_id), + is_active = COALESCE($5, is_active), + sport_id = COALESCE($6, sport_id) +WHERE id = $1 ` type UpdateLeagueParams struct { - Name string `json:"name"` + ID int64 `json:"id"` + Name pgtype.Text `json:"name"` CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` IsActive pgtype.Bool `json:"is_active"` - ID int64 `json:"id"` + SportID pgtype.Int4 `json:"sport_id"` } func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) error { _, err := q.db.Exec(ctx, UpdateLeague, + arg.ID, arg.Name, arg.CountryCode, arg.Bet365ID, arg.IsActive, - arg.ID, + arg.SportID, + ) + return err +} + +const UpdateLeagueByBet365ID = `-- name: UpdateLeagueByBet365ID :exec +UPDATE leagues +SET name = COALESCE($2, name), + id = COALESCE($3, id), + country_code = COALESCE($4, country_code), + is_active = COALESCE($5, is_active), + sport_id = COALESCE($6, sport_id) +WHERE bet365_id = $1 +` + +type UpdateLeagueByBet365IDParams struct { + Bet365ID pgtype.Int4 `json:"bet365_id"` + Name pgtype.Text `json:"name"` + ID pgtype.Int8 `json:"id"` + CountryCode pgtype.Text `json:"country_code"` + IsActive pgtype.Bool `json:"is_active"` + SportID pgtype.Int4 `json:"sport_id"` +} + +func (q *Queries) UpdateLeagueByBet365ID(ctx context.Context, arg UpdateLeagueByBet365IDParams) error { + _, err := q.db.Exec(ctx, UpdateLeagueByBet365ID, + arg.Bet365ID, + arg.Name, + arg.ID, + arg.CountryCode, + arg.IsActive, + arg.SportID, ) return err } diff --git a/gen/db/models.go b/gen/db/models.go index b7a1be5..420586e 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 package dbgen @@ -140,6 +140,7 @@ type BranchDetail struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` ManagerName interface{} `json:"manager_name"` ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"` + Balance pgtype.Int8 `json:"balance"` } type BranchOperation struct { @@ -208,6 +209,7 @@ type League struct { Name string `json:"name"` CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` + SportID int32 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` } @@ -327,6 +329,7 @@ type Ticket struct { ID int64 `json:"id"` Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` + Ip string `json:"ip"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` } @@ -352,6 +355,7 @@ type TicketWithOutcome struct { ID int64 `json:"id"` Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` + Ip string `json:"ip"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` Outcomes []TicketOutcome `json:"outcomes"` diff --git a/gen/db/monitor.sql.go b/gen/db/monitor.sql.go index a9a7ecb..db8a9ba 100644 --- a/gen/db/monitor.sql.go +++ b/gen/db/monitor.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: monitor.sql package dbgen diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index ba9882b..9d9b242 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: notification.sql package dbgen diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index ba59003..99c47b7 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: odds.sql package dbgen @@ -11,6 +11,16 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const DeleteOddsForEvent = `-- name: DeleteOddsForEvent :exec +DELETE FROM odds +Where fi = $1 +` + +func (q *Queries) DeleteOddsForEvent(ctx context.Context, fi pgtype.Text) error { + _, err := q.db.Exec(ctx, DeleteOddsForEvent, fi) + return err +} + const GetALLPrematchOdds = `-- name: GetALLPrematchOdds :many SELECT event_id, fi, @@ -29,7 +39,7 @@ SELECT event_id, is_active FROM odds WHERE is_active = true - AND source = 'b365api' + AND source = 'bet365' ` type GetALLPrematchOddsRow struct { @@ -94,7 +104,7 @@ WHERE e.id = $1 AND e.is_live = false AND e.status = 'upcoming' AND o.is_active = true - AND o.source = 'b365api' + AND o.source = 'bet365' LIMIT $3 OFFSET $2 ` @@ -159,7 +169,7 @@ SELECT event_id, is_active FROM odds WHERE is_active = true - AND source = 'b365api' + AND source = 'bet365' ` type GetPrematchOddsRow struct { @@ -224,7 +234,7 @@ WHERE e.id = $1 AND e.is_live = false AND e.status = 'upcoming' AND o.is_active = true - AND o.source = 'b365api' + AND o.source = 'bet365' ` func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, id string) ([]Odd, error) { @@ -274,7 +284,7 @@ FROM odds WHERE market_id = $1 AND fi = $2 AND is_active = true - AND source = 'b365api' + AND source = 'bet365' ` type GetRawOddsByMarketIDParams struct { diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index 7dba175..99cdd4c 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: otp.sql package dbgen diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index 3a7f337..d0ab21e 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: referal.sql package dbgen diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index bff7b1e..cb3fdd8 100644 --- a/gen/db/result.sql.go +++ b/gen/db/result.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: result.sql package dbgen diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 8718bce..443b266 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: ticket.sql package dbgen @@ -11,24 +11,39 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const CountTicketByIP = `-- name: CountTicketByIP :one +SELECT count(id) +FROM tickets +WHERE IP = $1 +` + +func (q *Queries) CountTicketByIP(ctx context.Context, ip string) (int64, error) { + row := q.db.QueryRow(ctx, CountTicketByIP, ip) + var count int64 + err := row.Scan(&count) + return count, err +} + const CreateTicket = `-- name: CreateTicket :one -INSERT INTO tickets (amount, total_odds) -VALUES ($1, $2) -RETURNING id, amount, total_odds, created_at, updated_at +INSERT INTO tickets (amount, total_odds, ip) +VALUES ($1, $2, $3) +RETURNING id, amount, total_odds, ip, created_at, updated_at ` type CreateTicketParams struct { Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` + Ip string `json:"ip"` } func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Ticket, error) { - row := q.db.QueryRow(ctx, CreateTicket, arg.Amount, arg.TotalOdds) + row := q.db.QueryRow(ctx, CreateTicket, arg.Amount, arg.TotalOdds, arg.Ip) var i Ticket err := row.Scan( &i.ID, &i.Amount, &i.TotalOdds, + &i.Ip, &i.CreatedAt, &i.UpdatedAt, ) @@ -81,7 +96,7 @@ func (q *Queries) DeleteTicketOutcome(ctx context.Context, ticketID int64) error } const GetAllTickets = `-- name: GetAllTickets :many -SELECT id, amount, total_odds, created_at, updated_at, outcomes +SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes FROM ticket_with_outcomes ` @@ -98,6 +113,7 @@ func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error &i.ID, &i.Amount, &i.TotalOdds, + &i.Ip, &i.CreatedAt, &i.UpdatedAt, &i.Outcomes, @@ -113,7 +129,7 @@ func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error } const GetTicketByID = `-- name: GetTicketByID :one -SELECT id, amount, total_odds, created_at, updated_at, outcomes +SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes FROM ticket_with_outcomes WHERE id = $1 ` @@ -125,6 +141,7 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int64) (TicketWithOutcom &i.ID, &i.Amount, &i.TotalOdds, + &i.Ip, &i.CreatedAt, &i.UpdatedAt, &i.Outcomes, diff --git a/gen/db/transactions.sql.go b/gen/db/transactions.sql.go index cbd5743..80e6022 100644 --- a/gen/db/transactions.sql.go +++ b/gen/db/transactions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: transactions.sql package dbgen diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 540f79b..2c8e6f6 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: transfer.sql package dbgen diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 89051b2..2b440c2 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: user.sql package dbgen diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 16034ee..eb832e7 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: virtual_games.sql package dbgen diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index e46ea0b..64c3359 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: wallet.sql package dbgen diff --git a/internal/domain/branch.go b/internal/domain/branch.go index fe71494..43d2cc0 100644 --- a/internal/domain/branch.go +++ b/internal/domain/branch.go @@ -16,6 +16,7 @@ type BranchDetail struct { Name string Location string WalletID int64 + Balance Currency BranchManagerID int64 CompanyID int64 IsSuspended bool diff --git a/internal/domain/event.go b/internal/domain/event.go index a1c2a6f..fd041a7 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -2,6 +2,39 @@ package domain import "time" +// TODO: turn status into an enum +// Status represents the status of an event. +// 0 Not Started +// 1 InPlay +// 2 TO BE FIXED +// 3 Ended +// 4 Postponed +// 5 Cancelled +// 6 Walkover +// 7 Interrupted +// 8 Abandoned +// 9 Retired +// 10 Suspended +// 11 Decided by FA +// 99 Removed +type EventStatus string + +const ( + STATUS_PENDING EventStatus = "upcoming" + STATUS_IN_PLAY EventStatus = "in_play" + STATUS_TO_BE_FIXED EventStatus = "to_be_fixed" + STATUS_ENDED EventStatus = "ended" + STATUS_POSTPONED EventStatus = "postponed" + STATUS_CANCELLED EventStatus = "cancelled" + STATUS_WALKOVER EventStatus = "walkover" + STATUS_INTERRUPTED EventStatus = "interrupted" + STATUS_ABANDONED EventStatus = "abandoned" + STATUS_RETIRED EventStatus = "retired" + STATUS_SUSPENDED EventStatus = "suspended" + STATUS_DECIDED_BY_FA EventStatus = "decided_by_fa" + STATUS_REMOVED EventStatus = "removed" +) + type Event struct { ID string SportID int32 @@ -53,20 +86,21 @@ type BetResult struct { } type UpcomingEvent struct { - ID string // Event ID - SportID int32 // Sport ID - MatchName string // Match or event name - HomeTeam string // Home team name (if available) - AwayTeam string // Away team name (can be empty/null) - HomeTeamID int32 // Home team ID - AwayTeamID int32 // Away team ID (can be empty/null) - HomeKitImage string // Kit or image for home team (optional) - AwayKitImage string // Kit or image for away team (optional) - LeagueID int32 // League ID - LeagueName string // League name - LeagueCC string // League country code - StartTime time.Time // Converted from "time" field in UNIX format - Source string // bet api provider (bet365, betfair) + ID string `json:"id"` // Event ID + SportID int32 `json:"sport_id"` // Sport ID + MatchName string `json:"match_name"` // Match or event name + HomeTeam string `json:"home_team"` // Home team name (if available) + AwayTeam string `json:"away_team"` // Away team name (can be empty/null) + HomeTeamID int32 `json:"home_team_id"` // Home team ID + AwayTeamID int32 `json:"away_team_id"` // Away team ID (can be empty/null) + HomeKitImage string `json:"home_kit_image"` // Kit or image for home team (optional) + AwayKitImage string `json:"away_kit_image"` // Kit or image for away team (optional) + LeagueID int32 `json:"league_id"` // League ID + LeagueName string `json:"league_name"` // League name + LeagueCC string `json:"league_cc"` // League country code + 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 } type MatchResult struct { EventID string @@ -83,3 +117,14 @@ type Odds struct { Name string `json:"name"` HitStatus string `json:"hit_status"` } + +type EventFilter struct { + SportID ValidInt32 + LeagueID ValidInt32 + CountryCode ValidString + FirstStartTime ValidTime + LastStartTime ValidTime + Limit ValidInt64 + Offset ValidInt64 + MatchStatus ValidString // e.g., "upcoming", "in_play", "ended" +} diff --git a/internal/domain/league.go b/internal/domain/league.go index f5ac35e..67787a5 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -1,9 +1,27 @@ package domain type League struct { - ID int64 - Name string - CountryCode string - Bet365ID int32 - IsActive bool + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"BPL"` + CountryCode string `json:"cc" example:"uk"` + Bet365ID int32 `json:"bet365_id" example:"1121"` + IsActive bool `json:"is_active" example:"false"` + SportID int32 `json:"sport_id" example:"1"` +} + +type UpdateLeague struct { + ID int64 `json:"id" example:"1"` + Name ValidString `json:"name" example:"BPL"` + CountryCode ValidString `json:"cc" example:"uk"` + Bet365ID ValidInt32 `json:"bet365_id" example:"1121"` + IsActive ValidBool `json:"is_active" example:"false"` + SportID ValidInt32 `json:"sport_id" example:"1"` +} + +type LeagueFilter struct { + CountryCode ValidString + SportID ValidInt32 + IsActive ValidBool + Limit ValidInt64 + Offset ValidInt64 } diff --git a/internal/domain/oddres.go b/internal/domain/oddres.go index 5b4a39f..649c2aa 100644 --- a/internal/domain/oddres.go +++ b/internal/domain/oddres.go @@ -12,6 +12,19 @@ type OddsSection struct { Sp map[string]OddsMarket `json:"sp"` } +type ParseOddSectionsRes struct { + Sections map[string]OddsSection + OtherRes []OddsSection + EventFI string +} +type RawOdd struct { + ID string `json:"id"` + Name string `json:"name"` + Header string `json:"header,omitempty"` + Handicap string `json:"handicap,omitempty"` + Odds string `json:"odds"` +} + // The Market ID for the json data can be either string / int which is causing problems when UnMarshalling type OddsMarket struct { ID json.RawMessage `json:"id"` diff --git a/internal/domain/odds.go b/internal/domain/odds.go index b617079..ea5a3e9 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -45,3 +45,5 @@ type RawOddsByMarketID struct { RawOdds []RawMessage `json:"raw_odds"` FetchedAt time.Time `json:"fetched_at"` } + + diff --git a/internal/domain/resultres.go b/internal/domain/resultres.go index 6595c69..f8aad66 100644 --- a/internal/domain/resultres.go +++ b/internal/domain/resultres.go @@ -27,6 +27,17 @@ type Score struct { Away string `json:"away"` } +type CommonResultResponse struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueRes `json:"league"` + Home Team `json:"home"` + Away Team `json:"away"` + SS string `json:"ss"` +} + type FootballResultResponse struct { ID string `json:"id"` SportID string `json:"sport_id"` diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index 15dd180..e85638f 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -51,4 +51,5 @@ type GetTicket struct { type CreateTicket struct { Amount Currency TotalOdds float32 + IP string } diff --git a/internal/domain/user.go b/internal/domain/user.go index ce9880c..4bb3ef4 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -73,17 +73,20 @@ type UpdateUserReferalCode struct { } type GetCashier struct { - ID int64 `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - Role Role `json:"role"` - EmailVerified bool `json:"email_verified"` - PhoneVerified bool `json:"phone_verified"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - SuspendedAt time.Time `json:"suspended_at"` - Suspended bool `json:"suspended"` - BranchID int64 `json:"branch_id"` + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + Role Role `json:"role"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + SuspendedAt time.Time `json:"suspended_at"` + Suspended bool `json:"suspended"` + BranchID int64 `json:"branch_id"` + BranchName string `json:"branch_name"` + BranchWallet int64 `json:"branch_wallet"` + BranchLocation string `json:"branch_location"` } diff --git a/internal/repository/branch.go b/internal/repository/branch.go index 0f5c5b5..51f460f 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -31,6 +31,7 @@ func convertDBBranchDetail(dbBranch dbgen.BranchDetail) domain.BranchDetail { IsSelfOwned: dbBranch.IsSelfOwned, ManagerName: dbBranch.ManagerName.(string), ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String, + Balance: domain.Currency(dbBranch.Balance.Int64), } } diff --git a/internal/repository/event.go b/internal/repository/event.go index 2366e75..219c915 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -88,13 +88,17 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), Source: e.Source.String, + Status: domain.EventStatus(e.Status.String), } } return upcomingEvents, nil } -func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { - events, err := s.queries.GetExpiredUpcomingEvents(ctx) +func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) { + events, err := s.queries.GetExpiredUpcomingEvents(ctx, pgtype.Text{ + String: filter.MatchStatus.Value, + Valid: filter.MatchStatus.Valid, + }) if err != nil { return nil, err } @@ -116,37 +120,42 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), Source: e.Source.String, + Status: domain.EventStatus(e.Status.String), } } return upcomingEvents, nil } -func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidInt32, sportID domain.ValidInt32, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) { +func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) { events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{ LeagueID: pgtype.Int4{ - Int32: leagueID.Value, - Valid: leagueID.Valid, + Int32: int32(filter.LeagueID.Value), + Valid: filter.LeagueID.Valid, }, SportID: pgtype.Int4{ - Int32: sportID.Value, - Valid: sportID.Valid, + Int32: int32(filter.SportID.Value), + Valid: filter.SportID.Valid, }, Limit: pgtype.Int4{ - Int32: int32(limit.Value), - Valid: limit.Valid, + Int32: int32(filter.Limit.Value), + Valid: filter.Limit.Valid, }, Offset: pgtype.Int4{ - Int32: int32(offset.Value * limit.Value), - Valid: offset.Valid, + Int32: int32(filter.Offset.Value * filter.Limit.Value), + Valid: filter.Offset.Valid, }, FirstStartTime: pgtype.Timestamp{ - Time: firstStartTime.Value.UTC(), - Valid: firstStartTime.Valid, + Time: filter.FirstStartTime.Value.UTC(), + Valid: filter.FirstStartTime.Valid, }, LastStartTime: pgtype.Timestamp{ - Time: lastStartTime.Value.UTC(), - Valid: lastStartTime.Valid, + Time: filter.LastStartTime.Value.UTC(), + Valid: filter.LastStartTime.Valid, + }, + CountryCode: pgtype.Text{ + String: filter.CountryCode.Value, + Valid: filter.CountryCode.Valid, }, }) @@ -170,31 +179,36 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.Val LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), Source: e.Source.String, + Status: domain.EventStatus(e.Status.String), } } totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ LeagueID: pgtype.Int4{ - Int32: leagueID.Value, - Valid: leagueID.Valid, + Int32: int32(filter.LeagueID.Value), + Valid: filter.LeagueID.Valid, }, SportID: pgtype.Int4{ - Int32: sportID.Value, - Valid: sportID.Valid, + Int32: int32(filter.SportID.Value), + Valid: filter.SportID.Valid, }, FirstStartTime: pgtype.Timestamp{ - Time: firstStartTime.Value.UTC(), - Valid: firstStartTime.Valid, + Time: filter.FirstStartTime.Value.UTC(), + Valid: filter.FirstStartTime.Valid, }, LastStartTime: pgtype.Timestamp{ - Time: lastStartTime.Value.UTC(), - Valid: lastStartTime.Valid, + Time: filter.LastStartTime.Value.UTC(), + Valid: filter.LastStartTime.Valid, + }, + CountryCode: pgtype.Text{ + String: filter.CountryCode.Value, + Valid: filter.CountryCode.Valid, }, }) if err != nil { return nil, 0, err } - numberOfPages := math.Ceil(float64(totalCount) / float64(limit.Value)) + numberOfPages := math.Ceil(float64(totalCount) / float64(filter.Limit.Value)) return upcomingEvents, int64(numberOfPages), nil } func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { @@ -218,12 +232,13 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc LeagueCC: event.LeagueCc.String, StartTime: event.StartTime.Time.UTC(), Source: event.Source.String, + Status: domain.EventStatus(event.Status.String), }, nil } -func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore, status string) error { +func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { params := dbgen.UpdateMatchResultParams{ Score: pgtype.Text{String: fullScore, Valid: true}, - Status: pgtype.Text{String: status, Valid: true}, + Status: pgtype.Text{String: string(status), Valid: true}, ID: eventID, } @@ -235,6 +250,24 @@ func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore, status return nil } +func (s *Store) UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error { + params := dbgen.UpdateMatchResultParams{ + Status: pgtype.Text{ + String: string(status), + Valid: true, + }, + ID: eventID, + } + + err := s.queries.UpdateMatchResult(ctx, params) + + if err != nil { + return err + } + return nil + +} + func (s *Store) DeleteEvent(ctx context.Context, eventID string) error { err := s.queries.DeleteEvent(ctx, eventID) if err != nil { diff --git a/internal/repository/league.go b/internal/repository/league.go index 7e5205f..67a1ba0 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -15,30 +15,33 @@ func (s *Store) SaveLeague(ctx context.Context, l domain.League) error { CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true}, + SportID: l.SportID, }) } -func (s *Store) GetSupportedLeagues(ctx context.Context) ([]domain.League, error) { - leagues, err := s.queries.GetSupportedLeagues(ctx) - if err != nil { - return nil, err - } - - supportedLeagues := make([]domain.League, len(leagues)) - for i, league := range leagues { - supportedLeagues[i] = domain.League{ - ID: league.ID, - Name: league.Name, - CountryCode: league.CountryCode.String, - Bet365ID: league.Bet365ID.Int32, - IsActive: league.IsActive.Bool, - } - } - return supportedLeagues, nil -} - -func (s *Store) GetAllLeagues(ctx context.Context) ([]domain.League, error) { - l, err := s.queries.GetAllLeagues(ctx) +func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) { + l, err := s.queries.GetAllLeagues(ctx, dbgen.GetAllLeaguesParams{ + CountryCode: pgtype.Text{ + String: filter.CountryCode.Value, + Valid: filter.CountryCode.Valid, + }, + SportID: pgtype.Int4{ + Int32: filter.SportID.Value, + Valid: filter.SportID.Valid, + }, + IsActive: pgtype.Bool{ + Bool: filter.IsActive.Value, + Valid: filter.IsActive.Valid, + }, + Limit: pgtype.Int4{ + Int32: int32(filter.Limit.Value), + Valid: filter.Limit.Valid, + }, + Offset: pgtype.Int4{ + Int32: int32(filter.Offset.Value * filter.Limit.Value), + Valid: filter.Offset.Valid, + }, + }) if err != nil { return nil, err } @@ -51,6 +54,7 @@ func (s *Store) GetAllLeagues(ctx context.Context) ([]domain.League, error) { CountryCode: league.CountryCode.String, Bet365ID: league.Bet365ID.Int32, IsActive: league.IsActive.Bool, + SportID: league.SportID, } } return leagues, nil @@ -60,16 +64,40 @@ func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, e return s.queries.CheckLeagueSupport(ctx, leagueID) } -func (s *Store) SetLeagueActive(ctx context.Context, leagueId int64) error { - return s.queries.SetLeagueActive(ctx, leagueId) -} - -// TODO: update based on id, no need for the entire league (same as the set active one) -func (s *Store) SetLeagueInActive(ctx context.Context, l domain.League) error { - return s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{ - Name: l.Name, - CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, - Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, - IsActive: pgtype.Bool{Bool: false, Valid: true}, +func (s *Store) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error { + return s.queries.SetLeagueActive(ctx, dbgen.SetLeagueActiveParams{ + ID: leagueId, + IsActive: pgtype.Bool{ + Bool: isActive, + Valid: true, + }, }) } + +func (s *Store) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { + err := s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{ + ID: league.ID, + Name: pgtype.Text{ + String: league.Name.Value, + Valid: league.Name.Valid, + }, + CountryCode: pgtype.Text{ + String: league.CountryCode.Value, + Valid: league.CountryCode.Valid, + }, + Bet365ID: pgtype.Int4{ + Int32: league.Bet365ID.Value, + Valid: league.Bet365ID.Valid, + }, + IsActive: pgtype.Bool{ + Bool: league.IsActive.Value, + Valid: league.IsActive.Valid, + }, + SportID: pgtype.Int4{ + Int32: league.SportID.Value, + Valid: league.SportID.Valid, + }, + }) + + return err +} diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 875ea97..2a0160e 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -3,6 +3,7 @@ package repository import ( "context" "encoding/json" + "os" "strconv" "time" @@ -274,6 +275,13 @@ func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID stri return domainOdds, nil } +func (s *Store) DeleteOddsForEvent(ctx context.Context, eventID string) error { + return s.queries.DeleteOddsForEvent(ctx, pgtype.Text{ + String: eventID, + Valid: true, + }) +} + func getString(v interface{}) string { if s, ok := v.(string); ok { return s diff --git a/internal/repository/otp.go b/internal/repository/otp.go index aaa4c10..29c3f81 100644 --- a/internal/repository/otp.go +++ b/internal/repository/otp.go @@ -3,6 +3,7 @@ package repository import ( "context" "database/sql" + "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -32,6 +33,7 @@ func (s *Store) GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor OtpFor: string(sentfor), }) if err != nil { + fmt.Printf("OTP REPO error: %v sentTo: %v, medium: %v, otpFor: %v\n", err, sentTo, medium, sentfor) if err == sql.ErrNoRows { return domain.Otp{}, domain.ErrOtpNotFound } diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index 5083f65..337accb 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -70,6 +70,7 @@ func convertCreateTicket(ticket domain.CreateTicket) dbgen.CreateTicketParams { return dbgen.CreateTicketParams{ Amount: int64(ticket.Amount), TotalOdds: ticket.TotalOdds, + Ip: ticket.IP, } } @@ -123,6 +124,16 @@ func (s *Store) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { return result, nil } +func (s *Store) CountTicketByIP(ctx context.Context, IP string) (int64, error) { + count, err := s.queries.CountTicketByIP(ctx, IP) + + if err != nil { + return 0, err + } + + return count, err +} + func (s *Store) UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { err := s.queries.UpdateTicketOutcomeStatus(ctx, dbgen.UpdateTicketOutcomeStatusParams{ Status: int32(status), diff --git a/internal/repository/user.go b/internal/repository/user.go index 1f41c35..da3ed81 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -82,6 +82,10 @@ func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) UpdatedAt: user.UpdatedAt.Time, SuspendedAt: user.SuspendedAt.Time, Suspended: user.Suspended, + CompanyID: domain.ValidInt64{ + Value: user.CompanyID.Int64, + Valid: user.CompanyID.Valid, + }, }, nil } func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.User, int64, error) { @@ -118,6 +122,10 @@ func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.U UpdatedAt: user.UpdatedAt.Time, SuspendedAt: user.SuspendedAt.Time, Suspended: user.Suspended, + CompanyID: domain.ValidInt64{ + Value: user.CompanyID.Int64, + Valid: user.CompanyID.Valid, + }, } } totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ @@ -130,29 +138,36 @@ func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.U return userList, totalCount, nil } -func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, error) { +func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, int64, error) { users, err := s.queries.GetAllCashiers(ctx) if err != nil { - return nil, err + return nil, 0, err } userList := make([]domain.GetCashier, len(users)) for i, user := range users { userList[i] = domain.GetCashier{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Role: domain.Role(user.Role), - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - CreatedAt: user.CreatedAt.Time, - UpdatedAt: user.UpdatedAt.Time, - SuspendedAt: user.SuspendedAt.Time, - Suspended: user.Suspended, + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + SuspendedAt: user.SuspendedAt.Time, + Suspended: user.Suspended, + BranchID: user.BranchID, + BranchName: user.BranchName, + BranchWallet: user.BranchWallet, + BranchLocation: user.BranchLocation, } } - return userList, nil + totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ + Role: string(domain.RoleCashier), + }) + return userList, totalCount, nil } func (s *Store) GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error) { @@ -161,19 +176,22 @@ func (s *Store) GetCashierByID(ctx context.Context, cashierID int64) (domain.Get return domain.GetCashier{}, err } return domain.GetCashier{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Role: domain.Role(user.Role), - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - CreatedAt: user.CreatedAt.Time, - UpdatedAt: user.UpdatedAt.Time, - SuspendedAt: user.SuspendedAt.Time, - Suspended: user.Suspended, - BranchID: user.BranchID, + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + SuspendedAt: user.SuspendedAt.Time, + Suspended: user.Suspended, + BranchID: user.BranchID, + BranchName: user.BranchName, + BranchWallet: user.BranchWallet, + BranchLocation: user.BranchLocation, }, nil } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index f0d6236..59d0bc0 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -30,14 +30,14 @@ var ( type Service struct { betStore BetStore eventSvc event.Service - prematchSvc odds.Service + prematchSvc odds.ServiceImpl walletSvc wallet.Service branchSvc branch.Service logger *slog.Logger mongoLogger *zap.Logger } -func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.Service, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger, mongoLogger *zap.Logger) *Service { +func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.ServiceImpl, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger, mongoLogger *zap.Logger) *Service { return &Service{ betStore: betStore, eventSvc: eventSvc, @@ -490,7 +490,12 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le // Get a unexpired event id events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx, - domain.ValidInt64{}, domain.ValidInt64{}, leagueID, sportID, firstStartTime, lastStartTime) + domain.EventFilter{ + SportID: sportID, + LeagueID: leagueID, + FirstStartTime: firstStartTime, + LastStartTime: lastStartTime, + }) if err != nil { s.mongoLogger.Error("failed to get paginated upcoming events", diff --git a/internal/services/event/port.go b/internal/services/event/port.go index af8397e..c548a32 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -10,9 +10,10 @@ type Service interface { FetchLiveEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) - GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) - GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidInt32, sportID domain.ValidInt32, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) + GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) + GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) // 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 } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index b456353..0ad44a5 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -209,16 +209,17 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour // log.Printf("❌ Failed to open leagues file %v", err) // return // } - for _, sportID := range sportIDs { + for sportIndex, sportID := range sportIDs { var totalPages int = 1 var page int = 0 - var limit int = 100 + var limit int = 200 var count int = 0 log.Printf("Sport ID %d", sportID) for page <= totalPages { page = page + 1 url := fmt.Sprintf(url, sportID, s.token, page) - log.Printf("📡 Fetching data from %s - sport %d, for event data page %d", source, sportID, page) + log.Printf("📡 Fetching data from %s - sport %d (%d/%d), for event data page (%d/%d)", + source, sportID, sportIndex+1, len(sportIDs), page, totalPages) resp, err := http.Get(url) if err != nil { log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) @@ -249,13 +250,23 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour } // doesn't make sense to save and check back to back, but for now it can be here - s.store.SaveLeague(ctx, domain.League{ + // no this its fine to keep it here + // but change the league id to bet365 id later + err = s.store.SaveLeague(ctx, domain.League{ ID: leagueID, Name: ev.League.Name, IsActive: true, + SportID: convertInt32(ev.SportID), }) + if err != nil { + log.Printf("❌ Error Saving League on %v", ev.League.Name) + log.Printf("err:%v", err) + continue + } + if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil { + log.Printf("Skipping league %v", ev.League.Name) skippedLeague = append(skippedLeague, ev.League.Name) continue } @@ -335,18 +346,25 @@ func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEv return s.store.GetAllUpcomingEvents(ctx) } -func (s *service) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { - return s.store.GetExpiredUpcomingEvents(ctx) +func (s *service) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) { + return s.store.GetExpiredUpcomingEvents(ctx, filter) } -func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidInt32, sportID domain.ValidInt32, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) { - return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID, firstStartTime, lastStartTime) +func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) { + return s.store.GetPaginatedUpcomingEvents(ctx, filter) } func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { return s.store.GetUpcomingEventByID(ctx, ID) } +func (s *service) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { + return s.store.UpdateFinalScore(ctx, eventID, fullScore, status) +} +func (s *service) UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error { + return s.store.UpdateEventStatus(ctx, eventID, status) +} + // func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error { // url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID) diff --git a/internal/services/league/port.go b/internal/services/league/port.go index 7b71a48..cfcf8b5 100644 --- a/internal/services/league/port.go +++ b/internal/services/league/port.go @@ -7,6 +7,8 @@ import ( ) type Service interface { - GetAllLeagues(ctx context.Context) ([]domain.League, error) - SetLeagueActive(ctx context.Context, leagueId int64) error + SaveLeague(ctx context.Context, l domain.League) error + GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) + SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error + UpdateLeague(ctx context.Context, league domain.UpdateLeague) error } diff --git a/internal/services/league/service.go b/internal/services/league/service.go index b1f05ed..e23bf22 100644 --- a/internal/services/league/service.go +++ b/internal/services/league/service.go @@ -17,10 +17,18 @@ func New(store *repository.Store) Service { } } -func (s *service) GetAllLeagues(ctx context.Context) ([]domain.League, error) { - return s.store.GetAllLeagues(ctx) +func (s *service) SaveLeague(ctx context.Context, l domain.League) error { + return s.store.SaveLeague(ctx, l) } -func (s *service) SetLeagueActive(ctx context.Context, leagueId int64) error { - return s.store.SetLeagueActive(ctx, leagueId) +func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) { + return s.store.GetAllLeagues(ctx, filter) +} + +func (s *service) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error { + return s.store.SetLeagueActive(ctx, leagueId, isActive) +} + +func (s *service) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { + return s.store.UpdateLeague(ctx, league) } diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 50275b2..8dd0088 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -2,15 +2,19 @@ package odds import ( "context" + "encoding/json" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) type Service interface { FetchNonLiveOdds(ctx context.Context) error + FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) + ParseOddSections(ctx context.Context, res json.RawMessage, sportID int64) (domain.ParseOddSectionsRes, error) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) + DeleteOddsForEvent(ctx context.Context, eventID string) error } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 30ae708..b3010d0 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -41,18 +41,23 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { // wg.Add(2) wg.Add(1) + go func() { + defer wg.Done() + if err := s.fetchBet365Odds(ctx); err != nil { + errChan <- fmt.Errorf("bet365 odds fetching error: %w", err) + } + }() + // go func() { // defer wg.Done() - // if err := s.fetchBet365Odds(ctx); err != nil { - // errChan <- fmt.Errorf("bet365 odds fetching error: %w", err) + // if err := s.fetchBwinOdds(ctx); err != nil { + // errChan <- fmt.Errorf("bwin odds fetching error: %w", err) // } // }() go func() { - defer wg.Done() - if err := s.fetchBwinOdds(ctx); err != nil { - errChan <- fmt.Errorf("bwin odds fetching error: %w", err) - } + wg.Wait() + close(errChan) }() var errs []error @@ -77,98 +82,40 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { var errs []error for index, event := range eventIDs { + log.Printf("📡 Fetching prematch odds for event ID: %v (%d/%d) ", event.ID, index, len(eventIDs)) - eventID, err := strconv.ParseInt(event.ID, 10, 64) + oddsData, err := s.FetchNonLiveOddsByEventID(ctx, event.ID) if err != nil { - s.logger.Error("Failed to parse event id", "error", err.Error()) - return err - } - - url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%d", s.config.Bet365Token, eventID) - - log.Printf("📡 Fetching prematch odds for event ID: %d (%d/%d) ", eventID, index, len(eventIDs)) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - log.Printf("❌ Failed to create request for event %d: %v", eventID, err) - s.logger.Error("Failed to create request for event%d: %v", strconv.FormatInt(eventID, 10), err.Error()) + s.logger.Error("Failed to fetch prematch odds", "eventID", event.ID, "error", err) + errs = append(errs, fmt.Errorf("failed to fetch prematch odds for event %v: %w", event.ID, err)) continue } - resp, err := s.client.Do(req) + parsedOddSections, err := s.ParseOddSections(ctx, oddsData.Results[0], event.SportID) if err != nil { - log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err) - continue - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Printf("❌ Failed to read response body for event %d: %v", eventID, err) - continue - } - var oddsData domain.BaseNonLiveOddResponse - - if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { - log.Printf("❌ Invalid prematch data for event %d", eventID) + s.logger.Error("Failed to parse odd section", "error", err) + errs = append(errs, fmt.Errorf("failed to parse odd section for event %v: %w", event.ID, err)) continue } - switch event.SportID { - case domain.FOOTBALL: - if err := s.parseFootball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting football odd") + if parsedOddSections.EventFI == "" { + s.logger.Error("Skipping result with no valid Event FI field", "fi", parsedOddSections.EventFI) + errs = append(errs, errors.New("event FI is empty")) + continue + } + + for oddCategory, section := range parsedOddSections.Sections { + if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, oddCategory, section); err != nil { + s.logger.Error("Error storing odd section", "eventID", event.ID, "odd", oddCategory) + log.Printf("⚠️ Error when storing %v", err) errs = append(errs, err) } - case domain.BASKETBALL: - if err := s.parseBasketball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting basketball odd") - errs = append(errs, err) - } - case domain.ICE_HOCKEY: - if err := s.parseIceHockey(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting ice hockey odd") - errs = append(errs, err) - } - case domain.CRICKET: - if err := s.parseCricket(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting cricket odd") - errs = append(errs, err) - } - case domain.VOLLEYBALL: - if err := s.parseVolleyball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting volleyball odd") - errs = append(errs, err) - } - case domain.DARTS: - if err := s.parseDarts(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting darts odd") - errs = append(errs, err) - } - case domain.FUTSAL: - if err := s.parseFutsal(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting futsal odd") - errs = append(errs, err) - } - case domain.AMERICAN_FOOTBALL: - if err := s.parseAmericanFootball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting american football odd") - errs = append(errs, err) - } - case domain.RUGBY_LEAGUE: - if err := s.parseRugbyLeague(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting rugby league odd") - errs = append(errs, err) - } - case domain.RUGBY_UNION: - if err := s.parseRugbyUnion(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting rugby union odd") - errs = append(errs, err) - } - case domain.BASEBALL: - if err := s.parseBaseball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting baseball odd") + } + for _, section := range parsedOddSections.OtherRes { + if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, "others", section); err != nil { + s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) + continue } } @@ -176,6 +123,10 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { } + for err := range errs { + log.Printf("❌ Error: %v", err) + } + return nil } @@ -266,446 +217,205 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { return nil } -func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) error { - var footballRes domain.FootballOddsResponse - if err := json.Unmarshal(res, &footballRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "error", err) - return err - } - if footballRes.EventID == "" && footballRes.FI == "" { - s.logger.Error("Skipping football result with no valid Event ID", "eventID", footballRes.EventID, "fi", footballRes.FI) - return fmt.Errorf("Skipping football result with no valid Event ID Event ID %v", footballRes.EventID) - } - sections := map[string]domain.OddsSection{ - "main": footballRes.Main, - "asian_lines": footballRes.AsianLines, - "goals": footballRes.Goals, - "half": footballRes.Half, +func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) { + + eventID, err := strconv.ParseInt(eventIDStr, 10, 64) + if err != nil { + s.logger.Error("Failed to parse event id") + return domain.BaseNonLiveOddResponse{}, err } - var errs []error + url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%d", s.config.Bet365Token, eventID) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + log.Printf("❌ Failed to create request for event %d: %v", eventID, err) - for oddCategory, section := range sections { - if err := s.storeSection(ctx, footballRes.EventID, footballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Error storing football section", "eventID", footballRes.FI, "odd", oddCategory) - log.Printf("⚠️ Error when storing football %v", err) - errs = append(errs, err) - } } - if len(errs) > 0 { - return errors.Join(errs...) + resp, err := s.client.Do(req) + if err != nil { + log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err) + return domain.BaseNonLiveOddResponse{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("❌ Failed to read response body for event %d: %v", eventID, err) + return domain.BaseNonLiveOddResponse{}, err + } + var oddsData domain.BaseNonLiveOddResponse + + if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { + log.Printf("❌ Invalid prematch data for event %d", eventID) + return domain.BaseNonLiveOddResponse{}, err } - return nil + return oddsData, nil } -func (s *ServiceImpl) parseBasketball(ctx context.Context, res json.RawMessage) error { - var basketballRes domain.BasketballOddsResponse - if err := json.Unmarshal(res, &basketballRes); err != nil { - s.logger.Error("Failed to unmarshal basketball result", "error", err) - return err - } - if basketballRes.EventID == "" && basketballRes.FI == "" { - s.logger.Error("Skipping basketball result with no valid Event ID") - return fmt.Errorf("Skipping basketball result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": basketballRes.Main, - "half_props": basketballRes.HalfProps, - "quarter_props": basketballRes.QuarterProps, - "team_props": basketballRes.TeamProps, - } +func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, sportID int32) (domain.ParseOddSectionsRes, error) { + var sections map[string]domain.OddsSection + var OtherRes []domain.OddsSection + var eventFI string + switch sportID { + case domain.FOOTBALL: + var footballRes domain.FootballOddsResponse + if err := json.Unmarshal(res, &footballRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = footballRes.FI + sections = map[string]domain.OddsSection{ + "main": footballRes.Main, + "asian_lines": footballRes.AsianLines, + "goals": footballRes.Goals, + "half": footballRes.Half, + } + case domain.BASKETBALL: + var basketballRes domain.BasketballOddsResponse + if err := json.Unmarshal(res, &basketballRes); err != nil { + s.logger.Error("Failed to unmarshal basketball result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = basketballRes.FI + OtherRes = basketballRes.Others + sections = map[string]domain.OddsSection{ + "main": basketballRes.Main, + "half_props": basketballRes.HalfProps, + "quarter_props": basketballRes.QuarterProps, + "team_props": basketballRes.TeamProps, + } - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue + case domain.ICE_HOCKEY: + var iceHockeyRes domain.IceHockeyOddsResponse + if err := json.Unmarshal(res, &iceHockeyRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = iceHockeyRes.FI + OtherRes = iceHockeyRes.Others + sections = map[string]domain.OddsSection{ + "main": iceHockeyRes.Main, + "main_2": iceHockeyRes.Main2, + "1st_period": iceHockeyRes.FirstPeriod, + } + case domain.CRICKET: + var cricketRes domain.CricketOddsResponse + if err := json.Unmarshal(res, &cricketRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = cricketRes.FI + OtherRes = cricketRes.Others + sections = map[string]domain.OddsSection{ + "1st_over": cricketRes.Main, + "innings_1": cricketRes.First_Innings, + "main": cricketRes.Main, + "match": cricketRes.Match, + "player": cricketRes.Player, + "team": cricketRes.Team, + } + case domain.VOLLEYBALL: + var volleyballRes domain.VolleyballOddsResponse + if err := json.Unmarshal(res, &volleyballRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = volleyballRes.FI + OtherRes = volleyballRes.Others + sections = map[string]domain.OddsSection{ + "main": volleyballRes.Main, + } + case domain.DARTS: + var dartsRes domain.DartsOddsResponse + if err := json.Unmarshal(res, &dartsRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = dartsRes.FI + OtherRes = dartsRes.Others + sections = map[string]domain.OddsSection{ + "180s": dartsRes.OneEightys, + "extra": dartsRes.Extra, + "leg": dartsRes.Leg, + "main": dartsRes.Main, + } + case domain.FUTSAL: + var futsalRes domain.FutsalOddsResponse + if err := json.Unmarshal(res, &futsalRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = futsalRes.FI + OtherRes = futsalRes.Others + sections = map[string]domain.OddsSection{ + "main": futsalRes.Main, + "score": futsalRes.Score, + } + case domain.AMERICAN_FOOTBALL: + var americanFootballRes domain.AmericanFootballOddsResponse + if err := json.Unmarshal(res, &americanFootballRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = americanFootballRes.FI + OtherRes = americanFootballRes.Others + sections = map[string]domain.OddsSection{ + "half_props": americanFootballRes.HalfProps, + "main": americanFootballRes.Main, + "quarter_props": americanFootballRes.QuarterProps, + } + case domain.RUGBY_LEAGUE: + var rugbyLeagueRes domain.RugbyLeagueOddsResponse + if err := json.Unmarshal(res, &rugbyLeagueRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = rugbyLeagueRes.FI + OtherRes = rugbyLeagueRes.Others + sections = map[string]domain.OddsSection{ + "10minute": rugbyLeagueRes.TenMinute, + "main": rugbyLeagueRes.Main, + "main_2": rugbyLeagueRes.Main2, + "player": rugbyLeagueRes.Player, + "Score": rugbyLeagueRes.Score, + "Team": rugbyLeagueRes.Team, + } + case domain.RUGBY_UNION: + var rugbyUnionRes domain.RugbyUnionOddsResponse + if err := json.Unmarshal(res, &rugbyUnionRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = rugbyUnionRes.FI + OtherRes = rugbyUnionRes.Others + sections = map[string]domain.OddsSection{ + "main": rugbyUnionRes.Main, + "main_2": rugbyUnionRes.Main2, + "player": rugbyUnionRes.Player, + "Score": rugbyUnionRes.Score, + "Team": rugbyUnionRes.Team, + } + case domain.BASEBALL: + var baseballRes domain.BaseballOddsResponse + if err := json.Unmarshal(res, &baseballRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = baseballRes.FI + sections = map[string]domain.OddsSection{ + "main": baseballRes.Main, + "mani_props": baseballRes.MainProps, } } - for _, section := range basketballRes.Others { - if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} -func (s *ServiceImpl) parseIceHockey(ctx context.Context, res json.RawMessage) error { - var iceHockeyRes domain.IceHockeyOddsResponse - if err := json.Unmarshal(res, &iceHockeyRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if iceHockeyRes.EventID == "" && iceHockeyRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": iceHockeyRes.Main, - "main_2": iceHockeyRes.Main2, - "1st_period": iceHockeyRes.FirstPeriod, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range iceHockeyRes.Others { - if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseCricket(ctx context.Context, res json.RawMessage) error { - var cricketRes domain.CricketOddsResponse - if err := json.Unmarshal(res, &cricketRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if cricketRes.EventID == "" && cricketRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - - sections := map[string]domain.OddsSection{ - "1st_over": cricketRes.Main, - "innings_1": cricketRes.First_Innings, - "main": cricketRes.Main, - "match": cricketRes.Match, - "player": cricketRes.Player, - "team": cricketRes.Team, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, cricketRes.EventID, cricketRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range cricketRes.Others { - if err := s.storeSection(ctx, cricketRes.EventID, cricketRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseVolleyball(ctx context.Context, res json.RawMessage) error { - var volleyballRes domain.VolleyballOddsResponse - if err := json.Unmarshal(res, &volleyballRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if volleyballRes.EventID == "" && volleyballRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": volleyballRes.Main, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, volleyballRes.EventID, volleyballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range volleyballRes.Others { - if err := s.storeSection(ctx, volleyballRes.EventID, volleyballRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseDarts(ctx context.Context, res json.RawMessage) error { - var dartsRes domain.DartsOddsResponse - if err := json.Unmarshal(res, &dartsRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if dartsRes.EventID == "" && dartsRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "180s": dartsRes.OneEightys, - "extra": dartsRes.Extra, - "leg": dartsRes.Leg, - "main": dartsRes.Main, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, dartsRes.EventID, dartsRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range dartsRes.Others { - if err := s.storeSection(ctx, dartsRes.EventID, dartsRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseFutsal(ctx context.Context, res json.RawMessage) error { - var futsalRes domain.FutsalOddsResponse - if err := json.Unmarshal(res, &futsalRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if futsalRes.EventID == "" && futsalRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": futsalRes.Main, - "score": futsalRes.Score, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, futsalRes.EventID, futsalRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range futsalRes.Others { - if err := s.storeSection(ctx, futsalRes.EventID, futsalRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseAmericanFootball(ctx context.Context, res json.RawMessage) error { - var americanFootballRes domain.AmericanFootballOddsResponse - if err := json.Unmarshal(res, &americanFootballRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if americanFootballRes.EventID == "" && americanFootballRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "half_props": americanFootballRes.HalfProps, - "main": americanFootballRes.Main, - "quarter_props": americanFootballRes.QuarterProps, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, americanFootballRes.EventID, americanFootballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range americanFootballRes.Others { - if err := s.storeSection(ctx, americanFootballRes.EventID, americanFootballRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseRugbyLeague(ctx context.Context, res json.RawMessage) error { - var rugbyLeagueRes domain.RugbyLeagueOddsResponse - if err := json.Unmarshal(res, &rugbyLeagueRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if rugbyLeagueRes.EventID == "" && rugbyLeagueRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "10minute": rugbyLeagueRes.TenMinute, - "main": rugbyLeagueRes.Main, - "main_2": rugbyLeagueRes.Main2, - "player": rugbyLeagueRes.Player, - "Score": rugbyLeagueRes.Score, - "Team": rugbyLeagueRes.Team, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, rugbyLeagueRes.EventID, rugbyLeagueRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range rugbyLeagueRes.Others { - if err := s.storeSection(ctx, rugbyLeagueRes.EventID, rugbyLeagueRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseRugbyUnion(ctx context.Context, res json.RawMessage) error { - var rugbyUnionRes domain.RugbyUnionOddsResponse - if err := json.Unmarshal(res, &rugbyUnionRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if rugbyUnionRes.EventID == "" && rugbyUnionRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": rugbyUnionRes.Main, - "main_2": rugbyUnionRes.Main2, - "player": rugbyUnionRes.Player, - "Score": rugbyUnionRes.Score, - "Team": rugbyUnionRes.Team, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, rugbyUnionRes.EventID, rugbyUnionRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range rugbyUnionRes.Others { - if err := s.storeSection(ctx, rugbyUnionRes.EventID, rugbyUnionRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseBaseball(ctx context.Context, res json.RawMessage) error { - var baseballRes domain.BaseballOddsResponse - if err := json.Unmarshal(res, &baseballRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if baseballRes.EventID == "" && baseballRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": baseballRes.Main, - "mani_props": baseballRes.MainProps, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, baseballRes.EventID, baseballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil + return domain.ParseOddSectionsRes{ + Sections: sections, + OtherRes: OtherRes, + EventFI: eventFI, + }, nil } func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section domain.OddsSection) error { @@ -725,21 +435,20 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName // Check if the market id is a string var marketIDstr string err := json.Unmarshal(market.ID, &marketIDstr) + var marketIDint int64 if err != nil { // check if its int - var marketIDint int err := json.Unmarshal(market.ID, &marketIDint) if err != nil { s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) - errs = append(errs, err) + continue + } + } else { + marketIDint, err = strconv.ParseInt(marketIDstr, 10, 64) + if err != nil { + s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) + continue } - } - - marketIDint, err := strconv.ParseInt(marketIDstr, 10, 64) - if err != nil { - s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) - errs = append(errs, err) - continue } isSupported, ok := domain.SupportedMarkets[marketIDint] @@ -808,6 +517,10 @@ func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) } +func (s *ServiceImpl) DeleteOddsForEvent(ctx context.Context, eventID string) error { + return s.store.DeleteOddsForEvent(ctx, eventID) +} + func getString(v interface{}) string { if str, ok := v.(string); ok { return str diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 7edd1f3..189a0e3 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log" "log/slog" "net/http" "strconv" @@ -14,51 +15,63 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" ) type Service struct { - repo *repository.Store - config *config.Config - logger *slog.Logger - client *http.Client - betSvc bet.Service + repo *repository.Store + config *config.Config + logger *slog.Logger + client *http.Client + betSvc bet.Service + oddSvc odds.ServiceImpl + eventSvc event.Service + leagueSvc league.Service } -func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service) *Service { +func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service) *Service { return &Service{ - repo: repo, - config: cfg, - logger: logger, - client: &http.Client{Timeout: 10 * time.Second}, - betSvc: betSvc, + repo: repo, + config: cfg, + logger: logger, + client: &http.Client{Timeout: 10 * time.Second}, + betSvc: betSvc, + oddSvc: oddSvc, + eventSvc: eventSvc, + leagueSvc: leagueSvc, } } var ( - ErrEventIsNotActive = fmt.Errorf("Event has been cancelled or postponed") + ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed") ) func (s *Service) FetchAndProcessResults(ctx context.Context) error { // TODO: Optimize this because there could be many bet outcomes for the same odd // Take market id and match result as param and update all the bet outcomes at the same time - events, err := s.repo.GetExpiredUpcomingEvents(ctx) + events, err := s.repo.GetExpiredUpcomingEvents(ctx, domain.EventFilter{}) if err != nil { s.logger.Error("Failed to fetch events") return err } fmt.Printf("⚠️ Expired Events: %d \n", len(events)) removed := 0 + errs := make([]error, 0, len(events)) for i, event := range events { eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { s.logger.Error("Failed to parse event id") - return err + errs = append(errs, fmt.Errorf("failed to parse event id %s: %w", event.ID, err)) + continue } outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID) if err != nil { s.logger.Error("Failed to get pending bet outcomes", "error", err) - return err + errs = append(errs, fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)) + continue } if len(outcomes) == 0 { @@ -68,6 +81,40 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { } isDeleted := true + result, err := s.fetchResult(ctx, eventID) + if err != nil { + if err == ErrEventIsNotActive { + s.logger.Warn("Event is not active", "event_id", eventID, "error", err) + continue + } + s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) + continue + } + var commonResp domain.CommonResultResponse + if err := json.Unmarshal(result.Results[0], &commonResp); err != nil { + s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err) + continue + } + + sportID, err := strconv.ParseInt(commonResp.SportID, 10, 64) + if err != nil { + s.logger.Error("Failed to parse sport id", "event_id", eventID, "error", err) + continue + } + timeStatusParsed, err := strconv.ParseInt(strings.TrimSpace(commonResp.TimeStatus), 10, 64) + if err != nil { + s.logger.Error("Failed to parse time status", "time_status", commonResp.TimeStatus, "error", err) + continue + } + + // TODO: Figure out what to do with the events that have been cancelled or postponed, etc... + if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) { + s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus) + fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events)) + isDeleted = false + continue + } + for j, outcome := range outcomes { fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", outcome.MarketName, @@ -80,23 +127,14 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { continue } - // TODO: optimize this because the result is being fetched for each outcome which will have the same event id but different market id - result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, int64(event.SportID), outcome) + parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) if err != nil { - if err == ErrEventIsNotActive { - s.logger.Warn("Event is not active", "event_id", outcome.EventID, "error", err) - continue - } - fmt.Printf("❌ failed to parse 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", - outcome.MarketName, - event.HomeTeam+" "+event.AwayTeam, event.ID, - j+1, len(outcomes)) - s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "market_id", outcome.MarketID, "market", outcome.MarketName, "error", err) isDeleted = false + s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err) + errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err)) continue } - - outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status) + outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status) if err != nil { isDeleted = false s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) @@ -147,113 +185,389 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) return err } + err = s.repo.DeleteOddsForEvent(ctx, event.ID) + if err != nil { + s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err) + return err + } } } fmt.Printf("🗑️ Removed Events: %d \n", removed) - + if len(errs) > 0 { + s.logger.Error("Errors occurred while processing results", "errors", errs) + for _, err := range errs { + fmt.Println("Error:", err) + } + return fmt.Errorf("errors occurred while processing results: %v", errs) + } + s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events)) return nil } -func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, sportID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { - // url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) - url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID) +func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error) { + events, err := s.repo.GetExpiredUpcomingEvents(ctx, domain.EventFilter{}) + if err != nil { + s.logger.Error("Failed to fetch events") + return 0, err + } + fmt.Printf("⚠️ Expired Events: %d \n", len(events)) + updated := 0 + for i, event := range events { + fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events)) + eventID, err := strconv.ParseInt(event.ID, 10, 64) + if err != nil { + s.logger.Error("Failed to parse event id") + continue + } + + if event.Status == domain.STATUS_REMOVED { + s.logger.Info("Skipping updating removed event") + continue + } + result, err := s.fetchResult(ctx, eventID) + if err != nil { + s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) + continue + } + if result.Success != 1 || len(result.Results) == 0 { + s.logger.Error("Invalid API response", "event_id", eventID) + fmt.Printf("⚠️ Invalid API response for event %v \n", result) + continue + } + + var commonResp domain.CommonResultResponse + if err := json.Unmarshal(result.Results[0], &commonResp); err != nil { + s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err) + continue + } + + var eventStatus domain.EventStatus + // TODO Change event status to int64 enum + timeStatus, err := strconv.ParseInt(strings.TrimSpace(commonResp.TimeStatus), 10, 64) + switch timeStatus { + case int64(domain.TIME_STATUS_NOT_STARTED): + eventStatus = domain.STATUS_PENDING + case int64(domain.TIME_STATUS_IN_PLAY): + eventStatus = domain.STATUS_IN_PLAY + case int64(domain.TIME_STATUS_TO_BE_FIXED): + eventStatus = domain.STATUS_TO_BE_FIXED + case int64(domain.TIME_STATUS_ENDED): + eventStatus = domain.STATUS_ENDED + case int64(domain.TIME_STATUS_POSTPONED): + eventStatus = domain.STATUS_POSTPONED + case int64(domain.TIME_STATUS_CANCELLED): + eventStatus = domain.STATUS_CANCELLED + case int64(domain.TIME_STATUS_WALKOVER): + eventStatus = domain.STATUS_WALKOVER + case int64(domain.TIME_STATUS_INTERRUPTED): + eventStatus = domain.STATUS_INTERRUPTED + case int64(domain.TIME_STATUS_ABANDONED): + eventStatus = domain.STATUS_ABANDONED + case int64(domain.TIME_STATUS_RETIRED): + eventStatus = domain.STATUS_RETIRED + case int64(domain.TIME_STATUS_SUSPENDED): + eventStatus = domain.STATUS_SUSPENDED + case int64(domain.TIME_STATUS_DECIDED_BY_FA): + eventStatus = domain.STATUS_DECIDED_BY_FA + case int64(domain.TIME_STATUS_REMOVED): + eventStatus = domain.STATUS_REMOVED + default: + s.logger.Error("Invalid time status", "time_status", commonResp.TimeStatus, "event_id", eventID) + } + + err = s.eventSvc.UpdateFinalScore(ctx, strconv.FormatInt(eventID, 10), commonResp.SS, eventStatus) + if err != nil { + s.logger.Error("Failed to update final score", "event_id", eventID, "error", err) + continue + } + updated++ + fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events)) + + // Update the league because the league country code is only found on the result response + leagueID, err := strconv.ParseInt(commonResp.League.ID, 10, 64) + if err != nil { + log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID) + continue + } + + err = s.leagueSvc.UpdateLeague(ctx, domain.UpdateLeague{ + ID: int64(event.LeagueID), + CountryCode: domain.ValidString{ + Value: commonResp.League.CC, + Valid: true, + }, + Bet365ID: domain.ValidInt32{ + Value: int32(leagueID), + Valid: true, + }, + }) + + if err != nil { + log.Printf("❌ Error Updating League %v", commonResp.League.Name) + log.Printf("err:%v", err) + continue + } + fmt.Printf("✅ Updated League %v with country code %v \n", leagueID, commonResp.League.CC) + + } + + if updated == 0 { + s.logger.Info("No events were updated") + return 0, nil + } + + s.logger.Info("Successfully updated live events", "updated_events", updated, "total_events", len(events)) + return int64(updated), nil + +} + +func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json.RawMessage, []domain.BetOutcome, error) { + id, err := strconv.ParseInt(eventID, 10, 64) + if err != nil { + s.logger.Error("Failed to parse event id") + return json.RawMessage{}, nil, err + } + + result, err := s.fetchResult(ctx, id) + if err != nil { + s.logger.Error("Failed to fetch result", "event_id", id, "error", err) + } + if result.Success != 1 || len(result.Results) == 0 { + fmt.Printf("⚠️ Invalid API response for event %v \n", result) + s.logger.Error("Invalid API response", "event_id", id) + return json.RawMessage{}, nil, fmt.Errorf("invalid API response for event %d", id) + } + + var commonResp domain.CommonResultResponse + if err := json.Unmarshal(result.Results[0], &commonResp); err != nil { + s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err) + return json.RawMessage{}, nil, err + } + + sportID, err := strconv.ParseInt(commonResp.SportID, 10, 32) + if err != nil { + s.logger.Error("Failed to parse sport id", "event_id", eventID, "error", err) + return json.RawMessage{}, nil, fmt.Errorf("failed to parse sport id: %w", err) + } + + expireUnix, err := strconv.ParseInt(commonResp.Time, 10, 64) + if err != nil { + s.logger.Error("Failed to parse expire time", "event_id", eventID, "error", err) + return json.RawMessage{}, nil, fmt.Errorf("Failed to parse expire time for event %s: %w", eventID, err) + } + + expires := time.Unix(expireUnix, 0) + + odds, err := s.oddSvc.FetchNonLiveOddsByEventID(ctx, eventID) + if err != nil { + s.logger.Error("Failed to fetch non-live odds by event ID", "event_id", eventID, "error", err) + return json.RawMessage{}, nil, fmt.Errorf("failed to fetch non-live odds for event %s: %w", eventID, err) + } + + parsedOddSections, err := s.oddSvc.ParseOddSections(ctx, odds.Results[0], int32(sportID)) + if err != nil { + s.logger.Error("Failed to parse odd section", "error", err) + return json.RawMessage{}, nil, fmt.Errorf("failed to parse odd section for event %v: %w", eventID, err) + } + + outcomes := make([]domain.BetOutcome, 0) + for _, section := range parsedOddSections.Sections { + // TODO: Remove repeat code here, same as in odds service + for _, market := range section.Sp { + var marketIDstr string + err := json.Unmarshal(market.ID, &marketIDstr) + var marketIDint int64 + if err != nil { + // check if its int + err := json.Unmarshal(market.ID, &marketIDint) + if err != nil { + s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) + continue + } + } else { + marketIDint, err = strconv.ParseInt(marketIDstr, 10, 64) + if err != nil { + s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) + continue + } + } + + isSupported, ok := domain.SupportedMarkets[marketIDint] + + if !ok || !isSupported { + // s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name) + continue + } + for _, oddRes := range market.Odds { + var odd domain.RawOdd + if err := json.Unmarshal(oddRes, &odd); err != nil { + s.logger.Error("Failed to unmarshal odd", "error", err) + continue + } + + oddID, err := strconv.ParseInt(odd.ID, 10, 64) + + if err != nil { + s.logger.Error("Failed to parse odd id", "odd_id", odd.ID, "error", err) + continue + } + + oddValue, err := strconv.ParseFloat(odd.Odds, 64) + if err != nil { + s.logger.Error("Failed to parse odd value", "odd_value", odd.Odds, "error", err) + continue + } + + outcome := domain.BetOutcome{ + EventID: id, + MarketID: marketIDint, + OddID: oddID, + MarketName: market.Name, + OddHeader: odd.Header, + OddHandicap: odd.Handicap, + OddName: odd.Name, + Odd: float32(oddValue), + SportID: sportID, + HomeTeamName: commonResp.Home.Name, + AwayTeamName: commonResp.Away.Name, + Status: domain.OUTCOME_STATUS_PENDING, + Expires: expires, + BetID: 0, // This won't be set + } + outcomes = append(outcomes, outcome) + } + + } + + } + + if len(outcomes) == 0 { + s.logger.Warn("No outcomes found for event", "event_id", eventID) + return json.RawMessage{}, nil, fmt.Errorf("no outcomes found for event %s", eventID) + } + s.logger.Info("Successfully fetched outcomes for event", "event_id", eventID, "outcomes_count", len(outcomes)) + + // Get results for outcome + for i, outcome := range outcomes { + // Parse the result based on sport type + parsedResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) + if err != nil { + s.logger.Error("Failed to parse result for outcome", "event_id", outcome.EventID, "error", err) + return json.RawMessage{}, nil, fmt.Errorf("failed to parse result for outcome %d: %w", i, err) + } + outcomes[i].Status = parsedResult.Status + } + + return result.Results[0], outcomes, err +} + +func (s *Service) fetchResult(ctx context.Context, eventID int64) (domain.BaseResultResponse, error) { + url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) + // url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { s.logger.Error("Failed to create request", "event_id", eventID, "error", err) - return domain.CreateResult{}, err + return domain.BaseResultResponse{}, err } resp, err := s.client.Do(req) if err != nil { s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) - return domain.CreateResult{}, err + return domain.BaseResultResponse{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", resp.StatusCode) - return domain.CreateResult{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + return domain.BaseResultResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var resultResp domain.BaseResultResponse if err := json.NewDecoder(resp.Body).Decode(&resultResp); err != nil { s.logger.Error("Failed to decode result", "event_id", eventID, "error", err) - return domain.CreateResult{}, err + return domain.BaseResultResponse{}, err } if resultResp.Success != 1 || len(resultResp.Results) == 0 { s.logger.Error("Invalid API response", "event_id", eventID) - return domain.CreateResult{}, fmt.Errorf("invalid API response") + fmt.Printf("⚠️ Invalid API response for event %v \n", resultResp) + return domain.BaseResultResponse{}, fmt.Errorf("invalid API response") } + return resultResp, nil +} + +func (s *Service) parseResult(ctx context.Context, resultResp json.RawMessage, outcome domain.BetOutcome, sportID int64) (domain.CreateResult, error) { + var result domain.CreateResult + var err error switch sportID { case domain.FOOTBALL: - result, err = s.parseFootball(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseFootball(resultResp, outcome) if err != nil { - s.logger.Error("Failed to parse football", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse football", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.BASKETBALL: - result, err = s.parseBasketball(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseBasketball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse basketball", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse basketball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.ICE_HOCKEY: - result, err = s.parseIceHockey(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseIceHockey(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse ice hockey", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse ice hockey", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.CRICKET: - result, err = s.parseCricket(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseCricket(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse cricket", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.VOLLEYBALL: - result, err = s.parseVolleyball(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseVolleyball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse volleyball", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse volleyball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.DARTS: - result, err = s.parseDarts(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseDarts(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse darts", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse darts", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.FUTSAL: - result, err = s.parseFutsal(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseFutsal(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse futsal", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse futsal", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.AMERICAN_FOOTBALL: - result, err = s.parseNFL(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseNFL(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse american football", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse american football", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.RUGBY_UNION: - result, err = s.parseRugbyUnion(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseRugbyUnion(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse rugby_union", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse rugby_union", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.RUGBY_LEAGUE: - result, err = s.parseRugbyLeague(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseRugbyLeague(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse rugby_league", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse rugby_league", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.BASEBALL: - result, err = s.parseBaseball(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseBaseball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse baseball", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse baseball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } default: @@ -264,52 +578,14 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo return result, nil } -func (s *Service) parseTimeStatus(timeStatusStr string) (bool, error) { - timeStatusParsed, err := strconv.ParseInt(strings.TrimSpace(timeStatusStr), 10, 64) - if err != nil { - s.logger.Error("Failed to parse time status", "time_status", timeStatusStr, "error", err) - return false, fmt.Errorf("failed to parse time status: %w", err) - } - timeStatus := domain.TimeStatus(timeStatusParsed) - - switch timeStatus { - case domain.TIME_STATUS_NOT_STARTED, domain.TIME_STATUS_IN_PLAY, domain.TIME_STATUS_TO_BE_FIXED, domain.TIME_STATUS_ENDED: - return true, nil - case domain.TIME_STATUS_POSTPONED, - domain.TIME_STATUS_CANCELLED, - domain.TIME_STATUS_WALKOVER, - domain.TIME_STATUS_INTERRUPTED, - domain.TIME_STATUS_ABANDONED, - domain.TIME_STATUS_RETIRED, - domain.TIME_STATUS_SUSPENDED, - domain.TIME_STATUS_DECIDED_BY_FA, - domain.TIME_STATUS_REMOVED: - return false, nil - default: - s.logger.Error("Invalid time status", "time_status", timeStatus) - return false, fmt.Errorf("invalid time status: %d", timeStatus) - } - -} - -func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { +func (s *Service) parseFootball(resultRes json.RawMessage, outcome domain.BetOutcome) (domain.CreateResult, error) { var fbResp domain.FootballResultResponse if err := json.Unmarshal(resultRes, &fbResp); err != nil { - s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + s.logger.Error("Failed to unmarshal football result", "event_id", outcome.EventID, "error", err) return domain.CreateResult{}, err } result := fbResp - isEventActive, err := s.parseTimeStatus(result.TimeStatus) - if err != nil { - s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err) - return domain.CreateResult{}, err - } - if !isEventActive { - s.logger.Warn("Event is not active", "event_id", eventID) - return domain.CreateResult{}, ErrEventIsNotActive - } - finalScore := parseSS(result.SS) firstHalfScore := parseScore(result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away) secondHalfScore := parseScore(result.Scores.SecondHalf.Home, result.Scores.SecondHalf.Away) @@ -318,15 +594,15 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke halfTimeCorners := parseStats(result.Stats.HalfTimeCorners) status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, result.Events) if err != nil { - s.logger.Error("Failed to evaluate football outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to evaluate football outcome", "event_id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } return domain.CreateResult{ BetOutcomeID: 0, - EventID: eventID, - OddID: oddID, - MarketID: marketID, + EventID: outcome.EventID, + OddID: outcome.OddID, + MarketID: outcome.MarketID, Status: status, Score: result.SS, }, nil @@ -340,15 +616,6 @@ func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, mark s.logger.Error("Failed to unmarshal basketball result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } - isEventActive, err := s.parseTimeStatus(basketBallRes.TimeStatus) - if err != nil { - s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err) - return domain.CreateResult{}, err - } - if !isEventActive { - s.logger.Warn("Event is not active", "event_id", eventID) - return domain.CreateResult{}, ErrEventIsNotActive - } status, err := s.evaluateBasketballOutcome(outcome, basketBallRes) if err != nil { @@ -373,15 +640,6 @@ func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marke s.logger.Error("Failed to unmarshal ice hockey result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } - isEventActive, err := s.parseTimeStatus(iceHockeyRes.TimeStatus) - if err != nil { - s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err) - return domain.CreateResult{}, err - } - if !isEventActive { - s.logger.Warn("Event is not active", "event_id", eventID) - return domain.CreateResult{}, ErrEventIsNotActive - } status, err := s.evaluateIceHockeyOutcome(outcome, iceHockeyRes) if err != nil { diff --git a/internal/services/ticket/port.go b/internal/services/ticket/port.go index 930026e..d4201e3 100644 --- a/internal/services/ticket/port.go +++ b/internal/services/ticket/port.go @@ -11,6 +11,7 @@ type TicketStore interface { CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, error) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) + CountTicketByIP(ctx context.Context, IP string) (int64, error) UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error DeleteOldTickets(ctx context.Context) error DeleteTicket(ctx context.Context, id int64) error diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index 509f353..67c8a5a 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -31,6 +31,10 @@ func (s *Service) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) return s.ticketStore.GetAllTickets(ctx) } +func (s *Service) CountTicketByIP(ctx context.Context, IP string) (int64, error) { + return s.ticketStore.CountTicketByIP(ctx, IP) +} + func (s *Service) UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { return s.ticketStore.UpdateTicketOutcomeStatus(ctx, id, status) } diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go index c61cd01..f8bd8f9 100644 --- a/internal/services/user/direct.go +++ b/internal/services/user/direct.go @@ -71,7 +71,7 @@ func (s *Service) GetCashiersByBranch(ctx context.Context, branchID int64) ([]do return s.userStore.GetCashiersByBranch(ctx, branchID) } -func (s *Service) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, error) { +func (s *Service) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, int64, error){ return s.userStore.GetAllCashiers(ctx) } diff --git a/internal/services/user/port.go b/internal/services/user/port.go index c8149df..2a9f9f8 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -11,7 +11,7 @@ type UserStore interface { CreateUserWithoutOtp(ctx context.Context, user domain.User, is_company bool) (domain.User, error) GetUserByID(ctx context.Context, id int64) (domain.User, error) GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, int64, error) - GetAllCashiers(ctx context.Context) ([]domain.GetCashier, error) + GetAllCashiers(ctx context.Context) ([]domain.GetCashier, int64, error) GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error) GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error diff --git a/internal/services/user/reset.go b/internal/services/user/reset.go index 70309a8..c6d3f47 100644 --- a/internal/services/user/reset.go +++ b/internal/services/user/reset.go @@ -2,6 +2,7 @@ package user import ( "context" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -33,6 +34,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo } else { sentTo = resetReq.PhoneNumber } + otp, err := s.otpStore.GetOtp( ctx, sentTo, domain.OtpReset, resetReq.OtpMedium) @@ -55,6 +57,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo return err } // reset pass and mark otp as used + err = s.userStore.UpdatePassword(ctx, sentTo, hashedPassword, otp.ID) if err != nil { return err diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 3b7baec..9aef6cc 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -39,26 +39,38 @@ func SetupReportCronJob(reportWorker *worker.ReportWorker) { s.StartAsync() } -func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service, resultService *resultsvc.Service) { +func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service) { c := cron.New(cron.WithSeconds()) schedule := []struct { 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 + spec: "0 */5 * * * *", // Every 5 Minutes task: func() { - if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - log.Printf("FetchUpcomingEvents error: %v", err) - } - }, - }, - { - spec: "0 */15 * * * *", // Every 15 minutes - task: func() { - if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - log.Printf("FetchNonLiveOdds error: %v", err) + log.Println("Updating expired events status...") + + if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { + log.Printf("Failed to update events: %v", err) + } else { + log.Printf("Successfully updated expired events") } }, }, @@ -77,7 +89,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } for _, job := range schedule { - // job.task() + job.task() if _, err := c.AddFunc(job.spec, job.task); err != nil { log.Fatalf("Failed to schedule cron job: %v", err) } diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 22bb7ca..a7a0706 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "strconv" "time" @@ -23,7 +24,7 @@ import ( // @Failure 500 {object} response.APIResponse // @Router /bet [post] func (h *Handler) CreateBet(c *fiber.Ctx) error { - + fmt.Printf("Calling leagues") // Get user_id from middleware userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) @@ -39,7 +40,8 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role) + res, err := h.betSvc. + PlaceBet(c.Context(), req, userID, role) if err != nil { h.logger.Error("PlaceBet failed", "error", err) diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index 395ba19..6f869a1 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -58,15 +58,16 @@ type BranchRes struct { } type BranchDetailRes 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"` - ManagerName string `json:"manager_name" example:"John Smith"` - ManagerPhoneNumber string `json:"manager_phone_number" example:"0911111111"` + 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"` + ManagerName string `json:"manager_name" example:"John Smith"` + ManagerPhoneNumber string `json:"manager_phone_number" example:"0911111111"` + Balance float32 `json:"balance" example:"100.5"` } func convertBranch(branch domain.Branch) BranchRes { @@ -92,6 +93,7 @@ func convertBranchDetail(branch domain.BranchDetail) BranchDetailRes { IsSelfOwned: branch.IsSelfOwned, ManagerName: branch.ManagerName, ManagerPhoneNumber: branch.ManagerPhoneNumber, + Balance: branch.Balance.Float32(), } } @@ -552,6 +554,50 @@ func (h *Handler) GetBranchCashiers(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Branch Cashiers retrieved successfully", result, nil) } +// GetBranchForCashier godoc +// @Summary Gets branch for cahier +// @Description Gets branch for cahier +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "Branch ID" +// @Success 200 {object} BranchDetailRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branchCashier [get] +func (h *Handler) GetBranchForCashier(c *fiber.Ctx) error { + cashierID, ok := c.Locals("user_id").(int64) + + if !ok { + h.logger.Error("Invalid cashier ID in context") + return response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid cashier identification", nil, nil) + } + + role, ok := c.Locals("role").(domain.Role) + + if !ok || role != domain.RoleCashier { + h.logger.Error("Unauthorized access", "cashierID", cashierID, "role", role) + return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) + } + + branchID, ok := c.Locals("branch_id").(domain.ValidInt64) + if !ok || !branchID.Valid { + h.logger.Error("Invalid branch ID in context", "cashierID", cashierID) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", nil, nil) + } + + branch, err := h.branchSvc.GetBranchByID(c.Context(), branchID.Value) + + if err != nil { + h.logger.Error("Failed to get branch by ID", "branchID", branchID.Value, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve branch", err, nil) + } + + res := convertBranchDetail(branch) + + return response.WriteJSON(c, fiber.StatusOK, "Branch retrieved successfully", res, nil) +} + // GetBetByBranchID godoc // @Summary Gets bets by its branch id // @Description Gets bets by its branch id diff --git a/internal/web_server/handlers/cashier.go b/internal/web_server/handlers/cashier.go index 18efb18..6dc18e7 100644 --- a/internal/web_server/handlers/cashier.go +++ b/internal/web_server/handlers/cashier.go @@ -85,20 +85,23 @@ func (h *Handler) CreateCashier(c *fiber.Ctx) error { } type GetCashierRes struct { - ID int64 `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - Role domain.Role `json:"role"` - EmailVerified bool `json:"email_verified"` - PhoneVerified bool `json:"phone_verified"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - SuspendedAt time.Time `json:"suspended_at"` - Suspended bool `json:"suspended"` - LastLogin time.Time `json:"last_login"` - BranchID int64 `json:"branch_id"` + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + Role domain.Role `json:"role"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + SuspendedAt time.Time `json:"suspended_at"` + Suspended bool `json:"suspended"` + LastLogin time.Time `json:"last_login"` + BranchID int64 `json:"branch_id"` + BranchName string `json:"branch_name"` + BranchWallet int64 `json:"branch_wallet"` + BranchLocation string `json:"branch_location"` } // GetAllCashiers godoc @@ -139,7 +142,7 @@ func (h *Handler) GetAllCashiers(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - cashiers, total, err := h.userSvc.GetAllUsers(c.Context(), filter) + cashiers, total, err := h.userSvc.GetAllCashiers(c.Context()) if err != nil { h.logger.Error("GetAllCashiers failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) @@ -159,19 +162,23 @@ func (h *Handler) GetAllCashiers(c *fiber.Ctx) error { } result = append(result, GetCashierRes{ - ID: cashier.ID, - FirstName: cashier.FirstName, - LastName: cashier.LastName, - Email: cashier.Email, - PhoneNumber: cashier.PhoneNumber, - Role: cashier.Role, - EmailVerified: cashier.EmailVerified, - PhoneVerified: cashier.PhoneVerified, - CreatedAt: cashier.CreatedAt, - UpdatedAt: cashier.UpdatedAt, - SuspendedAt: cashier.SuspendedAt, - Suspended: cashier.Suspended, - LastLogin: *lastLogin, + ID: cashier.ID, + FirstName: cashier.FirstName, + LastName: cashier.LastName, + Email: cashier.Email, + PhoneNumber: cashier.PhoneNumber, + Role: cashier.Role, + EmailVerified: cashier.EmailVerified, + PhoneVerified: cashier.PhoneVerified, + CreatedAt: cashier.CreatedAt, + UpdatedAt: cashier.UpdatedAt, + SuspendedAt: cashier.SuspendedAt, + Suspended: cashier.Suspended, + LastLogin: *lastLogin, + BranchID: cashier.BranchID, + BranchName: cashier.BranchName, + BranchWallet: cashier.BranchWallet, + BranchLocation: cashier.BranchLocation, }) } @@ -215,6 +222,7 @@ func (h *Handler) GetCashierByID(c *fiber.Ctx) error { } user, err := h.userSvc.GetCashierByID(c.Context(), cashierID) + if err != nil { h.logger.Error("Get User By ID failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) @@ -230,20 +238,23 @@ func (h *Handler) GetCashierByID(c *fiber.Ctx) error { } res := GetCashierRes{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email, - PhoneNumber: user.PhoneNumber, - Role: user.Role, - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - SuspendedAt: user.SuspendedAt, - Suspended: user.Suspended, - LastLogin: *lastLogin, - BranchID: user.BranchID, + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + SuspendedAt: user.SuspendedAt, + Suspended: user.Suspended, + LastLogin: *lastLogin, + BranchID: user.BranchID, + BranchName: user.BranchName, + BranchWallet: user.BranchWallet, + BranchLocation: user.BranchLocation, } return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil) } diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 22e1b82..1bf2422 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -15,6 +15,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" @@ -48,6 +49,7 @@ type Handler struct { veliVirtualGameSvc veli.VeliVirtualGameService recommendationSvc recommendation.RecommendationService authSvc *authentication.Service + resultSvc result.Service jwtConfig jwtutil.JwtConfig validator *customvalidator.CustomValidator Cfg *config.Config @@ -76,6 +78,7 @@ func New( prematchSvc *odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service, + resultSvc result.Service, cfg *config.Config, ) *Handler { return &Handler{ @@ -100,6 +103,7 @@ func New( veliVirtualGameSvc: veliVirtualGameSvc, recommendationSvc: recommendationSvc, authSvc: authSvc, + resultSvc: resultSvc, jwtConfig: jwtConfig, Cfg: cfg, } diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index d4f78ee..9bd3299 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -1,22 +1,85 @@ package handlers import ( + "fmt" "strconv" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) +// GetAllLeagues godoc +// @Summary Gets all leagues +// @Description Gets all leagues +// @Tags leagues +// @Accept json +// @Produce json +// @Success 200 {array} domain.League +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /leagues [get] func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { - leagues, err := h.leagueSvc.GetAllLeagues(c.Context()) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get leagues", err, nil) + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("page_size", 10) + + limit := domain.ValidInt64{ + Value: int64(pageSize), + Valid: pageSize == 0, + } + offset := domain.ValidInt64{ + Value: int64(page - 1), + Valid: true, } - return response.WriteJSON(c, fiber.StatusOK, "All leagues retrived", leagues, nil) + countryCodeQuery := c.Query("cc") + countryCode := domain.ValidString{ + Value: countryCodeQuery, + Valid: countryCodeQuery != "", + } + isActiveQuery := c.QueryBool("is_active", false) + isActiveFilter := c.QueryBool("is_active_filter", false) + isActive := domain.ValidBool{ + Value: isActiveQuery, + Valid: isActiveFilter, + } + + sportIDQuery := c.Query("sport_id") + var sportID domain.ValidInt32 + if sportIDQuery != "" { + sportIDint, err := strconv.Atoi(sportIDQuery) + if err != nil { + h.logger.Error("invalid sport id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) + } + sportID = domain.ValidInt32{ + Value: int32(sportIDint), + Valid: true, + } + } + + leagues, err := h.leagueSvc.GetAllLeagues(c.Context(), domain.LeagueFilter{ + CountryCode: countryCode, + IsActive: isActive, + SportID: sportID, + Limit: limit, + Offset: offset, + }) + + if err != nil { + fmt.Printf("Error fetching league %v \n", err) + h.logger.Error("Failed to get leagues", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get leagues", err, nil) + } + return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", leagues, nil) +} + +type SetLeagueActiveReq struct { + IsActive bool `json:"is_active"` } func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { + fmt.Printf("Set Active Leagues") leagueIdStr := c.Params("id") if leagueIdStr == "" { response.WriteJSON(c, fiber.StatusBadRequest, "Missing league id", nil, nil) @@ -26,7 +89,18 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) } - if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId)); err != nil { + var req SetLeagueActiveReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("SetLeagueReq failed", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Failed to parse request", err, nil) + } + + valErrs, ok := h.validator.Validate(c, req) + if !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId), req.IsActive); err != nil { response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update league", err, nil) } diff --git a/internal/web_server/handlers/notification_handler.go b/internal/web_server/handlers/notification_handler.go index 3f6f6b8..24332d0 100644 --- a/internal/web_server/handlers/notification_handler.go +++ b/internal/web_server/handlers/notification_handler.go @@ -78,11 +78,10 @@ func (h *Handler) ConnectSocket(c *fiber.Ctx) error { } h.notificationSvc.Hub.Register <- client - h.logger.Info("WebSocket connection established", "userID", userID) + // h.logger.Info("WebSocket connection established", "userID", userID) defer func() { h.notificationSvc.Hub.Unregister <- client - h.logger.Info("WebSocket connection closed", "userID", userID) conn.Close() }() diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 89d5dfd..7117d1d 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "strconv" "time" @@ -9,33 +10,6 @@ import ( "github.com/gofiber/fiber/v2" ) -// GetPrematchOdds godoc -// @Summary Retrieve prematch odds for an event -// @Description Retrieve prematch odds for a specific event by event ID -// @Tags prematch -// @Accept json -// @Produce json -// @Param event_id path string true "Event ID" -// @Success 200 {array} domain.Odd -// @Failure 400 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /prematch/odds/{event_id} [get] -func (h *Handler) GetPrematchOdds(c *fiber.Ctx) error { - - eventID := c.Params("event_id") - if eventID == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing event_id", nil, nil) - } - - odds, err := h.prematchSvc.GetPrematchOdds(c.Context(), eventID) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve odds", nil, nil) - } - - return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) - -} - // GetALLPrematchOdds // @Summary Retrieve all prematch odds // @Description Retrieve all prematch odds from the database @@ -44,7 +18,7 @@ func (h *Handler) GetPrematchOdds(c *fiber.Ctx) error { // @Produce json // @Success 200 {array} domain.Odd // @Failure 500 {object} response.APIResponse -// @Router /prematch/odds [get] +// @Router /odds [get] func (h *Handler) GetALLPrematchOdds(c *fiber.Ctx) error { odds, err := h.prematchSvc.GetALLPrematchOdds(c.Context()) @@ -67,7 +41,7 @@ func (h *Handler) GetALLPrematchOdds(c *fiber.Ctx) error { // @Success 200 {array} domain.RawOddsByMarketID // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /prematch/odds/upcoming/{upcoming_id}/market/{market_id} [get] +// @Router /odds/upcoming/{upcoming_id}/market/{market_id} [get] func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { marketID := c.Params("market_id") @@ -82,7 +56,8 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) if err != nil { - // h.logger.Error("failed to fetch raw odds", "error", err) + fmt.Printf("Failed to fetch raw odds: %v market_id:%v upcomingID:%v\n", err, marketID, upcomingID) + h.logger.Error("failed to fetch raw odds", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", err, nil) } @@ -99,37 +74,51 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { // @Param page_size query int false "Page size" // @Param league_id query string false "League ID Filter" // @Param sport_id query string false "Sport ID Filter" +// @Param cc query string false "Country Code Filter" // @Param first_start_time query string false "Start Time" // @Param last_start_time query string false "End Time" // @Success 200 {array} domain.UpcomingEvent // @Failure 500 {object} response.APIResponse -// @Router /prematch/events [get] +// @Router /events [get] func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 10) - leagueIDQuery, err := strconv.Atoi(c.Query("league_id")) - if err != nil { - h.logger.Error("invalid league id", "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) + limit := domain.ValidInt64{ + Value: int64(pageSize), + Valid: true, + } + offset := domain.ValidInt64{ + Value: int64(page - 1), + Valid: true, } - sportIDQuery, err := strconv.Atoi(c.Query("sport_id")) - if err != nil { - h.logger.Error("invalid sport id", "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) + leagueIDQuery := c.Query("league_id") + var leagueID domain.ValidInt32 + if leagueIDQuery != "" { + leagueIDInt, err := strconv.Atoi(leagueIDQuery) + if err != nil { + h.logger.Error("invalid league id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) + } + leagueID = domain.ValidInt32{ + Value: int32(leagueIDInt), + Valid: true, + } + } + sportIDQuery := c.Query("sport_id") + var sportID domain.ValidInt32 + if sportIDQuery != "" { + sportIDint, err := strconv.Atoi(sportIDQuery) + if err != nil { + h.logger.Error("invalid sport id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) + } + sportID = domain.ValidInt32{ + Value: int32(sportIDint), + Valid: true, + } } firstStartTimeQuery := c.Query("first_start_time") - lastStartTimeQuery := c.Query("last_start_time") - - leagueID := domain.ValidInt32{ - Value: int32(leagueIDQuery), - Valid: leagueIDQuery != 0, - } - sportID := domain.ValidInt32{ - Value: int32(sportIDQuery), - Valid: sportIDQuery != 0, - } - var firstStartTime domain.ValidTime if firstStartTimeQuery != "" { firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) @@ -142,6 +131,8 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { Valid: true, } } + + lastStartTimeQuery := c.Query("last_start_time") var lastStartTime domain.ValidTime if lastStartTimeQuery != "" { lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) @@ -155,17 +146,21 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { } } - limit := domain.ValidInt64{ - Value: int64(pageSize), - Valid: true, + countryCodeQuery := c.Query("cc") + countryCode := domain.ValidString{ + Value: countryCodeQuery, + Valid: countryCodeQuery != "", } - offset := domain.ValidInt64{ - Value: int64(page - 1), - Valid: true, - } - events, total, err := h.eventSvc.GetPaginatedUpcomingEvents( - c.Context(), limit, offset, leagueID, sportID, firstStartTime, lastStartTime) + c.Context(), domain.EventFilter{ + SportID: sportID, + LeagueID: leagueID, + FirstStartTime: firstStartTime, + LastStartTime: lastStartTime, + Limit: limit, + Offset: offset, + CountryCode: countryCode, + }) // fmt.Printf("League ID: %v", leagueID) if err != nil { @@ -186,7 +181,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { // @Success 200 {object} domain.UpcomingEvent // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /prematch/events/{id} [get] +// @Router /events/{id} [get] func (h *Handler) GetUpcomingEventByID(c *fiber.Ctx) error { id := c.Params("id") @@ -214,8 +209,8 @@ func (h *Handler) GetUpcomingEventByID(c *fiber.Ctx) error { // @Success 200 {array} domain.Odd // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /prematch/odds/upcoming/{upcoming_id} [get] -func (h *Handler) GetPrematchOddsByUpcomingID(c *fiber.Ctx) error { +// @Router /odds/upcoming/{upcoming_id} [get] +func (h *Handler) GetOddsByUpcomingID(c *fiber.Ctx) error { upcomingID := c.Params("upcoming_id") if upcomingID == "" { @@ -240,3 +235,29 @@ func (h *Handler) GetPrematchOddsByUpcomingID(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) } + +type UpdateEventStatusReq struct { +} + +// SetEventStatusToRemoved godoc +// @Summary Set the event status to removed +// @Description Set the event status to removed +// @Tags event +// @Accept json +// @Produce json +// @Param id path int true "Event ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /events/{id} [delete] +func (h *Handler) SetEventStatusToRemoved(c *fiber.Ctx) error { + eventID := c.Params("id") + err := h.eventSvc.UpdateEventStatus(c.Context(), eventID, domain.STATUS_REMOVED) + + if err != nil { + h.logger.Error("Failed to update event status", "eventID", eventID, "error", err) + } + + return response.WriteJSON(c, fiber.StatusOK, "Event updated successfully", nil, nil) + +} diff --git a/internal/web_server/handlers/result_handler.go b/internal/web_server/handlers/result_handler.go new file mode 100644 index 0000000..05f752e --- /dev/null +++ b/internal/web_server/handlers/result_handler.go @@ -0,0 +1,47 @@ +package handlers + +import ( + "encoding/json" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "github.com/gofiber/fiber/v2" +) + +type ResultRes struct { + ResultData json.RawMessage `json:"result_data"` + Outcomes []domain.BetOutcome `json:"outcomes"` +} + +// This will take an event ID and return the success results for +// all the odds for that event. +// @Summary Get results for an event +// @Description Get results for an event +// @Tags result +// @Accept json +// @Produce json +// @Param id path string true "Event ID" +// @Success 200 {array} ResultRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /result/{id} [get] +func (h *Handler) GetResultsByEventID(c *fiber.Ctx) error { + eventID := c.Params("id") + if eventID == "" { + h.logger.Error("Event ID is required") + return fiber.NewError(fiber.StatusBadRequest, "Event ID is required") + } + + results, outcomes, err := h.resultSvc.GetResultsForEvent(c.Context(), eventID) + if err != nil { + h.logger.Error("Failed to get results by Event ID", "eventID", eventID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve results") + } + + resultRes := ResultRes{ + ResultData: results, + Outcomes: outcomes, + } + + return response.WriteJSON(c, fiber.StatusOK, "Results retrieved successfully", resultRes, nil) +} diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index de3eeb2..9706d2a 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -65,6 +65,20 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { if len(req.Outcomes) > 30 { return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) } + + if req.Amount > 100000 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with an amount above 100,000 birr", nil, nil) + } + + clientIP := c.IP() + count, err := h.ticketSvc.CountTicketByIP(c.Context(), clientIP) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching user info", nil, nil) + } + + if count > 50 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Ticket Limit reached", nil, nil) + } var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes)) var totalOdds float32 = 1 for _, outcome := range req.Outcomes { @@ -129,10 +143,16 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { OddHandicap: selectedOdd.Handicap, Expires: event.StartTime, }) + + } + totalWinnings := req.Amount * totalOdds + if totalWinnings > 1000000 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with 1,000,000 winnings", nil, nil) } ticket, err := h.ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ Amount: domain.ToCurrency(req.Amount), TotalOdds: totalOdds, + IP: clientIP, }) if err != nil { h.logger.Error("CreateTicketReq failed", "error", err) diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index a7aa599..443e7ce 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -253,7 +253,7 @@ func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error { // return fiber.NewError(fiber.StatusBadRequest, "Invalid company_id") // } - h.logger.Info("Fetching customer wallet", "userID", userID) + // h.logger.Info("Fetching customer wallet", "userID", userID) wallet, err := h.walletSvc.GetWalletsByUser(c.Context(), userID) if err != nil { @@ -265,3 +265,64 @@ func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) } + +// GetWalletForCashier godoc +// @Summary Get wallet for cashier +// @Description Get wallet for cashier +// @Tags cashier +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {object} UserProfileRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /cashierWallet [get] +func (h *Handler) GetWalletForCashier(c *fiber.Ctx) error { + cashierID, ok := c.Locals("user_id").(int64) + + if !ok || cashierID == 0 { + h.logger.Error("Invalid cashier ID in context") + return response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid cashier identification", nil, nil) + } + + role, ok := c.Locals("role").(domain.Role) + + if !ok || role != domain.RoleCashier { + h.logger.Error("Unauthorized access", "cashierID", cashierID, "role", role) + return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) + } + + branchID, ok := c.Locals("branch_id").(domain.ValidInt64) + if !ok || !branchID.Valid { + h.logger.Error("Invalid branch ID in context", "cashierID", cashierID) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", nil, nil) + } + + branch, err := h.branchSvc.GetBranchByID(c.Context(), branchID.Value) + + if err != nil { + h.logger.Error("Failed to get branch by ID", "branchID", branchID.Value, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve branch", err, nil) + } + + wallet, err := h.walletSvc.GetWalletByID(c.Context(), branch.WalletID) + + if err != nil { + h.logger.Error("Failed to get wallet for cashier", "cashierID", cashierID, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallet", err, nil) + } + + res := WalletRes{ + ID: wallet.ID, + Balance: wallet.Balance.Float32(), + IsWithdraw: wallet.IsWithdraw, + IsBettable: wallet.IsBettable, + IsTransferable: wallet.IsTransferable, + IsActive: wallet.IsActive, + UserID: wallet.UserID, + UpdatedAt: wallet.UpdatedAt, + CreatedAt: wallet.CreatedAt, + } + return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) +} diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index 33c8829..3a6303d 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -14,7 +14,7 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { authHeader := c.Get("Authorization") if authHeader == "" { - fmt.Println("Auth Header Missing") + // fmt.Println("Auth Header Missing") return fiber.NewError(fiber.StatusUnauthorized, "Authorization header missing") } @@ -39,7 +39,6 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { if refreshToken == "" { // refreshToken = c.Cookies("refresh_token", "") - // return fiber.NewError(fiber.StatusUnauthorized, "Refresh token missing") } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 49020b3..d40c932 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -42,6 +42,7 @@ func (a *App) initAppRoutes() { a.prematchSvc, a.eventSvc, a.leagueSvc, + *a.resultSvc, a.cfg, ) @@ -121,17 +122,19 @@ func (a *App) initAppRoutes() { a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers) a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID) - a.fiber.Get("/events/odds/:event_id", h.GetPrematchOdds) - a.fiber.Get("/events/odds", h.GetALLPrematchOdds) - a.fiber.Get("/events/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) + a.fiber.Get("/odds", h.GetALLPrematchOdds) + a.fiber.Get("/odds/upcoming/:upcoming_id", h.GetOddsByUpcomingID) + a.fiber.Get("/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) - a.fiber.Get("/events/:id", h.GetUpcomingEventByID) a.fiber.Get("/events", h.GetAllUpcomingEvents) - a.fiber.Get("/events/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) + a.fiber.Get("/events/:id", h.GetUpcomingEventByID) + a.fiber.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) // Leagues a.fiber.Get("/leagues", h.GetAllLeagues) - a.fiber.Get("/leagues/:id/set-active", h.SetLeagueActive) + a.fiber.Put("/leagues/:id/set-active", h.SetLeagueActive) + + a.fiber.Get("/result/:id", h.GetResultsByEventID) // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) @@ -147,6 +150,7 @@ func (a *App) initAppRoutes() { // /branch/search // branch/wallet a.fiber.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers) + a.fiber.Get("/branchCashier", a.authMiddleware, h.GetBranchForCashier) // Branch Operation a.fiber.Get("/supportedOperation", a.authMiddleware, h.GetAllSupportedOperations) @@ -185,6 +189,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/wallet/:id", h.GetWalletByID) a.fiber.Put("/wallet/:id", h.UpdateWalletActive) a.fiber.Get("/branchWallet", a.authMiddleware, h.GetAllBranchWallets) + a.fiber.Get("/cashierWallet", a.authMiddleware, h.GetWalletForCashier) // Transfer // /transfer/wallet - transfer from one wallet to another wallet diff --git a/internal/web_server/ws/ws.go b/internal/web_server/ws/ws.go index 28fb860..eb0a2ae 100644 --- a/internal/web_server/ws/ws.go +++ b/internal/web_server/ws/ws.go @@ -1,7 +1,6 @@ package ws import ( - "log" "net/http" "sync" @@ -37,7 +36,7 @@ func (h *NotificationHub) Run() { h.mu.Lock() h.Clients[client] = true h.mu.Unlock() - log.Printf("Client registered: %d", client.RecipientID) + // log.Printf("Client registered: %d", client.RecipientID) case client := <-h.Unregister: h.mu.Lock() if _, ok := h.Clients[client]; ok { @@ -45,7 +44,7 @@ func (h *NotificationHub) Run() { client.Conn.Close() } h.mu.Unlock() - log.Printf("Client unregistered: %d", client.RecipientID) + // log.Printf("Client unregistered: %d", client.RecipientID) case message := <-h.Broadcast: h.mu.Lock() for client := range h.Clients { diff --git a/makefile b/makefile index a40a255..3f32fcf 100644 --- a/makefile +++ b/makefile @@ -50,7 +50,7 @@ swagger: .PHONY: db-up db-up: - @docker compose up -d postgres migrate + @docker compose up -d postgres migrate mongo .PHONY: db-down db-down: