diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index c351619..a2568d1 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -234,12 +234,17 @@ CREATE TABLE companies ( wallet_id BIGINT NOT NULL ); -- Views -CREATE VIEW companies_with_wallets AS +CREATE VIEW companies_details AS SELECT companies.*, wallets.balance, - wallets.is_active + wallets.is_active, + users.first_name AS admin_first_name, + users.last_name AS admin_last_name, + users.phone_number AS admin_phone_number FROM companies - JOIN wallets ON wallets.id = companies.wallet_id; + JOIN wallets ON wallets.id = companies.wallet_id + JOIN users ON users.id = companies.admin_id; +; CREATE VIEW branch_details AS SELECT branches.*, CONCAT(users.first_name, ' ', users.last_name) AS manager_name, @@ -290,11 +295,11 @@ ALTER TABLE branch_operations ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id) ON DELETE CASCADE, ADD CONSTRAINT fk_branch_operations_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; ALTER TABLE branch_cashiers -ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id), - ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id); +ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; ALTER TABLE companies ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), - ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id); + ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE; ----------------------------------------------seed data------------------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- CREATE EXTENSION IF NOT EXISTS pgcrypto; @@ -344,7 +349,7 @@ VALUES ( 'Test', 'Admin', 'test.admin@gmail.com', - '0911111111', + '0988554466', crypt('password123', gen_salt('bf'))::bytea, 'admin', TRUE, @@ -400,7 +405,7 @@ VALUES ( 'Kirubel', 'Kibru', 'kirubeljkl679 @gmail.com', - '0911111111', + '0911554486', crypt('password@123', gen_salt('bf'))::bytea, 'super_admin', TRUE, @@ -412,8 +417,7 @@ VALUES ( ); INSERT INTO supported_operations (name, description) VALUES ('SportBook', 'Sportbook operations'), - ('Virtual', 'Virtual operations'), - ('GameZone', 'GameZone operations'); + ('Virtual', 'Virtual operations'); INSERT INTO wallets ( balance, is_withdraw, @@ -433,4 +437,54 @@ VALUES ( TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + ); +INSERT INTO companies ( + name, + admin_id, + wallet_id + ) +values ( + 'Test Company', + 2, + 1 + ); +INSERT INTO wallets ( + balance, + is_withdraw, + is_bettable, + is_transferable, + user_id, + is_active, + created_at, + updated_at + ) +VALUES ( + 10000, + TRUE, + TRUE, + TRUE, + 2, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ); +INSERT INTO branches ( + name, + location, + wallet_id, + branch_manager_id, + company_id, + is_self_owned, + created_at, + updated_at + ) +values ( + 'Test Branch', + 'Addis Ababa', + 2, + 2, + 1, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP ); \ No newline at end of file diff --git a/db/query/company.sql b/db/query/company.sql index 35d37c1..3315132 100644 --- a/db/query/company.sql +++ b/db/query/company.sql @@ -8,14 +8,14 @@ VALUES ($1, $2, $3) RETURNING *; -- name: GetAllCompanies :many SELECT * -FROM companies_with_wallets; +FROM companies_details; -- name: GetCompanyByID :one SELECT * -FROM companies_with_wallets +FROM companies_details WHERE id = $1; -- name: SearchCompanyByName :many SELECT * -FROM companies_with_wallets +FROM companies_details WHERE name ILIKE '%' || $1 || '%'; -- name: UpdateCompany :one UPDATE companies diff --git a/docs/docs.go b/docs/docs.go index 2a56dac..b817783 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -805,6 +805,53 @@ const docTemplate = `{ } } }, + "/branch/{id}/cashier": { + "get": { + "description": "Gets branch cashiers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branch cashiers", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.GetCashierRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/branch/{id}/operation": { "get": { "description": "Gets branch operations", @@ -2756,6 +2803,50 @@ const docTemplate = `{ } } }, + "/user/delete/{id}": { + "delete": { + "description": "Delete a user by their ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Delete user by ID", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/profile": { "get": { "security": [ @@ -3052,7 +3143,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/handlers.UserProfileRes" } }, "400": { @@ -3076,6 +3167,52 @@ const docTemplate = `{ } } }, + "/user/suspend": { + "post": { + "description": "Suspend or unsuspend a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Suspend or unsuspend a user", + "parameters": [ + { + "description": "Suspend or unsuspend a user", + "name": "updateUserSuspend", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateUserSuspendReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.UpdateUserSuspendRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/wallet": { "get": { "security": [ @@ -3601,9 +3738,11 @@ const docTemplate = `{ 1, 2, 3, - 4 + 4, + 5 ], "x-enum-comments": { + "OUTCOME_STATUS_ERROR": "Half Win and Half Given Back", "OUTCOME_STATUS_HALF": "Half Win and Half Given Back", "OUTCOME_STATUS_VOID": "Give Back" }, @@ -3612,7 +3751,8 @@ const docTemplate = `{ "OUTCOME_STATUS_WIN", "OUTCOME_STATUS_LOSS", "OUTCOME_STATUS_VOID", - "OUTCOME_STATUS_HALF" + "OUTCOME_STATUS_HALF", + "OUTCOME_STATUS_ERROR" ] }, "domain.PaymentOption": { @@ -3660,10 +3800,18 @@ const docTemplate = `{ }, "domain.RandomBetReq": { "type": "object", + "required": [ + "branch_id", + "number_of_bets" + ], "properties": { "branch_id": { "type": "integer", "example": 1 + }, + "number_of_bets": { + "type": "integer", + "example": 1 } } }, @@ -4383,6 +4531,50 @@ const docTemplate = `{ } } }, + "handlers.GetCashierRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.ManagersRes": { "type": "object", "properties": { @@ -4733,6 +4925,34 @@ const docTemplate = `{ } } }, + "handlers.UpdateUserSuspendReq": { + "type": "object", + "required": [ + "suspended", + "user_id" + ], + "properties": { + "suspended": { + "type": "boolean", + "example": true + }, + "user_id": { + "type": "integer", + "example": 123 + } + } + }, + "handlers.UpdateUserSuspendRes": { + "type": "object", + "properties": { + "suspended": { + "type": "boolean" + }, + "user_id": { + "type": "integer" + } + } + }, "handlers.UpdateWalletActiveReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 66fa0cd..d4e8cfe 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -797,6 +797,53 @@ } } }, + "/branch/{id}/cashier": { + "get": { + "description": "Gets branch cashiers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branch cashiers", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.GetCashierRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/branch/{id}/operation": { "get": { "description": "Gets branch operations", @@ -2748,6 +2795,50 @@ } } }, + "/user/delete/{id}": { + "delete": { + "description": "Delete a user by their ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Delete user by ID", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/profile": { "get": { "security": [ @@ -3044,7 +3135,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/handlers.UserProfileRes" } }, "400": { @@ -3068,6 +3159,52 @@ } } }, + "/user/suspend": { + "post": { + "description": "Suspend or unsuspend a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Suspend or unsuspend a user", + "parameters": [ + { + "description": "Suspend or unsuspend a user", + "name": "updateUserSuspend", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateUserSuspendReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.UpdateUserSuspendRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/wallet": { "get": { "security": [ @@ -3593,9 +3730,11 @@ 1, 2, 3, - 4 + 4, + 5 ], "x-enum-comments": { + "OUTCOME_STATUS_ERROR": "Half Win and Half Given Back", "OUTCOME_STATUS_HALF": "Half Win and Half Given Back", "OUTCOME_STATUS_VOID": "Give Back" }, @@ -3604,7 +3743,8 @@ "OUTCOME_STATUS_WIN", "OUTCOME_STATUS_LOSS", "OUTCOME_STATUS_VOID", - "OUTCOME_STATUS_HALF" + "OUTCOME_STATUS_HALF", + "OUTCOME_STATUS_ERROR" ] }, "domain.PaymentOption": { @@ -3652,10 +3792,18 @@ }, "domain.RandomBetReq": { "type": "object", + "required": [ + "branch_id", + "number_of_bets" + ], "properties": { "branch_id": { "type": "integer", "example": 1 + }, + "number_of_bets": { + "type": "integer", + "example": 1 } } }, @@ -4375,6 +4523,50 @@ } } }, + "handlers.GetCashierRes": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "last_login": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_verified": { + "type": "boolean" + }, + "role": { + "$ref": "#/definitions/domain.Role" + }, + "suspended": { + "type": "boolean" + }, + "suspended_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, "handlers.ManagersRes": { "type": "object", "properties": { @@ -4725,6 +4917,34 @@ } } }, + "handlers.UpdateUserSuspendReq": { + "type": "object", + "required": [ + "suspended", + "user_id" + ], + "properties": { + "suspended": { + "type": "boolean", + "example": true + }, + "user_id": { + "type": "integer", + "example": 123 + } + } + }, + "handlers.UpdateUserSuspendRes": { + "type": "object", + "properties": { + "suspended": { + "type": "boolean" + }, + "user_id": { + "type": "integer" + } + } + }, "handlers.UpdateWalletActiveReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index fee0fad..0387494 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -165,8 +165,10 @@ definitions: - 2 - 3 - 4 + - 5 type: integer x-enum-comments: + OUTCOME_STATUS_ERROR: Half Win and Half Given Back OUTCOME_STATUS_HALF: Half Win and Half Given Back OUTCOME_STATUS_VOID: Give Back x-enum-varnames: @@ -175,6 +177,7 @@ definitions: - OUTCOME_STATUS_LOSS - OUTCOME_STATUS_VOID - OUTCOME_STATUS_HALF + - OUTCOME_STATUS_ERROR domain.PaymentOption: enum: - 0 @@ -211,6 +214,12 @@ definitions: branch_id: example: 1 type: integer + number_of_bets: + example: 1 + type: integer + required: + - branch_id + - number_of_bets type: object domain.RawOddsByMarketID: properties: @@ -718,6 +727,35 @@ definitions: static_updated_at: type: string type: object + handlers.GetCashierRes: + properties: + created_at: + type: string + email: + type: string + email_verified: + type: boolean + first_name: + type: string + id: + type: integer + last_login: + type: string + last_name: + type: string + phone_number: + type: string + phone_verified: + type: boolean + role: + $ref: '#/definitions/domain.Role' + suspended: + type: boolean + suspended_at: + type: string + updated_at: + type: string + type: object handlers.ManagersRes: properties: created_at: @@ -963,6 +1001,25 @@ definitions: example: true type: boolean type: object + handlers.UpdateUserSuspendReq: + properties: + suspended: + example: true + type: boolean + user_id: + example: 123 + type: integer + required: + - suspended + - user_id + type: object + handlers.UpdateUserSuspendRes: + properties: + suspended: + type: boolean + user_id: + type: integer + type: object handlers.UpdateWalletActiveReq: properties: is_active: @@ -1658,6 +1715,37 @@ paths: summary: Gets bets by its branch id tags: - branch + /branch/{id}/cashier: + get: + consumes: + - application/json + description: Gets branch cashiers + parameters: + - description: Branch ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.GetCashierRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets branch cashiers + tags: + - branch /branch/{id}/operation: get: consumes: @@ -2940,6 +3028,35 @@ paths: summary: Check if phone number or email exist tags: - user + /user/delete/{id}: + delete: + consumes: + - application/json + description: Delete a user by their ID + parameters: + - description: User ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Delete user by ID + tags: + - user /user/profile: get: consumes: @@ -3132,7 +3249,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/response.APIResponse' + $ref: '#/definitions/handlers.UserProfileRes' "400": description: Bad Request schema: @@ -3148,6 +3265,36 @@ paths: summary: Get user by id tags: - user + /user/suspend: + post: + consumes: + - application/json + description: Suspend or unsuspend a user + parameters: + - description: Suspend or unsuspend a user + in: body + name: updateUserSuspend + required: true + schema: + $ref: '#/definitions/handlers.UpdateUserSuspendReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.UpdateUserSuspendRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Suspend or unsuspend a user + tags: + - user /user/wallet: get: consumes: diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index 13a1940..3c5a6b1 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -50,19 +50,19 @@ func (q *Queries) DeleteCompany(ctx context.Context, id int64) error { } const GetAllCompanies = `-- name: GetAllCompanies :many -SELECT id, name, admin_id, wallet_id, balance, is_active -FROM companies_with_wallets +SELECT id, name, admin_id, wallet_id, balance, is_active, admin_first_name, admin_last_name, admin_phone_number +FROM companies_details ` -func (q *Queries) GetAllCompanies(ctx context.Context) ([]CompaniesWithWallet, error) { +func (q *Queries) GetAllCompanies(ctx context.Context) ([]CompaniesDetail, error) { rows, err := q.db.Query(ctx, GetAllCompanies) if err != nil { return nil, err } defer rows.Close() - var items []CompaniesWithWallet + var items []CompaniesDetail for rows.Next() { - var i CompaniesWithWallet + var i CompaniesDetail if err := rows.Scan( &i.ID, &i.Name, @@ -70,6 +70,9 @@ func (q *Queries) GetAllCompanies(ctx context.Context) ([]CompaniesWithWallet, e &i.WalletID, &i.Balance, &i.IsActive, + &i.AdminFirstName, + &i.AdminLastName, + &i.AdminPhoneNumber, ); err != nil { return nil, err } @@ -82,14 +85,14 @@ func (q *Queries) GetAllCompanies(ctx context.Context) ([]CompaniesWithWallet, e } const GetCompanyByID = `-- name: GetCompanyByID :one -SELECT id, name, admin_id, wallet_id, balance, is_active -FROM companies_with_wallets +SELECT id, name, admin_id, wallet_id, balance, is_active, admin_first_name, admin_last_name, admin_phone_number +FROM companies_details WHERE id = $1 ` -func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesWithWallet, error) { +func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesDetail, error) { row := q.db.QueryRow(ctx, GetCompanyByID, id) - var i CompaniesWithWallet + var i CompaniesDetail err := row.Scan( &i.ID, &i.Name, @@ -97,25 +100,28 @@ func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesWithWa &i.WalletID, &i.Balance, &i.IsActive, + &i.AdminFirstName, + &i.AdminLastName, + &i.AdminPhoneNumber, ) return i, err } const SearchCompanyByName = `-- name: SearchCompanyByName :many -SELECT id, name, admin_id, wallet_id, balance, is_active -FROM companies_with_wallets +SELECT id, name, admin_id, wallet_id, balance, is_active, admin_first_name, admin_last_name, admin_phone_number +FROM companies_details WHERE name ILIKE '%' || $1 || '%' ` -func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) ([]CompaniesWithWallet, error) { +func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) ([]CompaniesDetail, error) { rows, err := q.db.Query(ctx, SearchCompanyByName, dollar_1) if err != nil { return nil, err } defer rows.Close() - var items []CompaniesWithWallet + var items []CompaniesDetail for rows.Next() { - var i CompaniesWithWallet + var i CompaniesDetail if err := rows.Scan( &i.ID, &i.Name, @@ -123,6 +129,9 @@ func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) &i.WalletID, &i.Balance, &i.IsActive, + &i.AdminFirstName, + &i.AdminLastName, + &i.AdminPhoneNumber, ); err != nil { return nil, err } diff --git a/gen/db/models.go b/gen/db/models.go index 0cc5956..9b27432 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -146,13 +146,16 @@ type BranchOperation struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } -type CompaniesWithWallet struct { - ID int64 `json:"id"` - Name string `json:"name"` - AdminID int64 `json:"admin_id"` - WalletID int64 `json:"wallet_id"` - Balance int64 `json:"balance"` - IsActive bool `json:"is_active"` +type CompaniesDetail struct { + ID int64 `json:"id"` + Name string `json:"name"` + AdminID int64 `json:"admin_id"` + WalletID int64 `json:"wallet_id"` + Balance int64 `json:"balance"` + IsActive bool `json:"is_active"` + AdminFirstName string `json:"admin_first_name"` + AdminLastName string `json:"admin_last_name"` + AdminPhoneNumber pgtype.Text `json:"admin_phone_number"` } type Company struct { diff --git a/internal/domain/bet.go b/internal/domain/bet.go index e8f4ee2..d681bb8 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -97,7 +97,8 @@ type CreateBetReq struct { } type RandomBetReq struct { - BranchID int64 `json:"branch_id" validate:"required" example:"1"` + BranchID int64 `json:"branch_id" validate:"required" example:"1"` + NumberOfBets int64 `json:"number_of_bets" validate:"required" example:"1"` } type CreateBetRes struct { diff --git a/internal/domain/company.go b/internal/domain/company.go index 9a05e4c..f0a6420 100644 --- a/internal/domain/company.go +++ b/internal/domain/company.go @@ -11,12 +11,15 @@ type Company struct { } type GetCompany struct { - ID int64 - Name string - AdminID int64 - WalletID int64 - WalletBalance Currency - IsWalletActive bool + ID int64 + Name string + AdminID int64 + AdminFirstName string + AdminLastName string + AdminPhoneNumber string + WalletID int64 + WalletBalance Currency + IsWalletActive bool } type CreateCompany struct { diff --git a/internal/domain/league.go b/internal/domain/league.go index 8f63445..a4a9cc2 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -26,11 +26,14 @@ var SupportedLeagues = []int64{ // Basketball 173998768, //NBA 10041830, //NBA + 10049984, //WNBA + 10037165, //German Bundesliga + 10036608, //Italian Lega 1 + 10040795, //EuroLeague // Ice Hockey 10037477, //NHL 10037447, //AHL 10069385, //IIHF World Championship - 10040795, //EuroLeague } diff --git a/internal/domain/result.go b/internal/domain/result.go index fc3a621..3400e4e 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -64,3 +64,21 @@ func (o *OutcomeStatus) String() string { return "UNKNOWN" } } + +type TimeStatus int32 + +const ( + TIME_STATUS_NOT_STARTED TimeStatus = 0 + TIME_STATUS_IN_PLAY TimeStatus = 1 + TIME_STATUS_TO_BE_FIXED TimeStatus = 2 + TIME_STATUS_ENDED TimeStatus = 3 + TIME_STATUS_POSTPONED TimeStatus = 4 + TIME_STATUS_CANCELLED TimeStatus = 5 + TIME_STATUS_WALKOVER TimeStatus = 6 + TIME_STATUS_INTERRUPTED TimeStatus = 7 + TIME_STATUS_ABANDONED TimeStatus = 8 + TIME_STATUS_RETIRED TimeStatus = 9 + TIME_STATUS_SUSPENDED TimeStatus = 10 + TIME_STATUS_DECIDED_BY_FA TimeStatus = 11 + TIME_STATUS_REMOVED TimeStatus = 99 +) diff --git a/internal/domain/resultres.go b/internal/domain/resultres.go index b69a6a9..8a17f24 100644 --- a/internal/domain/resultres.go +++ b/internal/domain/resultres.go @@ -43,6 +43,7 @@ type FootballResultResponse struct { Stats struct { Attacks []string `json:"attacks"` Corners []string `json:"corners"` + HalfTimeCorners []string `json:"corner_h"` DangerousAttacks []string `json:"dangerous_attacks"` Goals []string `json:"goals"` OffTarget []string `json:"off_target"` @@ -94,7 +95,7 @@ type BasketballResultResponse struct { Possession []string `json:"possession"` SuccessAttempts []string `json:"success_attempts"` TimeSpendInLead []string `json:"timespent_inlead"` - Timeuts []string `json:"time_outs"` + TimeOuts []string `json:"time_outs"` } `json:"stats"` Extra struct { HomePos string `json:"home_pos"` @@ -104,7 +105,7 @@ type BasketballResultResponse struct { NumberOfPeriods string `json:"numberofperiods"` PeriodLength string `json:"periodlength"` StadiumData map[string]string `json:"stadium_data"` - Length string `json:"length"` + Length int `json:"length"` Round string `json:"round"` } `json:"extra"` Events []map[string]string `json:"events"` @@ -142,7 +143,7 @@ type IceHockeyResultResponse struct { NumberOfPeriods string `json:"numberofperiods"` PeriodLength string `json:"periodlength"` StadiumData map[string]string `json:"stadium_data"` - Length string `json:"length"` + Length int `json:"length"` Round string `json:"round"` } `json:"extra"` Events []map[string]string `json:"events"` diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go index 360afee..ded71bb 100644 --- a/internal/domain/sportmarket.go +++ b/internal/domain/sportmarket.go @@ -3,12 +3,12 @@ package domain type FootballMarket int64 const ( - FOOTBALL_FULL_TIME_RESULT FootballMarket = 40 //"full_time_result" - FOOTBALL_DOUBLE_CHANCE FootballMarket = 10114 //"double_chance" - FOOTBALL_GOALS_OVER_UNDER FootballMarket = 981 //"goals_over_under" - FOOTBALL_CORRECT_SCORE FootballMarket = 43 //"correct_score" - FOOTBALL_ASIAN_HANDICAP FootballMarket = 938 //"asian_handicap" - FOOTBALL_GOAL_LINE FootballMarket = 10143 //"goal_line" + FOOTBALL_FULL_TIME_RESULT FootballMarket = 40 //"full_time_result" + FOOTBALL_DOUBLE_CHANCE FootballMarket = 10114 //"double_chance" + FOOTBALL_GOALS_OVER_UNDER FootballMarket = 981 //"goals_over_under" + FOOTBALL_CORRECT_SCORE FootballMarket = 43 //"correct_score" + FOOTBALL_ASIAN_HANDICAP FootballMarket = 938 //"asian_handicap" + FOOTBALL_GOAL_LINE FootballMarket = 10143 //"goal_line" FOOTBALL_HALF_TIME_RESULT FootballMarket = 1579 //"half_time_result" FOOTBALL_FIRST_HALF_ASIAN_HANDICAP FootballMarket = 50137 //"1st_half_asian_handicap" @@ -17,7 +17,14 @@ const ( FOOTBALL_GOALS_ODD_EVEN FootballMarket = 10111 //"goals_odd_even" FOOTBALL_DRAW_NO_BET FootballMarket = 10544 //"draw_no_bet" - + FOOTBALL_CORNERS FootballMarket = 760 //"corners" + FOOTBALL_CORNERS_TWO_WAY FootballMarket = 10235 //"corners_2_way" + FOOTBALL_FIRST_HALF_CORNERS FootballMarket = 10539 //"first_half_corners" + FOOTBALL_ASIAN_TOTAL_CORNERS FootballMarket = 10164 //"asian_total_corners" + FOOTBALL_FIRST_HALF_ASIAN_CORNERS FootballMarket = 10233 //"1st_half_asian_corners" + FOOTBALL_FIRST_HALF_GOALS_ODD_EVEN FootballMarket = 10206 //"1st_half_goals_odd_even" + FOOTBALL_SECOND_HALF_GOALS_ODD_EVEN FootballMarket = 50433 //"2nd_half_goals_odd_even" + ) type BasketBallMarket int64 @@ -99,19 +106,26 @@ const ( var SupportedMarkets = map[int64]bool{ // Football Markets - int64(FOOTBALL_FULL_TIME_RESULT): true, //"full_time_result" - int64(FOOTBALL_DOUBLE_CHANCE): true, //"double_chance" - int64(FOOTBALL_GOALS_OVER_UNDER): true, //"goals_over_under" - int64(FOOTBALL_CORRECT_SCORE): true, //"correct_score" - int64(FOOTBALL_ASIAN_HANDICAP): true, //"asian_handicap" - int64(FOOTBALL_GOAL_LINE): true, //"goal_line" - int64(FOOTBALL_HALF_TIME_RESULT): true, //"half_time_result" - int64(FOOTBALL_FIRST_HALF_ASIAN_HANDICAP): true, //"1st_half_asian_handicap" - int64(FOOTBALL_FIRST_HALF_GOAL_LINE): true, //"1st_half_goal_line" - int64(FOOTBALL_FIRST_TEAM_TO_SCORE): true, //"first_team_to_score" - int64(FOOTBALL_GOALS_ODD_EVEN): true, //"goals_odd_even" - int64(FOOTBALL_DRAW_NO_BET): true, //"draw_no_bet" - + int64(FOOTBALL_FULL_TIME_RESULT): true, //"full_time_result" + int64(FOOTBALL_DOUBLE_CHANCE): true, //"double_chance" + int64(FOOTBALL_GOALS_OVER_UNDER): true, //"goals_over_under" + int64(FOOTBALL_CORRECT_SCORE): true, //"correct_score" + int64(FOOTBALL_ASIAN_HANDICAP): true, //"asian_handicap" + int64(FOOTBALL_GOAL_LINE): true, //"goal_line" + int64(FOOTBALL_HALF_TIME_RESULT): true, //"half_time_result" + int64(FOOTBALL_FIRST_HALF_ASIAN_HANDICAP): true, //"1st_half_asian_handicap" + int64(FOOTBALL_FIRST_HALF_GOAL_LINE): true, //"1st_half_goal_line" + int64(FOOTBALL_FIRST_TEAM_TO_SCORE): true, //"first_team_to_score" + int64(FOOTBALL_GOALS_ODD_EVEN): true, //"goals_odd_even" + int64(FOOTBALL_DRAW_NO_BET): true, //"draw_no_bet" + int64(FOOTBALL_CORNERS): true, + int64(FOOTBALL_CORNERS_TWO_WAY): true, + int64(FOOTBALL_FIRST_HALF_CORNERS): true, + int64(FOOTBALL_ASIAN_TOTAL_CORNERS): true, + int64(FOOTBALL_FIRST_HALF_ASIAN_CORNERS): true, + int64(FOOTBALL_FIRST_HALF_GOALS_ODD_EVEN): true, + int64(FOOTBALL_SECOND_HALF_GOALS_ODD_EVEN): true, + // Basketball Markets int64(BASKETBALL_GAME_LINES): true, int64(BASKETBALL_RESULT_AND_BOTH_TEAMS_TO_SCORE_X_POINTS): true, diff --git a/internal/repository/company.go b/internal/repository/company.go index 8f3fe1a..d9b8e06 100644 --- a/internal/repository/company.go +++ b/internal/repository/company.go @@ -25,14 +25,17 @@ func convertDBCompany(dbCompany dbgen.Company) domain.Company { } } -func convertDBCompanyWithWallet(dbCompany dbgen.CompaniesWithWallet) domain.GetCompany { +func convertDBCompanyDetails(dbCompany dbgen.CompaniesDetail) domain.GetCompany { return domain.GetCompany{ - ID: dbCompany.ID, - Name: dbCompany.Name, - AdminID: dbCompany.AdminID, - WalletID: dbCompany.WalletID, - WalletBalance: domain.Currency(dbCompany.Balance), - IsWalletActive: dbCompany.IsActive, + ID: dbCompany.ID, + Name: dbCompany.Name, + AdminID: dbCompany.AdminID, + WalletID: dbCompany.WalletID, + WalletBalance: domain.Currency(dbCompany.Balance), + IsWalletActive: dbCompany.IsActive, + AdminFirstName: dbCompany.AdminFirstName, + AdminLastName: dbCompany.AdminLastName, + AdminPhoneNumber: dbCompany.AdminPhoneNumber.String, } } @@ -74,7 +77,7 @@ func (s *Store) GetAllCompanies(ctx context.Context) ([]domain.GetCompany, error var companies []domain.GetCompany = make([]domain.GetCompany, 0, len(dbCompanies)) for _, dbCompany := range dbCompanies { - companies = append(companies, convertDBCompanyWithWallet(dbCompany)) + companies = append(companies, convertDBCompanyDetails(dbCompany)) } return companies, nil @@ -92,7 +95,7 @@ func (s *Store) SearchCompanyByName(ctx context.Context, name string) ([]domain. var companies []domain.GetCompany = make([]domain.GetCompany, 0, len(dbCompanies)) for _, dbCompany := range dbCompanies { - companies = append(companies, convertDBCompanyWithWallet(dbCompany)) + companies = append(companies, convertDBCompanyDetails(dbCompany)) } return companies, nil } @@ -103,7 +106,7 @@ func (s *Store) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany if err != nil { return domain.GetCompany{}, err } - return convertDBCompanyWithWallet(dbCompany), nil + return convertDBCompanyDetails(dbCompany), nil } func (s *Store) UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error) { diff --git a/internal/repository/event.go b/internal/repository/event.go index 895a963..904ca2c 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -118,7 +118,7 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming } func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) { - + events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{ LeagueID: pgtype.Text{ String: leagueID.Value, @@ -128,7 +128,7 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.Val String: sportID.Value, Valid: sportID.Valid, }, - Limit: pgtype.Int4{ + Limit: pgtype.Int4{ Int32: int32(limit.Value), Valid: limit.Valid, }, diff --git a/internal/repository/user.go b/internal/repository/user.go index 3c0c910..c2aa930 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -230,6 +230,22 @@ func (s *Store) UpdateUserCompany(ctx context.Context, id int64, companyID int64 } return nil } + +func (s *Store) UpdateUserSuspend(ctx context.Context, id int64, status bool) error { + err := s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{ + ID: id, + Suspended: status, + SuspendedAt: pgtype.Timestamptz{ + Time: time.Now(), + Valid: true, + }, + }) + if err != nil { + return err + } + return nil +} + func (s *Store) DeleteUser(ctx context.Context, id int64) error { err := s.queries.DeleteUser(ctx, id) if err != nil { diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 5bc392d..a644021 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -362,7 +362,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le // TODO: Add the option of passing number of created events var selectedUpcomingEvents []domain.UpcomingEvent - numEventsPerBet := random.Intn(4) + 1 //Eliminate the option of 0 + numEventsPerBet := min(random.Intn(4)+1, len(events)) //Eliminate the option of 0 for i := 0; i < int(numEventsPerBet); i++ { randomIndex := random.Intn(len(events)) @@ -371,7 +371,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le } - s.logger.Info("Generating random bet events", "selectedUpcomingEvents", len(selectedUpcomingEvents)) + // s.logger.Info("Generating random bet events", "selectedUpcomingEvents", len(selectedUpcomingEvents)) // Get market and odds for that var randomOdds []domain.CreateBetOutcome @@ -395,7 +395,7 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le return domain.CreateBetRes{}, ErrGenerateRandomOutcome } - s.logger.Info("Generated Random bet Outcome", "randomOdds", len(randomOdds)) + // s.logger.Info("Generated Random bet Outcome", "randomOdds", len(randomOdds)) var cashoutID string @@ -491,9 +491,9 @@ func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domai status = betOutcome.Status case domain.OUTCOME_STATUS_WIN: if betOutcome.Status == domain.OUTCOME_STATUS_LOSS { - status = domain.OUTCOME_STATUS_HALF + status = domain.OUTCOME_STATUS_LOSS } else if betOutcome.Status == domain.OUTCOME_STATUS_HALF { - status = domain.OUTCOME_STATUS_VOID + status = domain.OUTCOME_STATUS_HALF } else if betOutcome.Status == domain.OUTCOME_STATUS_WIN { status = domain.OUTCOME_STATUS_WIN } else if betOutcome.Status == domain.OUTCOME_STATUS_VOID { @@ -509,16 +509,18 @@ func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domai } else if betOutcome.Status == domain.OUTCOME_STATUS_WIN { status = domain.OUTCOME_STATUS_LOSS } else if betOutcome.Status == domain.OUTCOME_STATUS_VOID { - status = domain.OUTCOME_STATUS_VOID + status = domain.OUTCOME_STATUS_LOSS } else { status = domain.OUTCOME_STATUS_ERROR } case domain.OUTCOME_STATUS_VOID: if betOutcome.Status == domain.OUTCOME_STATUS_VOID || betOutcome.Status == domain.OUTCOME_STATUS_WIN || - betOutcome.Status == domain.OUTCOME_STATUS_LOSS || betOutcome.Status == domain.OUTCOME_STATUS_HALF { status = domain.OUTCOME_STATUS_VOID + } else if betOutcome.Status == domain.OUTCOME_STATUS_LOSS { + status = domain.OUTCOME_STATUS_LOSS + } else { status = domain.OUTCOME_STATUS_ERROR } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index f344e2c..37781d1 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -100,7 +100,7 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { func (s *service) FetchUpcomingEvents(ctx context.Context) error { // sportIDs := []int{1, 18, 17} - sportIDs := []int{18} + sportIDs := []int{18, 17} for _, sportID := range sportIDs { var totalPages int = 1 @@ -142,6 +142,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { ID string `json:"id"` Name string `json:"name"` } `json:"away"` + } `json:"results"` } if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { @@ -164,7 +165,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { } if !slices.Contains(domain.SupportedLeagues, leagueID) { - + fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID) skippedLeague = append(skippedLeague, ev.League.Name) continue } @@ -172,7 +173,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { event := domain.UpcomingEvent{ ID: ev.ID, SportID: ev.SportID, - MatchName: ev.Home.Name, + MatchName: "", HomeTeam: ev.Home.Name, AwayTeam: "", // handle nil safely HomeTeamID: ev.Home.ID, @@ -188,6 +189,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { if ev.Away != nil { event.AwayTeam = ev.Away.Name event.AwayTeamID = ev.Away.ID + event.MatchName = ev.Home.Name + " vs " + ev.Away.Name } err = s.store.SaveUpcomingEvent(ctx, event) @@ -234,7 +236,7 @@ func (s *service) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcomi return s.store.GetExpiredUpcomingEvents(ctx) } -func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error){ +func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) { return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID, firstStartTime, lastStartTime) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 5d6f1d0..a2c4016 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -246,7 +246,7 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName var marketIDint int err := json.Unmarshal(market.ID, &marketIDint) if err != nil { - s.logger.Error("Invalid market id") + s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) errs = append(errs, err) } } diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index 93f3d64..c9502d0 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -85,6 +85,8 @@ func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.Outcom return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("❌ mutli outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) } + // fmt.Printf("| Multi Outcome | %v -> %v \n", outcome.String(), secondOutcome.String()) + switch outcome { case domain.OUTCOME_STATUS_PENDING: return secondOutcome, nil @@ -94,7 +96,7 @@ func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.Outcom } else if secondOutcome == domain.OUTCOME_STATUS_LOSS { return domain.OUTCOME_STATUS_LOSS, nil } else if secondOutcome == domain.OUTCOME_STATUS_HALF { - return domain.OUTCOME_STATUS_HALF, nil + return domain.OUTCOME_STATUS_VOID, nil } else if secondOutcome == domain.OUTCOME_STATUS_VOID { return domain.OUTCOME_STATUS_HALF, nil } else { @@ -107,14 +109,14 @@ func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.Outcom secondOutcome == domain.OUTCOME_STATUS_HALF { return domain.OUTCOME_STATUS_LOSS, nil } else if secondOutcome == domain.OUTCOME_STATUS_VOID { - return domain.OUTCOME_STATUS_HALF, nil + return domain.OUTCOME_STATUS_VOID, nil } else { fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } case domain.OUTCOME_STATUS_VOID: if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_LOSS { - return domain.OUTCOME_STATUS_HALF, nil + return domain.OUTCOME_STATUS_VOID, nil } else if secondOutcome == domain.OUTCOME_STATUS_VOID || secondOutcome == domain.OUTCOME_STATUS_HALF { return domain.OUTCOME_STATUS_VOID, nil } else { @@ -123,7 +125,7 @@ func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.Outcom } case domain.OUTCOME_STATUS_HALF: if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_HALF { - return domain.OUTCOME_STATUS_HALF, nil + return domain.OUTCOME_STATUS_VOID, nil } else if secondOutcome == domain.OUTCOME_STATUS_LOSS { return domain.OUTCOME_STATUS_LOSS, nil } else if secondOutcome == domain.OUTCOME_STATUS_VOID { @@ -139,6 +141,8 @@ func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.Outcom } // Asian Handicap betting is a type of betting that eliminates the possibility of a draw by giving one team a virtual advantage or disadvantage. +// When the handicap has two values like "+0.5, +1.0" or "-0.5, -1.0", then it a multi outcome bet +// . // // { // "id": "548319135", @@ -178,26 +182,32 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i if err != nil { return domain.OUTCOME_STATUS_ERROR, err } + continue } newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } + continue } else if adjustedHomeScore < adjustedAwayScore { if outcome.OddHeader == "2" { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } + continue } newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } - } - newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, err + continue + } else if adjustedHomeScore == adjustedAwayScore { + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, err + } + continue } } return newOutcome, nil @@ -306,24 +316,60 @@ func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away in return domain.OUTCOME_STATUS_LOSS, nil } +func evaluateTeamOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + + switch outcome.OddHeader { + case "1": + if outcome.OddHandicap == "Odd" { + if score.Home%2 == 1 { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } else if outcome.OddHandicap == "Even" { + if score.Home%2 == 0 { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } else { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap) + } + case "2": + if outcome.OddHandicap == "Odd" { + if score.Away%2 == 1 { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } else if outcome.OddHandicap == "Even" { + if score.Away%2 == 0 { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } else { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap) + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) + + } +} + // Double Chance betting is a type of bet where the bettor predicts two of the three possible outcomes of a match. func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { isHomeWin := score.Home > score.Away isDraw := score.Home == score.Away isAwayWin := score.Away > score.Home - switch outcome.OddName { - case "1 or Draw", (outcome.HomeTeamName + " or " + "Draw"): + case "1 or Draw", (outcome.HomeTeamName + " or " + "Draw"), ("Draw" + " or " + outcome.HomeTeamName): if isHomeWin || isDraw { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil - case "Draw or 2", ("Draw" + " or " + outcome.AwayTeamName): + case "Draw or 2", ("Draw" + " or " + outcome.AwayTeamName), (outcome.AwayTeamName + " or " + "Draw"): if isDraw || isAwayWin { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil - case "1 or 2", (outcome.HomeTeamName + " or " + outcome.AwayTeamName): + case "1 or 2", (outcome.HomeTeamName + " or " + outcome.AwayTeamName), (outcome.AwayTeamName + " or " + outcome.HomeTeamName): if isHomeWin || isAwayWin { return domain.OUTCOME_STATUS_WIN, nil } @@ -346,6 +392,34 @@ func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int } return domain.OUTCOME_STATUS_LOSS, nil } +func evaluateCorners(outcome domain.BetOutcome, corners struct{ Home, Away int }) (domain.OutcomeStatus, error) { + + totalCorners := corners.Home + corners.Away + threshold, err := strconv.ParseFloat(outcome.OddName, 10) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) + } + switch outcome.OddHeader { + case "Over": + if totalCorners > int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "Under": + if totalCorners < int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "Exactly": + if totalCorners == int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) + } +} + // Basketball evaluations // Game Lines is an aggregate of money line, spread and total betting markets in one @@ -715,6 +789,30 @@ func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ return domain.OUTCOME_STATUS_LOSS, nil } +// Team With Highest Scoring Quarter betting is a type of bet where the bettor predicts which team will have the highest score in a specific quarter. +func evaluateTeamWithHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + homeTeamHighestQuarter := max(firstScore.Home, secondScore.Home, thirdScore.Home, fourthScore.Home) + awayTeamHighestQuarter := max(firstScore.Away, secondScore.Away, thirdScore.Away, fourthScore.Away) + + switch outcome.OddName { + case "1": + if homeTeamHighestQuarter > awayTeamHighestQuarter { + return domain.OUTCOME_STATUS_WIN, nil + } + case "2": + if awayTeamHighestQuarter > homeTeamHighestQuarter { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Tie": + if homeTeamHighestQuarter == awayTeamHighestQuarter { + return domain.OUTCOME_STATUS_WIN, nil + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + return domain.OUTCOME_STATUS_LOSS, nil +} + // Handicap and Total betting is a combination of spread betting and total points betting // where the bettor predicts the outcome of a match with a point spread and the total number of points scored is over or under a specified number. func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { diff --git a/internal/services/result/service.go b/internal/services/result/service.go index d7cc80a..77d3a2e 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -34,8 +34,9 @@ func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, } } -type ResultCheck struct { -} +var ( + 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 @@ -72,7 +73,7 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { if outcome.Expires.After(time.Now()) { isDeleted = false - s.logger.Info("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID) + s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID) continue } @@ -85,6 +86,10 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { // 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, sportID, outcome) 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, @@ -123,13 +128,14 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { } continue } - fmt.Printf("🧾 Updating bet status for event %v (%d/%d) to %v\n", event.ID, j+1, len(outcomes), status.String()) + fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String()) err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status) if err != nil { s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) continue } - fmt.Printf("✅ Successfully updated 🎫 Bet for event %v(%v) (%d/%d) \n", + fmt.Printf("✅ Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n", + outcome.BetID, event.HomeTeam+" "+event.AwayTeam, event.ID, j+1, len(outcomes)) @@ -273,6 +279,34 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo } +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) { var fbResp domain.FootballResultResponse if err := json.Unmarshal(resultRes, &fbResp); err != nil { @@ -280,16 +314,24 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke return domain.CreateResult{}, err } result := fbResp - if result.TimeStatus != "3" { - s.logger.Warn("Match not yet completed", "event_id", eventID) - return domain.CreateResult{}, fmt.Errorf("match not yet completed") + + 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 := parseSS(fmt.Sprintf("%s-%s", result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away)) + firstHalfScore := parseScore(result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away) + secondHalfScore := parseScore(result.Scores.SecondHalf.Home, result.Scores.SecondHalf.Away) corners := parseStats(result.Stats.Corners) - status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, corners, result.Events) + 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) return domain.CreateResult{}, err @@ -309,12 +351,17 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var basketBallRes domain.BasketballResultResponse if err := json.Unmarshal(response, &basketBallRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + s.logger.Error("Failed to unmarshal basketball result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } - if basketBallRes.TimeStatus != "3" { - s.logger.Warn("Match not yet completed", "event_id", eventID) - return domain.CreateResult{}, fmt.Errorf("match not yet completed") + 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) @@ -337,12 +384,17 @@ func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, mark func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var iceHockeyRes domain.IceHockeyResultResponse if err := json.Unmarshal(response, &iceHockeyRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + s.logger.Error("Failed to unmarshal ice hockey result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } - if iceHockeyRes.TimeStatus != "3" { - s.logger.Warn("Match not yet completed", "event_id", eventID) - return domain.CreateResult{}, fmt.Errorf("match not yet completed") + 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) @@ -388,7 +440,10 @@ func parseStats(stats []string) struct{ Home, Away int } { } // evaluateOutcome determines the outcome status based on market type and odd -func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, firstHalfScore struct{ Home, Away int }, corners struct{ Home, Away int }, events []map[string]string) (domain.OutcomeStatus, error) { +func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, + firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int }, + corners struct{ Home, Away int }, halfTimeCorners struct{ Home, Away int }, + events []map[string]string) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) @@ -420,6 +475,21 @@ func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, return evaluateDoubleChance(outcome, finalScore) case int64(domain.FOOTBALL_DRAW_NO_BET): return evaluateDrawNoBet(outcome, finalScore) + case int64(domain.FOOTBALL_CORNERS): + return evaluateCorners(outcome, corners) + case int64(domain.FOOTBALL_CORNERS_TWO_WAY): + return evaluateCorners(outcome, corners) + case int64(domain.FOOTBALL_FIRST_HALF_CORNERS): + return evaluateCorners(outcome, halfTimeCorners) + case int64(domain.FOOTBALL_ASIAN_TOTAL_CORNERS): + return evaluateCorners(outcome, corners) + case int64(domain.FOOTBALL_FIRST_HALF_ASIAN_CORNERS): + return evaluateCorners(outcome, halfTimeCorners) + case int64(domain.FOOTBALL_FIRST_HALF_GOALS_ODD_EVEN): + return evaluateGoalsOddEven(outcome, firstHalfScore) + case int64(domain.FOOTBALL_SECOND_HALF_GOALS_ODD_EVEN): + return evaluateGoalsOddEven(outcome, secondHalfScore) + default: s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName) @@ -456,7 +526,9 @@ func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domai case int64(domain.BASKETBALL_GAME_TOTAL_ODD_EVEN): return evaluateGoalsOddEven(outcome, finalScore) case int64(domain.BASKETBALL_TEAM_TOTALS): - return evaluateGoalsOddEven(outcome, finalScore) + return evaluateTeamTotal(outcome, finalScore) + case int64(domain.BASKETBALL_TEAM_TOTAL_ODD_EVEN): + return evaluateTeamOddEven(outcome, finalScore) case int64(domain.BASKETBALL_FIRST_HALF): return evaluateGameLines(outcome, firstHalfScore) @@ -487,6 +559,11 @@ func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domai return evaluateDoubleChance(outcome, firstQuarter) case int64(domain.BASKETBALL_HIGHEST_SCORING_QUARTER): return evaluateHighestScoringQuarter(outcome, firstQuarter, secondQuarter, thirdQuarter, fourthQuarter) + case int64(domain.BASKETBALL_FIRST_QUARTER_RESULT_AND_TOTAL): + return evaluateResultAndTotal(outcome, firstQuarter) + + case int64(domain.BASKETBALL_TEAM_WITH_HIGHEST_SCORING_QUARTER): + return evaluateTeamWithHighestScoringQuarter(outcome, firstQuarter, secondQuarter, thirdQuarter, fourthQuarter) default: s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName) diff --git a/internal/services/user/port.go b/internal/services/user/port.go index 3dfa77e..c7d1bfb 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -15,6 +15,7 @@ type UserStore interface { GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error UpdateUserCompany(ctx context.Context, id int64, companyID int64) error + UpdateUserSuspend(ctx context.Context, id int64, status bool) error DeleteUser(ctx context.Context, id int64) error CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) GetUserByEmail(ctx context.Context, email string) (domain.User, error) diff --git a/internal/services/user/user.go b/internal/services/user/user.go index 225ecc6..a9d303e 100644 --- a/internal/services/user/user.go +++ b/internal/services/user/user.go @@ -20,7 +20,11 @@ func (s *Service) UpdateUser(ctx context.Context, user domain.UpdateUserReq) err func (s *Service) UpdateUserCompany(ctx context.Context, id int64, companyID int64) error { // update user return s.userStore.UpdateUserCompany(ctx, id, companyID) +} +func (s *Service) UpdateUserSuspend(ctx context.Context, id int64, status bool) error { + // update user + return s.userStore.UpdateUserSuspend(ctx, id, status) } func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) { return s.userStore.GetUserByID(ctx, id) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index e9bca42..1bbefe7 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -21,53 +21,24 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S spec string task func() }{ - // { - // spec: "0 0 * * * *", // Every 1 hour - // task: func() { - // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - // log.Printf("FetchUpcomingEvents error: %v", err) - // } - // }, - // }, - - // { - // spec: "*/5 * * * * *", // Every 5 seconds - // task: func() { - // if err := eventService.FetchLiveEvents(context.Background()); err != nil { - // log.Printf("FetchLiveEvents 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) - // } - // }, - // }, - // { - // spec: "0 */15 * * * *", - // task: func() { - // log.Println("Fetching results for upcoming events...") - - // upcomingEvents, err := eventService.GetAllUpcomingEvents(context.Background()) - // if err != nil { - // log.Printf("Failed to fetch upcoming events: %v", err) - // return - // } - - // for _, event := range upcomingEvents { - // if err := resultService.FetchAndStoreResult(context.Background(), event.ID); err != nil { - // log.Printf(" Failed to fetch/store result for event %s: %v", event.ID, err) - // } else { - // log.Printf(" Successfully stored result for event %s", event.ID) - // } - // } - // }, - // }, { - spec: "0 */15 * * * *", + spec: "0 0 * * * *", // Every 1 hour + 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) + } + }, + }, + { + spec: "0 */15 * * * *", // Every 15 Minutes task: func() { log.Println("Fetching results for upcoming events...") @@ -81,7 +52,6 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } for _, job := range schedule { - 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 860d128..da85139 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -116,17 +116,20 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - res, err := h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime) + var res domain.CreateBetRes + var err error + for i := 0; i < int(req.NumberOfBets); i++ { + res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime) - if err != nil { - h.logger.Error("Random Bet failed", "error", err) - switch err { - case bet.ErrNoEventsAvailable: - return fiber.NewError(fiber.StatusBadRequest, "No events found") + if err != nil { + h.logger.Error("Random Bet failed", "error", err) + switch err { + case bet.ErrNoEventsAvailable: + return fiber.NewError(fiber.StatusBadRequest, "No events found") + } + return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet") } - return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet") } - return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) } @@ -177,8 +180,9 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error { bet, err := h.betSvc.GetBetByID(c.Context(), id) if err != nil { + // TODO: handle all the errors types h.logger.Error("Failed to get bet by ID", "betID", id, "error", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bet") + return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") } res := domain.ConvertBet(bet) diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index 4ec72e7..8f090ee 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -4,6 +4,7 @@ import ( "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) @@ -492,6 +493,64 @@ func (h *Handler) GetBranchOperations(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Branch Operations retrieved successfully", result, nil) } +// GetBranchCashiers godoc +// @Summary Gets branch cashiers +// @Description Gets branch cashiers +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "Branch ID" +// @Success 200 {array} GetCashierRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branch/{id}/cashier [get] +func (h *Handler) GetBranchCashiers(c *fiber.Ctx) error { + branchID := c.Params("id") + id, err := strconv.ParseInt(branchID, 10, 64) + if err != nil { + h.logger.Error("Invalid branch ID", "branchID", branchID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", err, nil) + } + + cashiers, err := h.userSvc.GetCashiersByBranch(c.Context(), id) + + if err != nil { + h.logger.Error("Failed to get cashier by branch ID", "branchID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve cashier", err, nil) + } + + var result []GetCashierRes = make([]GetCashierRes, 0, len(cashiers)) + + for _, cashier := range cashiers { + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), cashier.ID) + if err != nil { + if err == authentication.ErrRefreshTokenNotFound { + lastLogin = &cashier.CreatedAt + } else { + h.logger.Error("Failed to get user last login", "userID", cashier.ID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") + } + } + 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, + }) + } + + return response.WriteJSON(c, fiber.StatusOK, "Branch Cashiers retrieved successfully", result, nil) +} + // GetBetByBranchID godoc // @Summary Gets bets by its branch id // @Description Gets bets by its branch id diff --git a/internal/web_server/handlers/company_handler.go b/internal/web_server/handlers/company_handler.go index 6e0f713..46b8a7d 100644 --- a/internal/web_server/handlers/company_handler.go +++ b/internal/web_server/handlers/company_handler.go @@ -25,12 +25,15 @@ type CompanyRes struct { } type GetCompanyRes struct { - ID int64 `json:"id" example:"1"` - Name string `json:"name" example:"CompanyName"` - AdminID int64 `json:"admin_id" example:"1"` - WalletID int64 `json:"wallet_id" example:"1"` - WalletBalance float32 `json:"balance" example:"1"` - IsActive bool `json:"is_active" example:"false"` + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"CompanyName"` + AdminID int64 `json:"admin_id" example:"1"` + WalletID int64 `json:"wallet_id" example:"1"` + WalletBalance float32 `json:"balance" example:"1"` + IsActive bool `json:"is_active" example:"false"` + AdminFirstName string `json:"admin_first_name" example:"John"` + AdminLastName string `json:"admin_last_name" example:"Doe"` + AdminPhoneNumber string `json:"admin_phone_number" example:"1234567890"` } func convertCompany(company domain.Company) CompanyRes { @@ -44,12 +47,15 @@ func convertCompany(company domain.Company) CompanyRes { func convertGetCompany(company domain.GetCompany) GetCompanyRes { return GetCompanyRes{ - ID: company.ID, - Name: company.Name, - AdminID: company.AdminID, - WalletID: company.WalletID, - WalletBalance: company.WalletBalance.Float32(), - IsActive: company.IsWalletActive, + ID: company.ID, + Name: company.Name, + AdminID: company.AdminID, + WalletID: company.WalletID, + WalletBalance: company.WalletBalance.Float32(), + IsActive: company.IsWalletActive, + AdminFirstName: company.AdminFirstName, + AdminLastName: company.AdminLastName, + AdminPhoneNumber: company.AdminPhoneNumber, } } diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index f91d0f2..5885ce0 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -182,7 +182,7 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error { ticket, err := h.ticketSvc.GetTicketByID(c.Context(), id) if err != nil { - // h.logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err) + h.logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err) return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve ticket") } diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 09fd436..55de2af 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -450,7 +450,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path int true "User ID" -// @Success 200 {object} response.APIResponse +// @Success 200 {object} UserProfileRes // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse @@ -513,3 +513,75 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", res, nil) } + +// DeleteUser godoc +// @Summary Delete user by ID +// @Description Delete a user by their ID +// @Tags user +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /user/delete/{id} [delete] +func (h *Handler) DeleteUser(c *fiber.Ctx) error { + userIDstr := c.Params("id") + userID, err := strconv.ParseInt(userIDstr, 10, 64) + if err != nil { + h.logger.Error("DeleteUser failed", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid user ID", nil, nil) + } + + err = h.userSvc.DeleteUser(c.Context(), userID) + if err != nil { + h.logger.Error("Failed to delete user", "userID", userID, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to delete user", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "User deleted successfully", nil, nil) +} + +type UpdateUserSuspendReq struct { + UserID int64 `json:"user_id" validate:"required" example:"123"` + Suspended bool `json:"suspended" validate:"required" example:"true"` +} +type UpdateUserSuspendRes struct { + UserID int64 `json:"user_id"` + Suspended bool `json:"suspended"` +} + +// UpdateUserSuspend godoc +// @Summary Suspend or unsuspend a user +// @Description Suspend or unsuspend a user +// @Tags user +// @Accept json +// @Produce json +// @Param updateUserSuspend body UpdateUserSuspendReq true "Suspend or unsuspend a user" +// @Success 200 {object} UpdateUserSuspendRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /user/suspend [post] +func (h *Handler) UpdateUserSuspend(c *fiber.Ctx) error { + var req UpdateUserSuspendReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse UpdateUserSuspend request", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + if valErrs, ok := h.validator.Validate(c, req); !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + err := h.userSvc.UpdateUserSuspend(c.Context(), req.UserID, req.Suspended) + if err != nil { + h.logger.Error("Failed to update user suspend status", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update user suspend status") + } + + res := UpdateUserSuspendRes{ + UserID: req.UserID, + Suspended: req.Suspended, + } + return response.WriteJSON(c, fiber.StatusOK, "User suspend status updated successfully", res, nil) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 0623d62..0ee2a83 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -35,7 +35,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "message": "Welcome to the FortuneBet API", - "version": "1.0.1", + "version": "1.0dev2", }) }) @@ -77,6 +77,8 @@ func (a *App) initAppRoutes() { a.fiber.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist) a.fiber.Get("/user/profile", a.authMiddleware, h.UserProfile) a.fiber.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) + a.fiber.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) + a.fiber.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend) a.fiber.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet) a.fiber.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone) @@ -120,12 +122,14 @@ func (a *App) initAppRoutes() { a.fiber.Get("/search/branch", a.authMiddleware, h.SearchBranch) // /branch/search // branch/wallet + a.fiber.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers) // Branch Operation a.fiber.Get("/supportedOperation", a.authMiddleware, h.GetAllSupportedOperations) a.fiber.Post("/supportedOperation", a.authMiddleware, h.CreateSupportedOperation) a.fiber.Post("/operation", a.authMiddleware, h.CreateBranchOperation) a.fiber.Get("/branch/:id/operation", a.authMiddleware, h.GetBranchOperations) + a.fiber.Delete("/branch/:id/operation/:opID", a.authMiddleware, h.DeleteBranchOperation) // Company @@ -145,13 +149,13 @@ func (a *App) initAppRoutes() { // Bet Routes a.fiber.Post("/bet", a.authMiddleware, h.CreateBet) a.fiber.Get("/bet", a.authMiddleware, h.GetAllBet) - a.fiber.Get("/bet/:id", a.authMiddleware, h.GetBetByID) + a.fiber.Get("/bet/:id", h.GetBetByID) a.fiber.Get("/bet/cashout/:id", a.authMiddleware, h.GetBetByCashoutID) a.fiber.Patch("/bet/:id", a.authMiddleware, h.UpdateCashOut) a.fiber.Delete("/bet/:id", a.authMiddleware, h.DeleteBet) - + a.fiber.Post("/random/bet", a.authMiddleware, h.RandomBet) - + // Wallet a.fiber.Get("/wallet", h.GetAllWallets) a.fiber.Get("/wallet/:id", h.GetWalletByID)