feat: added bet and ticket handlers

This commit is contained in:
Samuel Tariku 2025-04-01 22:03:23 +03:00
parent 0c38426549
commit 959390b506
23 changed files with 1986 additions and 162 deletions

View File

@ -11,6 +11,8 @@ import (
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms" mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "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" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server" httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
@ -46,16 +48,22 @@ func main() {
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel) logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
store := repository.NewStore(db) store := repository.NewStore(db)
v := customvalidator.NewCustomValidator(validator.New()) v := customvalidator.NewCustomValidator(validator.New())
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
mockSms := mocksms.NewMockSMS() mockSms := mocksms.NewMockSMS()
mockemail := mockemail.NewMockEmail() mockemail := mockemail.NewMockEmail()
userSvc := user.NewService(store, store, mockSms, mockemail) 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{ app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey, JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry, JwtAccessExpiry: cfg.AccessExpiry,
}, userSvc, }, userSvc, ticketSvc, betSvc,
) )
logger.Info("Starting server", "port", cfg.Port) logger.Info("Starting server", "port", cfg.Port)
if err := app.Run(); err != nil { if err := app.Run(); err != nil {
logger.Error("Failed to start server", "error", err) logger.Error("Failed to start server", "error", err)
os.Exit(1) os.Exit(1)

View File

@ -36,6 +36,44 @@ CREATE TABLE refresh_tokens (
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ NOT NULL 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------------------------------------------------------------- ----------------------------------------------seed data-------------------------------------------------------------
-------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- -------------------------------------- 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,
-- );

View File

@ -1,6 +1,6 @@
-- name: CreateBet :one -- name: CreateBet :one
INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id) 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) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *; RETURNING *;
-- name: GetAllBets :many -- name: GetAllBets :many

View File

@ -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": { "/user/checkPhoneEmailExist": {
"post": { "post": {
"description": "Check if phone number or email exist", "description": "Check if phone number or email exist",
@ -452,6 +797,24 @@ const docTemplate = `{
} }
}, },
"definitions": { "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": { "domain.Role": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -469,6 +832,57 @@ const docTemplate = `{
"RoleCashier" "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": { "handlers.CheckPhoneEmailExistReq": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.RegisterCodeReq": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.UserProfileRes": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -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": { "/user/checkPhoneEmailExist": {
"post": { "post": {
"description": "Check if phone number or email exist", "description": "Check if phone number or email exist",
@ -444,6 +789,24 @@
} }
}, },
"definitions": { "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": { "domain.Role": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -461,6 +824,57 @@
"RoleCashier" "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": { "handlers.CheckPhoneEmailExistReq": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.RegisterCodeReq": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.UserProfileRes": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -1,4 +1,18 @@
definitions: 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: domain.Role:
enum: enum:
- admin - admin
@ -13,6 +27,41 @@ definitions:
- RoleSuperAdmin - RoleSuperAdmin
- RoleBranchManager - RoleBranchManager
- RoleCashier - 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: handlers.CheckPhoneEmailExistReq:
properties: properties:
email: email:
@ -29,6 +78,51 @@ definitions:
phone_number_exist: phone_number_exist:
type: boolean type: boolean
type: object 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: handlers.RegisterCodeReq:
properties: properties:
email: email:
@ -83,6 +177,27 @@ definitions:
phoneNumber: phoneNumber:
type: string type: string
type: object 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: handlers.UserProfileRes:
properties: properties:
created_at: created_at:
@ -275,6 +390,234 @@ paths:
summary: Refresh token summary: Refresh token
tags: tags:
- auth - 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: /user/checkPhoneEmailExist:
post: post:
consumes: consumes:

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.26.0 // sqlc v1.28.0
// source: auth.sql // source: auth.sql
package dbgen package dbgen

View File

@ -12,9 +12,9 @@ import (
) )
const CreateBet = `-- name: CreateBet :one const CreateBet = `-- name: CreateBet :one
INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id) 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) 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 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 { type CreateBetParams struct {
@ -25,6 +25,7 @@ type CreateBetParams struct {
PhoneNumber string PhoneNumber string
BranchID pgtype.Int8 BranchID pgtype.Int8
UserID pgtype.Int8 UserID pgtype.Int8
IsShopBet bool
} }
func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, error) { 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.PhoneNumber,
arg.BranchID, arg.BranchID,
arg.UserID, arg.UserID,
arg.IsShopBet,
) )
var i Bet var i Bet
err := row.Scan( err := row.Scan(
@ -50,6 +52,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro
&i.CashedOut, &i.CashedOut,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.IsShopBet,
) )
return i, err return i, err
} }
@ -64,7 +67,7 @@ func (q *Queries) DeleteBet(ctx context.Context, id int64) error {
} }
const GetAllBets = `-- name: GetAllBets :many 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) { 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.CashedOut,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.IsShopBet,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -100,7 +104,7 @@ func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) {
} }
const GetBetByID = `-- name: GetBetByID :one 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) { 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.CashedOut,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.IsShopBet,
) )
return i, err return i, err
} }

