From 959390b506b4d91f11398d22553371ae3fab3ae6 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 1 Apr 2025 22:03:23 +0300 Subject: [PATCH] feat: added bet and ticket handlers --- cmd/main.go | 10 +- db/migrations/000001_fortune.up.sql | 73 +-- db/query/bet.sql | 4 +- docs/docs.go | 512 ++++++++++++++++++ docs/swagger.json | 512 ++++++++++++++++++ docs/swagger.yaml | 343 ++++++++++++ gen/db/auth.sql.go | 2 +- gen/db/bet.sql.go | 15 +- gen/db/models.go | 29 +- gen/db/otp.sql.go | 2 +- internal/domain/bet.go | 19 +- internal/domain/common.go | 17 +- internal/domain/ticket.go | 8 +- internal/repository/bet.go | 106 ++-- internal/repository/ticket.go | 40 +- internal/services/bet/port.go | 4 +- internal/services/bet/service.go | 4 +- internal/services/ticket/port.go | 2 +- internal/services/ticket/service.go | 4 +- internal/web_server/app.go | 8 + internal/web_server/handlers/bet_handler.go | 266 +++++++++ .../web_server/handlers/ticket_handler.go | 153 ++++++ internal/web_server/routes.go | 15 +- 23 files changed, 1986 insertions(+), 162 deletions(-) create mode 100644 internal/web_server/handlers/bet_handler.go create mode 100644 internal/web_server/handlers/ticket_handler.go diff --git a/cmd/main.go b/cmd/main.go index 1797c8e..32c2231 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -11,6 +11,8 @@ import ( mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" @@ -46,16 +48,22 @@ func main() { logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel) store := repository.NewStore(db) v := customvalidator.NewCustomValidator(validator.New()) + authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) mockSms := mocksms.NewMockSMS() mockemail := mockemail.NewMockEmail() + userSvc := user.NewService(store, store, mockSms, mockemail) + ticketSvc := ticket.NewService(store) + betSvc := bet.NewService(store) + app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, - }, userSvc, + }, userSvc, ticketSvc, betSvc, ) logger.Info("Starting server", "port", cfg.Port) + if err := app.Run(); err != nil { logger.Error("Failed to start server", "error", err) os.Exit(1) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6526209..8f964bf 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -36,6 +36,44 @@ CREATE TABLE refresh_tokens ( created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMPTZ NOT NULL ); + +CREATE TABLE IF NOT EXISTS bets ( + id BIGSERIAL PRIMARY KEY, + amount BIGINT NOT NULL, + total_odds REAL NOT NULL, + status INT NOT NULL, + full_name VARCHAR(255) NOT NULL, + phone_number VARCHAR(255) NOT NULL, + branch_id BIGINT, + user_id BIGINT, + cashed_out BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP, + updated_at TIMESTAMP, + is_shop_bet BOOLEAN NOT NULL, + CHECK (user_id IS NOT NULL OR branch_id IS NOT NULL) +); + +CREATE TABLE IF NOT EXISTS tickets ( + id BIGSERIAL PRIMARY KEY, + amount BIGINT NULL, + total_odds REAL NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP +); + + +-- CREATE TABLE IF NOT EXISTS bet_outcomes ( +-- id BIGSERIAL PRIMARY KEY, +-- bet_id BIGINT NOT NULL, +-- outcome_id BIGINT NOT NULL, +-- ); + +-- CREATE TABLE IF NOT EXISTS ticket_outcomes ( +-- id BIGSERIAL PRIMARY KEY, +-- ticket_id BIGINT NOT NULL, +-- outcome_id BIGINT NOT NULL, +-- ); + ----------------------------------------------seed data------------------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- @@ -61,38 +99,3 @@ INSERT INTO users ( ); -CREATE TABLE IF NOT EXISTS bets ( - id BIGSERIAL PRIMARY KEY, - amount BIGINT NOT NULL, - total_odds REAL NOT NULL, - status INT NOT NULL, - full_name VARCHAR(255) NOT NULL, - phone_number VARCHAR(255) NOT NULL, - branch_id BIGINT, - user_id BIGINT, - cashed_out BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP, - updated_at TIMESTAMP, - CHECK (user_id IS NOT NULL OR branch_id IS NOT NULL) -); - -CREATE TABLE IF NOT EXISTS tickets ( - id BIGSERIAL PRIMARY KEY, - amount BIGINT NULL, - total_odds REAL NOT NULL, - created_at TIMESTAMP, - updated_at TIMESTAMP -); - - --- CREATE TABLE IF NOT EXISTS bet_outcomes ( --- id BIGSERIAL PRIMARY KEY, --- bet_id BIGINT NOT NULL, --- outcome_id BIGINT NOT NULL, --- ); - --- CREATE TABLE IF NOT EXISTS ticket_outcomes ( --- id BIGSERIAL PRIMARY KEY, --- ticket_id BIGINT NOT NULL, --- outcome_id BIGINT NOT NULL, --- ); \ No newline at end of file diff --git a/db/query/bet.sql b/db/query/bet.sql index f2f48e3..bf0d466 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -1,6 +1,6 @@ -- name: CreateBet :one -INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id) -VALUES ($1, $2, $3, $4, $5, $6, $7) +INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id, is_shop_bet) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; -- name: GetAllBets :many diff --git a/docs/docs.go b/docs/docs.go index 6625028..49956c9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -180,6 +180,351 @@ const docTemplate = `{ } } }, + "/bet": { + "get": { + "description": "Gets all the bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets all bets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BetRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Create a bet", + "parameters": [ + { + "description": "Creates bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/bet/{id}": { + "get": { + "description": "Gets a single bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Deletes bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Deletes bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet 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" + } + } + } + }, + "patch": { + "description": "Updates the cashed out field", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Updates the cashed out field", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updates Cashed Out", + "name": "updateCashOut", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateCashOutReq" + } + } + ], + "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" + } + } + } + } + }, + "/ticket": { + "get": { + "description": "Retrieve all tickets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ticket" + ], + "summary": "Get all tickets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.TicketRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a temporary ticket", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ticket" + ], + "summary": "Create a temporary ticket", + "parameters": [ + { + "description": "Creates ticket", + "name": "createTicket", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateTicketReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CreateTicketRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/ticket/{id}": { + "get": { + "description": "Retrieve ticket details by ticket ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ticket" + ], + "summary": "Get ticket by ID", + "parameters": [ + { + "type": "integer", + "description": "Ticket ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TicketRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/checkPhoneEmailExist": { "post": { "description": "Check if phone number or email exist", @@ -452,6 +797,24 @@ const docTemplate = `{ } }, "definitions": { + "domain.BetStatus": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BET_STATUS_PENDING", + "BET_STATUS_WIN", + "BET_STATUS_LOSS", + "BET_STATUS_ERROR" + ] + }, + "domain.Outcome": { + "type": "object" + }, "domain.Role": { "type": "string", "enum": [ @@ -469,6 +832,57 @@ const docTemplate = `{ "RoleCashier" ] }, + "handlers.BetRes": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "branch_id": { + "type": "integer", + "example": 2 + }, + "full_name": { + "type": "string", + "example": "John" + }, + "id": { + "type": "integer", + "example": 1 + }, + "is_shop_bet": { + "type": "boolean", + "example": false + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Outcome" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.BetStatus" + } + ], + "example": 1 + }, + "total_odds": { + "type": "number", + "example": 4.22 + }, + "user_id": { + "type": "integer", + "example": 2 + } + } + }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { @@ -493,6 +907,73 @@ const docTemplate = `{ } } }, + "handlers.CreateBetReq": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "full_name": { + "type": "string", + "example": "John" + }, + "is_shop_bet": { + "type": "boolean", + "example": false + }, + "outcomes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.BetStatus" + } + ], + "example": 1 + }, + "total_odds": { + "type": "number", + "example": 4.22 + } + } + }, + "handlers.CreateTicketReq": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "outcomes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "total_odds": { + "type": "number", + "example": 4.22 + } + } + }, + "handlers.CreateTicketRes": { + "type": "object", + "properties": { + "fast_code": { + "type": "integer", + "example": 1234 + } + } + }, "handlers.RegisterCodeReq": { "type": "object", "properties": { @@ -570,6 +1051,37 @@ const docTemplate = `{ } } }, + "handlers.TicketRes": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "id": { + "type": "integer", + "example": 1 + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Outcome" + } + }, + "total_odds": { + "type": "number", + "example": 4.22 + } + } + }, + "handlers.UpdateCashOutReq": { + "type": "object", + "properties": { + "cashedOut": { + "type": "boolean" + } + } + }, "handlers.UserProfileRes": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 76ae6c5..1fec68a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -172,6 +172,351 @@ } } }, + "/bet": { + "get": { + "description": "Gets all the bets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets all bets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BetRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a bet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Create a bet", + "parameters": [ + { + "description": "Creates bet", + "name": "createBet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateBetReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/bet/{id}": { + "get": { + "description": "Gets a single bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Deletes bet by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Deletes bet by id", + "parameters": [ + { + "type": "integer", + "description": "Bet 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" + } + } + } + }, + "patch": { + "description": "Updates the cashed out field", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Updates the cashed out field", + "parameters": [ + { + "type": "integer", + "description": "Bet ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updates Cashed Out", + "name": "updateCashOut", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateCashOutReq" + } + } + ], + "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" + } + } + } + } + }, + "/ticket": { + "get": { + "description": "Retrieve all tickets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ticket" + ], + "summary": "Get all tickets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.TicketRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a temporary ticket", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ticket" + ], + "summary": "Create a temporary ticket", + "parameters": [ + { + "description": "Creates ticket", + "name": "createTicket", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateTicketReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CreateTicketRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/ticket/{id}": { + "get": { + "description": "Retrieve ticket details by ticket ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "ticket" + ], + "summary": "Get ticket by ID", + "parameters": [ + { + "type": "integer", + "description": "Ticket ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TicketRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/checkPhoneEmailExist": { "post": { "description": "Check if phone number or email exist", @@ -444,6 +789,24 @@ } }, "definitions": { + "domain.BetStatus": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BET_STATUS_PENDING", + "BET_STATUS_WIN", + "BET_STATUS_LOSS", + "BET_STATUS_ERROR" + ] + }, + "domain.Outcome": { + "type": "object" + }, "domain.Role": { "type": "string", "enum": [ @@ -461,6 +824,57 @@ "RoleCashier" ] }, + "handlers.BetRes": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "branch_id": { + "type": "integer", + "example": 2 + }, + "full_name": { + "type": "string", + "example": "John" + }, + "id": { + "type": "integer", + "example": 1 + }, + "is_shop_bet": { + "type": "boolean", + "example": false + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Outcome" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.BetStatus" + } + ], + "example": 1 + }, + "total_odds": { + "type": "number", + "example": 4.22 + }, + "user_id": { + "type": "integer", + "example": 2 + } + } + }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { @@ -485,6 +899,73 @@ } } }, + "handlers.CreateBetReq": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "full_name": { + "type": "string", + "example": "John" + }, + "is_shop_bet": { + "type": "boolean", + "example": false + }, + "outcomes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.BetStatus" + } + ], + "example": 1 + }, + "total_odds": { + "type": "number", + "example": 4.22 + } + } + }, + "handlers.CreateTicketReq": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "outcomes": { + "type": "array", + "items": { + "type": "integer" + } + }, + "total_odds": { + "type": "number", + "example": 4.22 + } + } + }, + "handlers.CreateTicketRes": { + "type": "object", + "properties": { + "fast_code": { + "type": "integer", + "example": 1234 + } + } + }, "handlers.RegisterCodeReq": { "type": "object", "properties": { @@ -562,6 +1043,37 @@ } } }, + "handlers.TicketRes": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "id": { + "type": "integer", + "example": 1 + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Outcome" + } + }, + "total_odds": { + "type": "number", + "example": 4.22 + } + } + }, + "handlers.UpdateCashOutReq": { + "type": "object", + "properties": { + "cashedOut": { + "type": "boolean" + } + } + }, "handlers.UserProfileRes": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 166d41d..4cd5fdd 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,18 @@ definitions: + domain.BetStatus: + enum: + - 0 + - 1 + - 2 + - 3 + type: integer + x-enum-varnames: + - BET_STATUS_PENDING + - BET_STATUS_WIN + - BET_STATUS_LOSS + - BET_STATUS_ERROR + domain.Outcome: + type: object domain.Role: enum: - admin @@ -13,6 +27,41 @@ definitions: - RoleSuperAdmin - RoleBranchManager - RoleCashier + handlers.BetRes: + properties: + amount: + example: 100 + type: number + branch_id: + example: 2 + type: integer + full_name: + example: John + type: string + id: + example: 1 + type: integer + is_shop_bet: + example: false + type: boolean + outcomes: + items: + $ref: '#/definitions/domain.Outcome' + type: array + phone_number: + example: "1234567890" + type: string + status: + allOf: + - $ref: '#/definitions/domain.BetStatus' + example: 1 + total_odds: + example: 4.22 + type: number + user_id: + example: 2 + type: integer + type: object handlers.CheckPhoneEmailExistReq: properties: email: @@ -29,6 +78,51 @@ definitions: phone_number_exist: type: boolean type: object + handlers.CreateBetReq: + properties: + amount: + example: 100 + type: number + full_name: + example: John + type: string + is_shop_bet: + example: false + type: boolean + outcomes: + items: + type: integer + type: array + phone_number: + example: "1234567890" + type: string + status: + allOf: + - $ref: '#/definitions/domain.BetStatus' + example: 1 + total_odds: + example: 4.22 + type: number + type: object + handlers.CreateTicketReq: + properties: + amount: + example: 100 + type: number + outcomes: + items: + type: integer + type: array + total_odds: + example: 4.22 + type: number + type: object + handlers.CreateTicketRes: + properties: + fast_code: + example: 1234 + type: integer + type: object handlers.RegisterCodeReq: properties: email: @@ -83,6 +177,27 @@ definitions: phoneNumber: type: string type: object + handlers.TicketRes: + properties: + amount: + example: 100 + type: number + id: + example: 1 + type: integer + outcomes: + items: + $ref: '#/definitions/domain.Outcome' + type: array + total_odds: + example: 4.22 + type: number + type: object + handlers.UpdateCashOutReq: + properties: + cashedOut: + type: boolean + type: object handlers.UserProfileRes: properties: created_at: @@ -275,6 +390,234 @@ paths: summary: Refresh token tags: - auth + /bet: + get: + consumes: + - application/json + description: Gets all the bets + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.BetRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all bets + tags: + - bet + post: + consumes: + - application/json + description: Creates a bet + parameters: + - description: Creates bet + in: body + name: createBet + required: true + schema: + $ref: '#/definitions/handlers.CreateBetReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a bet + tags: + - bet + /bet/{id}: + delete: + consumes: + - application/json + description: Deletes bet by id + parameters: + - description: Bet 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: Deletes bet by id + tags: + - bet + get: + consumes: + - application/json + description: Gets a single bet by id + parameters: + - description: Bet ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets bet by id + tags: + - bet + patch: + consumes: + - application/json + description: Updates the cashed out field + parameters: + - description: Bet ID + in: path + name: id + required: true + type: integer + - description: Updates Cashed Out + in: body + name: updateCashOut + required: true + schema: + $ref: '#/definitions/handlers.UpdateCashOutReq' + 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: Updates the cashed out field + tags: + - bet + /ticket: + get: + consumes: + - application/json + description: Retrieve all tickets + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.TicketRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get all tickets + tags: + - ticket + post: + consumes: + - application/json + description: Creates a temporary ticket + parameters: + - description: Creates ticket + in: body + name: createTicket + required: true + schema: + $ref: '#/definitions/handlers.CreateTicketReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CreateTicketRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a temporary ticket + tags: + - ticket + /ticket/{id}: + get: + consumes: + - application/json + description: Retrieve ticket details by ticket ID + parameters: + - description: Ticket ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.TicketRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get ticket by ID + tags: + - ticket /user/checkPhoneEmailExist: post: consumes: diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 27fb891..c826c36 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.26.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 6163071..f3667c6 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -12,9 +12,9 @@ import ( ) const CreateBet = `-- name: CreateBet :one -INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id) -VALUES ($1, $2, $3, $4, $5, $6, $7) -RETURNING id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at +INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id, is_shop_bet) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +RETURNING id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at, is_shop_bet ` type CreateBetParams struct { @@ -25,6 +25,7 @@ type CreateBetParams struct { PhoneNumber string BranchID pgtype.Int8 UserID pgtype.Int8 + IsShopBet bool } func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, error) { @@ -36,6 +37,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro arg.PhoneNumber, arg.BranchID, arg.UserID, + arg.IsShopBet, ) var i Bet err := row.Scan( @@ -50,6 +52,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro &i.CashedOut, &i.CreatedAt, &i.UpdatedAt, + &i.IsShopBet, ) return i, err } @@ -64,7 +67,7 @@ func (q *Queries) DeleteBet(ctx context.Context, id int64) error { } const GetAllBets = `-- name: GetAllBets :many -SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at FROM bets +SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at, is_shop_bet FROM bets ` func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) { @@ -88,6 +91,7 @@ func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) { &i.CashedOut, &i.CreatedAt, &i.UpdatedAt, + &i.IsShopBet, ); err != nil { return nil, err } @@ -100,7 +104,7 @@ func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) { } const GetBetByID = `-- name: GetBetByID :one -SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at FROM bets WHERE id = $1 +SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at, is_shop_bet FROM bets WHERE id = $1 ` func (q *Queries) GetBetByID(ctx context.Context, id int64) (Bet, error) { @@ -118,6 +122,7 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (Bet, error) { &i.CashedOut, &i.CreatedAt, &i.UpdatedAt, + &i.IsShopBet, ) return i, err } diff --git a/gen/db/models.go b/gen/db/models.go index 6d4137d..63e7f92 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -8,6 +8,21 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +type Bet struct { + ID int64 + Amount int64 + TotalOdds float32 + Status int32 + FullName string + PhoneNumber string + BranchID pgtype.Int8 + UserID pgtype.Int8 + CashedOut pgtype.Bool + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + IsShopBet bool +} + type Otp struct { ID int64 SentTo string @@ -29,20 +44,6 @@ type RefreshToken struct { Revoked bool } -type Bet struct { - ID int64 - Amount int64 - TotalOdds float32 - Status int32 - FullName string - PhoneNumber string - BranchID pgtype.Int8 - UserID pgtype.Int8 - CashedOut pgtype.Bool - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp -} - type Ticket struct { ID int64 Amount pgtype.Int8 diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index 0e93b5a..a84ba3f 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.26.0 +// sqlc v1.28.0 // source: otp.sql package dbgen diff --git a/internal/domain/bet.go b/internal/domain/bet.go index d4540ad..87bc936 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -9,9 +9,11 @@ const ( BET_STATUS_ERROR ) +// If it is a ShopBet then UserID will be the cashier +// If it is a DigitalBet then UserID will be the user and the branchID will be 0 or nil type Bet struct { ID int64 - Outcome []Outcome + Outcomes []Outcome Amount Currency TotalOdds float32 Status BetStatus @@ -19,9 +21,24 @@ type Bet struct { PhoneNumber string BranchID ValidInt64 // Can Be Nullable UserID ValidInt64 // Can Be Nullable + IsShopBet bool CashedOut bool } +type CreateBet struct { + Outcomes []int64 + Amount Currency + TotalOdds float32 + Status BetStatus + FullName string + PhoneNumber string + BranchID ValidInt64 // Can Be Nullable + UserID ValidInt64 // Can Be Nullable + IsShopBet bool +} + func (b BetStatus) String() string { return []string{"Pending", "Win", "Loss", "Error"}[b] } + +// func isBetStatusValid() diff --git a/internal/domain/common.go b/internal/domain/common.go index 1712d6a..985e97e 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -7,6 +7,15 @@ type ValidInt64 struct { Valid bool } +type ValidString struct { + Value string + Valid bool +} +type ValidBool struct { + Value bool + Valid bool +} + type Currency int64 // ToCurrency converts a float32 to Currency @@ -28,11 +37,3 @@ func (m Currency) String() string { return fmt.Sprintf("$%.2f", x) } -type ValidString struct { - Value string - Valid bool -} -type ValidBool struct { - Value bool - Valid bool -} diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index 53d49e8..b1c000f 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -3,7 +3,13 @@ package domain // ID will serve as the fast code since this doesn't need to be secure type Ticket struct { ID int64 - Outcome []Outcome + Outcomes []Outcome + Amount Currency + TotalOdds float32 +} + +type CreateTicket struct { + Outcomes []int64 Amount Currency TotalOdds float32 } diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 614bd37..b3c4cc3 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -8,53 +8,7 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (s *Store) CreateBet(ctx context.Context, bet domain.Bet) (domain.Bet, error) { - - newBet, err := s.queries.CreateBet(ctx, dbgen.CreateBetParams{ - Amount: int64(bet.Amount), - TotalOdds: bet.TotalOdds, - Status: int32(bet.Status), - FullName: bet.FullName, - PhoneNumber: bet.PhoneNumber, - BranchID: pgtype.Int8{ - Int64: bet.BranchID.Value, - Valid: bet.BranchID.Valid, - }, - UserID: pgtype.Int8{ - Int64: bet.UserID.Value, - Valid: bet.UserID.Valid, - }, - }) - - if err != nil { - return domain.Bet{}, err - } - - return domain.Bet{ - ID: newBet.ID, - Amount: domain.Currency(newBet.Amount), - TotalOdds: newBet.TotalOdds, - Status: domain.BetStatus(newBet.Status), - FullName: newBet.FullName, - PhoneNumber: newBet.PhoneNumber, - BranchID: domain.ValidInt64{ - Value: newBet.BranchID.Int64, - Valid: newBet.BranchID.Valid, - }, - UserID: domain.ValidInt64{ - Value: newBet.UserID.Int64, - Valid: newBet.UserID.Valid, - }, - }, err - -} - -func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) { - bet, err := s.queries.GetBetByID(ctx, id) - if err != nil { - return domain.Bet{}, err - } - +func convertDBBet(bet dbgen.Bet) domain.Bet { return domain.Bet{ ID: bet.ID, Amount: domain.Currency(bet.Amount), @@ -70,7 +24,46 @@ func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) { Value: bet.UserID.Int64, Valid: bet.UserID.Valid, }, - }, nil + IsShopBet: bet.IsShopBet, + } +} + +func convertCreateBet(bet domain.CreateBet) dbgen.CreateBetParams { + return dbgen.CreateBetParams{ + Amount: int64(bet.Amount), + TotalOdds: bet.TotalOdds, + Status: int32(bet.Status), + FullName: bet.FullName, + PhoneNumber: bet.PhoneNumber, + BranchID: pgtype.Int8{ + Int64: bet.BranchID.Value, + Valid: bet.BranchID.Valid, + }, + UserID: pgtype.Int8{ + Int64: bet.UserID.Value, + Valid: bet.UserID.Valid, + }, + IsShopBet: bet.IsShopBet, + } +} + +func (s *Store) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) { + + newBet, err := s.queries.CreateBet(ctx, convertCreateBet(bet)) + if err != nil { + return domain.Bet{}, err + } + return convertDBBet(newBet), err + +} + +func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) { + bet, err := s.queries.GetBetByID(ctx, id) + if err != nil { + return domain.Bet{}, err + } + + return convertDBBet(bet), nil } func (s *Store) GetAllBets(ctx context.Context) ([]domain.Bet, error) { @@ -82,22 +75,7 @@ func (s *Store) GetAllBets(ctx context.Context) ([]domain.Bet, error) { var result []domain.Bet for _, bet := range bets { - result = append(result, domain.Bet{ - ID: bet.ID, - Amount: domain.Currency(bet.Amount), - TotalOdds: bet.TotalOdds, - Status: domain.BetStatus(bet.Status), - FullName: bet.FullName, - PhoneNumber: bet.PhoneNumber, - BranchID: domain.ValidInt64{ - Value: bet.BranchID.Int64, - Valid: bet.BranchID.Valid, - }, - UserID: domain.ValidInt64{ - Value: bet.UserID.Int64, - Valid: bet.UserID.Valid, - }, - }) + result = append(result, convertDBBet(bet)) } return result, nil diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index 3c35b2a..b2ba5b7 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -8,24 +8,30 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (s *Store) CreateTicket(ctx context.Context, amount domain.Currency, totalOdds float32) (domain.Ticket, error) { +func convertDBTicket(ticket dbgen.Ticket) domain.Ticket { + return domain.Ticket{ + ID: ticket.ID, + Amount: domain.Currency(ticket.Amount.Int64), + TotalOdds: ticket.TotalOdds, + } +} - ticket, err := s.queries.CreateTicket(ctx, dbgen.CreateTicketParams{ +func convertCreateTicket(ticket domain.CreateTicket) dbgen.CreateTicketParams { + return dbgen.CreateTicketParams{ Amount: pgtype.Int8{ - Int64: int64(amount), + Int64: int64(ticket.Amount), }, - TotalOdds: totalOdds, - }) + TotalOdds: ticket.TotalOdds, + } +} +func (s *Store) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) { + + newTicket, err := s.queries.CreateTicket(ctx, convertCreateTicket(ticket)) if err != nil { return domain.Ticket{}, err } - - return domain.Ticket{ - ID: ticket.ID, - Amount: amount, - TotalOdds: totalOdds, - }, err + return convertDBTicket(newTicket), err } @@ -35,11 +41,7 @@ func (s *Store) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, err return domain.Ticket{}, err } - return domain.Ticket{ - ID: ticket.ID, - Amount: domain.Currency(ticket.Amount.Int64), - TotalOdds: ticket.TotalOdds, - }, nil + return convertDBTicket(ticket), nil } func (s *Store) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) { @@ -51,11 +53,7 @@ func (s *Store) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) { var result []domain.Ticket for _, ticket := range tickets { - result = append(result, domain.Ticket{ - ID: ticket.ID, - Amount: domain.Currency(ticket.Amount.Int64), - TotalOdds: ticket.TotalOdds, - }) + result = append(result, convertDBTicket(ticket)) } return result, nil diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 9892ca2..1061b45 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -1,4 +1,4 @@ -package ticket +package bet import ( "context" @@ -7,7 +7,7 @@ import ( ) type BetStore interface { - CreateBet(ctx context.Context, bet domain.Bet) (domain.Bet, error) + CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) GetAllBets(ctx context.Context) ([]domain.Bet, error) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 84c32fb..58b9cc5 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -1,4 +1,4 @@ -package ticket +package bet import ( "context" @@ -16,7 +16,7 @@ func NewService(betStore BetStore) *Service { } } -func (s *Service) CreateBet(ctx context.Context, bet domain.Bet) (domain.Bet, error) { +func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) { return s.betStore.CreateBet(ctx, bet) } func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) { diff --git a/internal/services/ticket/port.go b/internal/services/ticket/port.go index b89328f..042d27a 100644 --- a/internal/services/ticket/port.go +++ b/internal/services/ticket/port.go @@ -7,7 +7,7 @@ import ( ) type TicketStore interface { - CreateTicket(ctx context.Context, amount domain.Currency, totalOdds float32) (domain.Ticket, error) + CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) DeleteOldTickets(ctx context.Context) error diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index 940c0fa..5779ce4 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -16,8 +16,8 @@ func NewService(ticketStore TicketStore) *Service { } } -func (s *Service) CreateTicket(ctx context.Context, amount domain.Currency, totalOdds float32) (domain.Ticket, error) { - return s.ticketStore.CreateTicket(ctx, amount, totalOdds) +func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) { + return s.ticketStore.CreateTicket(ctx, ticket) } func (s *Service) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) { return s.ticketStore.GetTicketByID(ctx, id) diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 2ebd22e..83e412a 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -5,6 +5,8 @@ import ( "log/slog" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" @@ -18,6 +20,8 @@ type App struct { port int authSvc *authentication.Service userSvc *user.Service + ticketSvc *ticket.Service + betSvc *bet.Service validator *customvalidator.CustomValidator JwtConfig jwtutil.JwtConfig } @@ -28,6 +32,8 @@ func NewApp( logger *slog.Logger, JwtConfig jwtutil.JwtConfig, userSvc *user.Service, + ticketSvc *ticket.Service, + betSvc *bet.Service, ) *App { app := fiber.New(fiber.Config{ CaseSensitive: true, @@ -43,6 +49,8 @@ func NewApp( logger: logger, JwtConfig: JwtConfig, userSvc: userSvc, + ticketSvc: ticketSvc, + betSvc: betSvc, } s.initAppRoutes() diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go new file mode 100644 index 0000000..fb49a6f --- /dev/null +++ b/internal/web_server/handlers/bet_handler.go @@ -0,0 +1,266 @@ +package handlers + +import ( + "log/slog" + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" + "github.com/gofiber/fiber/v2" +) + +type CreateBetReq struct { + Outcomes []int64 `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` + TotalOdds float32 `json:"total_odds" example:"4.22"` + Status domain.BetStatus `json:"status" example:"1"` + FullName string `json:"full_name" example:"John"` + PhoneNumber string `json:"phone_number" example:"1234567890"` + IsShopBet bool `json:"is_shop_bet" example:"false"` +} + +type BetRes struct { + ID int64 `json:"id" example:"1"` + Outcomes []domain.Outcome `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` + TotalOdds float32 `json:"total_odds" example:"4.22"` + Status domain.BetStatus `json:"status" example:"1"` + FullName string `json:"full_name" example:"John"` + PhoneNumber string `json:"phone_number" example:"1234567890"` + BranchID int64 `json:"branch_id" example:"2"` + UserID int64 `json:"user_id" example:"2"` + IsShopBet bool `json:"is_shop_bet" example:"false"` +} + +func convertBet(bet domain.Bet) BetRes { + return BetRes{ + ID: bet.ID, + Outcomes: bet.Outcomes, + Amount: bet.Amount.Float64(), + TotalOdds: bet.TotalOdds, + Status: bet.Status, + FullName: bet.FullName, + PhoneNumber: bet.PhoneNumber, + BranchID: bet.BranchID.Value, + UserID: bet.UserID.Value, + } +} + +// CreateBet godoc +// @Summary Create a bet +// @Description Creates a bet +// @Tags bet +// @Accept json +// @Produce json +// @Param createBet body CreateBetReq true "Creates bet" +// @Success 200 {object} BetRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /bet [post] +func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + + // TODO: Check the token, and find the role and get the branch id from there + + // TODO Reduce amount from the branch wallet + + var isShopBet bool = true + var branchID int64 = 1 + var userID int64 + + var req CreateBetReq + + if err := c.BodyParser(&req); err != nil { + logger.Error("CreateBetReq failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + + // TODO Validate Outcomes Here and make sure they didn't expire + + bet, err := betSvc.CreateBet(c.Context(), domain.CreateBet{ + Outcomes: req.Outcomes, + Amount: domain.Currency(req.Amount), + TotalOdds: req.TotalOdds, + Status: req.Status, + FullName: req.FullName, + PhoneNumber: req.PhoneNumber, + + BranchID: domain.ValidInt64{ + Value: branchID, + Valid: isShopBet, + }, + UserID: domain.ValidInt64{ + Value: userID, + Valid: !isShopBet, + }, + IsShopBet: req.IsShopBet, + }) + + if err != nil { + logger.Error("CreateBetReq failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) + } + + res := convertBet(bet) + + return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) + } +} + +// GetAllBet godoc +// @Summary Gets all bets +// @Description Gets all the bets +// @Tags bet +// @Accept json +// @Produce json +// @Success 200 {array} BetRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /bet [get] +func GetAllBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + bets, err := betSvc.GetAllBets(c.Context()) + + if err != nil { + logger.Error("Failed to get bets", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bets", err, nil) + } + + var res []BetRes + for _, bet := range bets { + res = append(res, convertBet(bet)) + } + + return response.WriteJSON(c, fiber.StatusOK, "All Bets Retrieved", res, nil) + } +} + +// GetBetByID godoc +// @Summary Gets bet by id +// @Description Gets a single bet by id +// @Tags bet +// @Accept json +// @Produce json +// @Param id path int true "Bet ID" +// @Success 200 {object} BetRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /bet/{id} [get] +func GetBetByID(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + betID := c.Params("id") + id, err := strconv.ParseInt(betID, 10, 64) + + if err != nil { + logger.Error("Invalid bet ID", "betID", betID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) + } + + bet, err := betSvc.GetBetByID(c.Context(), id) + + if err != nil { + logger.Error("Failed to get bet by ID", "betID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil) + } + + res := convertBet(bet) + + return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) + + } +} + +type UpdateCashOutReq struct { + CashedOut bool +} + +// UpdateCashOut godoc +// @Summary Updates the cashed out field +// @Description Updates the cashed out field +// @Tags bet +// @Accept json +// @Produce json +// @Param id path int true "Bet ID" +// @Param updateCashOut body UpdateCashOutReq true "Updates Cashed Out" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /bet/{id} [patch] +func UpdateCashOut(logger *slog.Logger, betSvc *bet.Service, + validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + betID := c.Params("id") + id, err := strconv.ParseInt(betID, 10, 64) + + if err != nil { + logger.Error("Invalid bet ID", "betID", betID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) + } + + var req UpdateCashOutReq + if err := c.BodyParser(&req); err != nil { + logger.Error("UpdateCashOutReq failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + + err = betSvc.UpdateCashOut(c.Context(), id, req.CashedOut) + + if err != nil { + logger.Error("Failed to update cash out bet", "betID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update cash out bet", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Bet updated successfully", nil, nil) + } +} + +// DeleteBet godoc +// @Summary Deletes bet by id +// @Description Deletes bet by id +// @Tags bet +// @Accept json +// @Produce json +// @Param id path int true "Bet ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /bet/{id} [delete] +func DeleteBet(logger *slog.Logger, betSvc *bet.Service, + validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + betID := c.Params("id") + id, err := strconv.ParseInt(betID, 10, 64) + + if err != nil { + logger.Error("Invalid bet ID", "betID", betID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) + } + + err = betSvc.DeleteBet(c.Context(), id) + + if err != nil { + logger.Error("Failed to delete by ID", "betID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to delete bet", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil) + } +} diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go new file mode 100644 index 0000000..696b738 --- /dev/null +++ b/internal/web_server/handlers/ticket_handler.go @@ -0,0 +1,153 @@ +package handlers + +import ( + "log/slog" + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" + "github.com/gofiber/fiber/v2" +) + +type CreateTicketReq struct { + Outcomes []int64 `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` + TotalOdds float32 `json:"total_odds" example:"4.22"` +} +type CreateTicketRes struct { + FastCode int64 `json:"fast_code" example:"1234"` +} + +// CreateTicket godoc +// @Summary Create a temporary ticket +// @Description Creates a temporary ticket +// @Tags ticket +// @Accept json +// @Produce json +// @Param createTicket body CreateTicketReq true "Creates ticket" +// @Success 200 {object} CreateTicketRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /ticket [post] +func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, + validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + var req CreateTicketReq + if err := c.BodyParser(&req); err != nil { + logger.Error("CreateTicketReq failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + + // TODO Validate Outcomes Here and make sure they didn't expire + + ticket, err := ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ + Outcomes: req.Outcomes, + Amount: domain.Currency(req.Amount), + TotalOdds: req.TotalOdds, + }) + if err != nil { + logger.Error("CreateTicketReq failed", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Internal server error", + }) + } + res := CreateTicketRes{ + FastCode: ticket.ID, + } + return response.WriteJSON(c, fiber.StatusOK, "Ticket Created", res, nil) + } +} + +type TicketRes struct { + ID int64 `json:"id" example:"1"` + Outcomes []domain.Outcome `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` + TotalOdds float32 `json:"total_odds" example:"4.22"` +} + +// GetTicketByID godoc +// @Summary Get ticket by ID +// @Description Retrieve ticket details by ticket ID +// @Tags ticket +// @Accept json +// @Produce json +// @Param id path int true "Ticket ID" +// @Success 200 {object} TicketRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /ticket/{id} [get] +func GetTicketByID(logger *slog.Logger, ticketSvc *ticket.Service, + validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + ticketID := c.Params("id") + + id, err := strconv.ParseInt(ticketID, 10, 64) + if err != nil { + logger.Error("Invalid ticket ID", "ticketID", ticketID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid ticket ID", err, nil) + } + + ticket, err := ticketSvc.GetTicketByID(c.Context(), id) + + if err != nil { + logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve ticket", err, nil) + } + + res := TicketRes{ + ID: ticket.ID, + Outcomes: ticket.Outcomes, + Amount: ticket.Amount.Float64(), + TotalOdds: ticket.TotalOdds, + } + + return response.WriteJSON(c, fiber.StatusOK, "Ticket retrieved successfully", res, nil) + + } +} + +// GetAllTickets godoc +// @Summary Get all tickets +// @Description Retrieve all tickets +// @Tags ticket +// @Accept json +// @Produce json +// @Success 200 {array} TicketRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /ticket [get] +func GetAllTickets(logger *slog.Logger, ticketSvc *ticket.Service, + validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + tickets, err := ticketSvc.GetAllTickets(c.Context()) + + if err != nil { + logger.Error("Failed to get tickets", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve tickets", err, nil) + } + + var res []TicketRes + + for _, ticket := range tickets { + res = append(res, TicketRes{ + ID: ticket.ID, + Outcomes: ticket.Outcomes, + Amount: ticket.Amount.Float64(), + TotalOdds: ticket.TotalOdds, + }) + } + + return response.WriteJSON(c, fiber.StatusOK, "All Tickets retrieved", res, nil) + + } +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c30622d..828e26c 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -27,7 +27,20 @@ func (a *App) initAppRoutes() { a.fiber.Post("/user/checkPhoneEmailExist", handlers.CheckPhoneEmailExist(a.logger, a.userSvc, a.validator)) a.fiber.Get("/user/profile", a.authMiddleware, handlers.UserProfile(a.logger, a.userSvc)) // Swagger - a.fiber.Get("/swagger/*", fiberSwagger.WrapHandler) + a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) + + // Ticket + a.fiber.Post("/ticket", handlers.CreateTicket(a.logger, a.ticketSvc, a.validator)) + a.fiber.Get("/ticket", handlers.GetAllTickets(a.logger, a.ticketSvc, a.validator)) + a.fiber.Get("/ticket/:id", handlers.GetTicketByID(a.logger, a.ticketSvc, a.validator)) + + // Bet + a.fiber.Post("/bet", handlers.CreateBet(a.logger, a.betSvc, a.validator)) + a.fiber.Get("/bet", handlers.GetAllBet(a.logger, a.betSvc, a.validator)) + a.fiber.Get("/bet/:id", handlers.GetAllBet(a.logger, a.betSvc, a.validator)) + a.fiber.Patch("/bet/:id", handlers.UpdateCashOut(a.logger, a.betSvc, a.validator)) + a.fiber.Delete("/bet/:id", handlers.DeleteBet(a.logger, a.betSvc, a.validator)) + } ///user/profile get