View File

@ -8,6 +8,21 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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 { type Otp struct {
ID int64 ID int64
SentTo string SentTo string
@ -29,20 +44,6 @@ type RefreshToken struct {
Revoked bool 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 { type Ticket struct {
ID int64 ID int64
Amount pgtype.Int8 Amount pgtype.Int8

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.26.0 // sqlc v1.28.0
// source: otp.sql // source: otp.sql
package dbgen package dbgen

View File

@ -9,9 +9,11 @@ const (
BET_STATUS_ERROR 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 { type Bet struct {
ID int64 ID int64
Outcome []Outcome Outcomes []Outcome
Amount Currency Amount Currency
TotalOdds float32 TotalOdds float32
Status BetStatus Status BetStatus
@ -19,9 +21,24 @@ type Bet struct {
PhoneNumber string PhoneNumber string
BranchID ValidInt64 // Can Be Nullable BranchID ValidInt64 // Can Be Nullable
UserID ValidInt64 // Can Be Nullable UserID ValidInt64 // Can Be Nullable
IsShopBet bool
CashedOut 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 { func (b BetStatus) String() string {
return []string{"Pending", "Win", "Loss", "Error"}[b] return []string{"Pending", "Win", "Loss", "Error"}[b]
} }
// func isBetStatusValid()

View File

@ -7,6 +7,15 @@ type ValidInt64 struct {
Valid bool Valid bool
} }
type ValidString struct {
Value string
Valid bool
}
type ValidBool struct {
Value bool
Valid bool
}
type Currency int64 type Currency int64
// ToCurrency converts a float32 to Currency // ToCurrency converts a float32 to Currency
@ -28,11 +37,3 @@ func (m Currency) String() string {
return fmt.Sprintf("$%.2f", x) return fmt.Sprintf("$%.2f", x)
} }
type ValidString struct {
Value string
Valid bool
}
type ValidBool struct {
Value bool
Valid bool
}

View File

@ -3,7 +3,13 @@ package domain
// ID will serve as the fast code since this doesn't need to be secure // ID will serve as the fast code since this doesn't need to be secure
type Ticket struct { type Ticket struct {
ID int64 ID int64
Outcome []Outcome Outcomes []Outcome
Amount Currency
TotalOdds float32
}
type CreateTicket struct {
Outcomes []int64
Amount Currency Amount Currency
TotalOdds float32 TotalOdds float32
} }

View File

@ -8,9 +8,28 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
func (s *Store) CreateBet(ctx context.Context, bet domain.Bet) (domain.Bet, error) { func convertDBBet(bet dbgen.Bet) domain.Bet {
return 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,
},
IsShopBet: bet.IsShopBet,
}
}
newBet, err := s.queries.CreateBet(ctx, dbgen.CreateBetParams{ func convertCreateBet(bet domain.CreateBet) dbgen.CreateBetParams {
return dbgen.CreateBetParams{
Amount: int64(bet.Amount), Amount: int64(bet.Amount),
TotalOdds: bet.TotalOdds, TotalOdds: bet.TotalOdds,
Status: int32(bet.Status), Status: int32(bet.Status),
@ -24,28 +43,17 @@ func (s *Store) CreateBet(ctx context.Context, bet domain.Bet) (domain.Bet, erro
Int64: bet.UserID.Value, Int64: bet.UserID.Value,
Valid: bet.UserID.Valid, 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 { if err != nil {
return domain.Bet{}, err return domain.Bet{}, err
} }
return convertDBBet(newBet), 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
} }
@ -55,22 +63,7 @@ func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) {
return domain.Bet{}, err return domain.Bet{}, err
} }
return domain.Bet{ return convertDBBet(bet), nil
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,
},
}, nil
} }
func (s *Store) GetAllBets(ctx context.Context) ([]domain.Bet, error) { 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 var result []domain.Bet
for _, bet := range bets { for _, bet := range bets {
result = append(result, domain.Bet{ result = append(result, convertDBBet(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,
},
})
} }
return result, nil return result, nil

View File

@ -8,24 +8,30 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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{ 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 { if err != nil {
return domain.Ticket{}, err return domain.Ticket{}, err
} }
return convertDBTicket(newTicket), err
return domain.Ticket{
ID: ticket.ID,
Amount: amount,
TotalOdds: totalOdds,
}, err
} }
@ -35,11 +41,7 @@ func (s *Store) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, err
return domain.Ticket{}, err return domain.Ticket{}, err
} }
return domain.Ticket{ return convertDBTicket(ticket), nil
ID: ticket.ID,
Amount: domain.Currency(ticket.Amount.Int64),
TotalOdds: ticket.TotalOdds,
}, nil
} }
func (s *Store) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) { 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 var result []domain.Ticket
for _, ticket := range tickets { for _, ticket := range tickets {
result = append(result, domain.Ticket{ result = append(result, convertDBTicket(ticket))
ID: ticket.ID,
Amount: domain.Currency(ticket.Amount.Int64),
TotalOdds: ticket.TotalOdds,
})
} }
return result, nil return result, nil

View File

@ -1,4 +1,4 @@
package ticket package bet
import ( import (
"context" "context"
@ -7,7 +7,7 @@ import (
) )
type BetStore interface { 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) GetBetByID(ctx context.Context, id int64) (domain.Bet, error)
GetAllBets(ctx context.Context) ([]domain.Bet, error) GetAllBets(ctx context.Context) ([]domain.Bet, error)
UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error

View File

@ -1,4 +1,4 @@
package ticket package bet
import ( import (
"context" "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) return s.betStore.CreateBet(ctx, bet)
} }
func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) { func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) {

View File

@ -7,7 +7,7 @@ import (
) )
type TicketStore interface { 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) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error)
GetAllTickets(ctx context.Context) ([]domain.Ticket, error) GetAllTickets(ctx context.Context) ([]domain.Ticket, error)
DeleteOldTickets(ctx context.Context) error DeleteOldTickets(ctx context.Context) error

View File

@ -16,8 +16,8 @@ func NewService(ticketStore TicketStore) *Service {
} }
} }
func (s *Service) CreateTicket(ctx context.Context, amount domain.Currency, totalOdds float32) (domain.Ticket, error) { func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) {
return s.ticketStore.CreateTicket(ctx, amount, totalOdds) return s.ticketStore.CreateTicket(ctx, ticket)
} }
func (s *Service) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) { func (s *Service) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) {
return s.ticketStore.GetTicketByID(ctx, id) return s.ticketStore.GetTicketByID(ctx, id)

View File

@ -5,6 +5,8 @@ import (
"log/slog" "log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "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" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
@ -18,6 +20,8 @@ type App struct {
port int port int
authSvc *authentication.Service authSvc *authentication.Service
userSvc *user.Service userSvc *user.Service
ticketSvc *ticket.Service
betSvc *bet.Service
validator *customvalidator.CustomValidator validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig JwtConfig jwtutil.JwtConfig
} }
@ -28,6 +32,8 @@ func NewApp(
logger *slog.Logger, logger *slog.Logger,
JwtConfig jwtutil.JwtConfig, JwtConfig jwtutil.JwtConfig,
userSvc *user.Service, userSvc *user.Service,
ticketSvc *ticket.Service,
betSvc *bet.Service,
) *App { ) *App {
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
CaseSensitive: true, CaseSensitive: true,
@ -43,6 +49,8 @@ func NewApp(
logger: logger, logger: logger,
JwtConfig: JwtConfig, JwtConfig: JwtConfig,
userSvc: userSvc, userSvc: userSvc,
ticketSvc: ticketSvc,
betSvc: betSvc,
} }
s.initAppRoutes() s.initAppRoutes()

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -27,7 +27,20 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/user/checkPhoneEmailExist", handlers.CheckPhoneEmailExist(a.logger, a.userSvc, a.validator)) 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)) a.fiber.Get("/user/profile", a.authMiddleware, handlers.UserProfile(a.logger, a.userSvc))
// Swagger // 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 ///user/profile get