fix: moving ticket logic into service

This commit is contained in:
Samuel Tariku 2025-06-19 19:11:19 +03:00
parent 93d64d06d7
commit b0803c968a
18 changed files with 874 additions and 841 deletions

View File

@ -102,7 +102,6 @@ func main() {
userSvc := user.NewService(store, store, cfg)
eventSvc := event.New(cfg.Bet365Token, store)
oddsSvc := odds.New(store, cfg, logger)
ticketSvc := ticket.NewService(store)
notificationRepo := repository.NewNotificationRepository(store)
virtuaGamesRepo := repository.NewVirtualGameRepository(store)
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
@ -120,6 +119,7 @@ func main() {
branchSvc := branch.NewService(store)
companySvc := company.NewService(store)
leagueSvc := league.New(store)
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger)
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger)
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
referalRepo := repository.NewReferralRepository(store)

View File

@ -76,4 +76,6 @@ DROP TABLE IF EXISTS refresh_tokens;
DROP TABLE IF EXISTS otps;
DROP TABLE IF EXISTS odds;
DROP TABLE IF EXISTS events;
DROP TABLE IF EXISTS leagues;
DROP TABLE IF EXISTS leagues;
DROP TABLE IF EXISTS teams;
DROP TABLE IF EXISTS settings;

View File

@ -264,6 +264,12 @@ CREATE TABLE teams (
bet365_id INT,
logo_url TEXT
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Views
CREATE VIEW companies_details AS
SELECT companies.*,

View File

@ -0,0 +1,5 @@
-- Settings Initial Data
INSERT INTO settings (key, value)
VALUES ('total_winnings_limit', '1000000') ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value;

9
db/query/settings.sql Normal file
View File

@ -0,0 +1,9 @@
-- name: GetSettings :many
SELECT *
from settings;
-- name: SaveSetting :one
INSERT INTO settings (key, value, updated_at)
VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value
RETURNING *;

View File

@ -811,76 +811,6 @@ const docTemplate = `{
}
}
},
"/api/veli/launch/{game_id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Generates authenticated launch URL for Veli games",
"tags": [
"Veli Games"
],
"summary": "Launch a Veli game",
"parameters": [
{
"type": "string",
"description": "Game ID (e.g., veli_aviator_v1)",
"name": "game_id",
"in": "path",
"required": true
},
{
"type": "string",
"default": "USD",
"description": "Currency code",
"name": "currency",
"in": "query"
},
{
"enum": [
"real",
"demo"
],
"type": "string",
"default": "real",
"description": "Game mode",
"name": "mode",
"in": "query"
}
],
"responses": {
"200": {
"description": "Returns launch URL",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/login": {
"post": {
"description": "Login customer",
@ -3377,7 +3307,7 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.TicketRes"
"$ref": "#/definitions/domain.TicketRes"
}
}
},
@ -3414,7 +3344,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.CreateTicketReq"
"$ref": "#/definitions/domain.CreateTicketReq"
}
}
],
@ -3422,7 +3352,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.CreateTicketRes"
"$ref": "#/definitions/domain.CreateTicketRes"
}
},
"400": {
@ -3466,7 +3396,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TicketRes"
"$ref": "#/definitions/domain.TicketRes"
}
},
"400": {
@ -3484,6 +3414,38 @@ const docTemplate = `{
}
}
},
"/top-leagues": {
"get": {
"description": "Retrieve all top leagues",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"prematch"
],
"summary": "Retrieve all top leagues",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.UpcomingEvent"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/transaction": {
"get": {
"description": "Gets all the transactions",
@ -4577,70 +4539,6 @@ const docTemplate = `{
}
}
}
},
"/webhooks/veli": {
"post": {
"description": "Processes game round settlements from Veli",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Veli Games"
],
"summary": "Veli Games webhook handler",
"parameters": [
{
"description": "Callback payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VeliCallback"
}
}
],
"responses": {
"200": {
"description": "Callback processed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid payload",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Invalid signature",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Processing error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
@ -4956,6 +4854,52 @@ const docTemplate = `{
}
}
},
"domain.CreateTicketOutcomeReq": {
"type": "object",
"properties": {
"event_id": {
"description": "TicketID int64 ` + "`" + `json:\"ticket_id\" example:\"1\"` + "`" + `",
"type": "integer",
"example": 1
},
"market_id": {
"type": "integer",
"example": 1
},
"odd_id": {
"type": "integer",
"example": 1
}
}
},
"domain.CreateTicketReq": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.CreateTicketOutcomeReq"
}
}
}
},
"domain.CreateTicketRes": {
"type": "object",
"properties": {
"created_number": {
"type": "integer",
"example": 3
},
"fast_code": {
"type": "integer",
"example": 1234
}
}
},
"domain.DashboardSummary": {
"type": "object",
"properties": {
@ -5132,6 +5076,10 @@ const docTemplate = `{
"type": "boolean",
"example": false
},
"is_featured": {
"type": "boolean",
"example": false
},
"name": {
"type": "string",
"example": "BPL"
@ -5193,6 +5141,17 @@ const docTemplate = `{
}
}
},
"domain.OtpProvider": {
"type": "string",
"enum": [
"twilio",
"aformessage"
],
"x-enum-varnames": [
"TwilioSms",
"AfroMessage"
]
},
"domain.OutcomeStatus": {
"type": "integer",
"enum": [
@ -5482,6 +5441,29 @@ const docTemplate = `{
}
}
},
"domain.TicketRes": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"id": {
"type": "integer",
"example": 1
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.TicketOutcome"
}
},
"total_odds": {
"type": "number",
"example": 4.22
}
}
},
"domain.UpcomingEvent": {
"type": "object",
"properties": {
@ -5551,51 +5533,6 @@ const docTemplate = `{
}
}
},
"domain.VeliCallback": {
"type": "object",
"properties": {
"amount": {
"description": "Transaction amount",
"type": "number"
},
"currency": {
"description": "e.g., \"USD\"",
"type": "string"
},
"event_type": {
"description": "\"bet_placed\", \"game_result\", etc.",
"type": "string"
},
"game_id": {
"description": "e.g., \"veli_aviator_v1\"",
"type": "string"
},
"multiplier": {
"description": "For games with multipliers (Aviator/Plinko)",
"type": "number"
},
"round_id": {
"description": "Unique round identifier (replaces transaction_id)",
"type": "string"
},
"session_id": {
"description": "Matches VirtualGameSession.SessionToken",
"type": "string"
},
"signature": {
"description": "HMAC-SHA256",
"type": "string"
},
"timestamp": {
"description": "Unix timestamp",
"type": "integer"
},
"user_id": {
"description": "Veli's user identifier",
"type": "string"
}
}
},
"domain.VirtualGame": {
"type": "object",
"properties": {
@ -5994,52 +5931,6 @@ const docTemplate = `{
}
}
},
"handlers.CreateTicketOutcomeReq": {
"type": "object",
"properties": {
"event_id": {
"description": "TicketID int64 ` + "`" + `json:\"ticket_id\" example:\"1\"` + "`" + `",
"type": "integer",
"example": 1
},
"market_id": {
"type": "integer",
"example": 1
},
"odd_id": {
"type": "integer",
"example": 1
}
}
},
"handlers.CreateTicketReq": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.CreateTicketOutcomeReq"
}
}
}
},
"handlers.CreateTicketRes": {
"type": "object",
"properties": {
"created_number": {
"type": "integer",
"example": 3
},
"fast_code": {
"type": "integer",
"example": 1234
}
}
},
"handlers.CreateTransactionReq": {
"type": "object",
"properties": {
@ -6249,6 +6140,9 @@ const docTemplate = `{
},
"handlers.RegisterCodeReq": {
"type": "object",
"required": [
"provider"
],
"properties": {
"email": {
"type": "string",
@ -6257,11 +6151,22 @@ const docTemplate = `{
"phone_number": {
"type": "string",
"example": "1234567890"
},
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
}
}
},
"handlers.RegisterUserReq": {
"type": "object",
"required": [
"provider"
],
"properties": {
"email": {
"type": "string",
@ -6287,6 +6192,14 @@ const docTemplate = `{
"type": "string",
"example": "1234567890"
},
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
},
"referal_code": {
"type": "string",
"example": "ABC123"
@ -6295,6 +6208,9 @@ const docTemplate = `{
},
"handlers.ResetCodeReq": {
"type": "object",
"required": [
"provider"
],
"properties": {
"email": {
"type": "string",
@ -6303,6 +6219,14 @@ const docTemplate = `{
"phone_number": {
"type": "string",
"example": "1234567890"
},
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
}
}
},
@ -6371,29 +6295,6 @@ const docTemplate = `{
}
}
},
"handlers.TicketRes": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"id": {
"type": "integer",
"example": 1
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.TicketOutcome"
}
},
"total_odds": {
"type": "number",
"example": 4.22
}
}
},
"handlers.TransactionRes": {
"type": "object",
"properties": {

View File

@ -803,76 +803,6 @@
}
}
},
"/api/veli/launch/{game_id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Generates authenticated launch URL for Veli games",
"tags": [
"Veli Games"
],
"summary": "Launch a Veli game",
"parameters": [
{
"type": "string",
"description": "Game ID (e.g., veli_aviator_v1)",
"name": "game_id",
"in": "path",
"required": true
},
{
"type": "string",
"default": "USD",
"description": "Currency code",
"name": "currency",
"in": "query"
},
{
"enum": [
"real",
"demo"
],
"type": "string",
"default": "real",
"description": "Game mode",
"name": "mode",
"in": "query"
}
],
"responses": {
"200": {
"description": "Returns launch URL",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/login": {
"post": {
"description": "Login customer",
@ -3369,7 +3299,7 @@
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.TicketRes"
"$ref": "#/definitions/domain.TicketRes"
}
}
},
@ -3406,7 +3336,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.CreateTicketReq"
"$ref": "#/definitions/domain.CreateTicketReq"
}
}
],
@ -3414,7 +3344,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.CreateTicketRes"
"$ref": "#/definitions/domain.CreateTicketRes"
}
},
"400": {
@ -3458,7 +3388,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.TicketRes"
"$ref": "#/definitions/domain.TicketRes"
}
},
"400": {
@ -3476,6 +3406,38 @@
}
}
},
"/top-leagues": {
"get": {
"description": "Retrieve all top leagues",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"prematch"
],
"summary": "Retrieve all top leagues",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.UpcomingEvent"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/transaction": {
"get": {
"description": "Gets all the transactions",
@ -4569,70 +4531,6 @@
}
}
}
},
"/webhooks/veli": {
"post": {
"description": "Processes game round settlements from Veli",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Veli Games"
],
"summary": "Veli Games webhook handler",
"parameters": [
{
"description": "Callback payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VeliCallback"
}
}
],
"responses": {
"200": {
"description": "Callback processed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid payload",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Invalid signature",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Processing error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
@ -4948,6 +4846,52 @@
}
}
},
"domain.CreateTicketOutcomeReq": {
"type": "object",
"properties": {
"event_id": {
"description": "TicketID int64 `json:\"ticket_id\" example:\"1\"`",
"type": "integer",
"example": 1
},
"market_id": {
"type": "integer",
"example": 1
},
"odd_id": {
"type": "integer",
"example": 1
}
}
},
"domain.CreateTicketReq": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.CreateTicketOutcomeReq"
}
}
}
},
"domain.CreateTicketRes": {
"type": "object",
"properties": {
"created_number": {
"type": "integer",
"example": 3
},
"fast_code": {
"type": "integer",
"example": 1234
}
}
},
"domain.DashboardSummary": {
"type": "object",
"properties": {
@ -5124,6 +5068,10 @@
"type": "boolean",
"example": false
},
"is_featured": {
"type": "boolean",
"example": false
},
"name": {
"type": "string",
"example": "BPL"
@ -5185,6 +5133,17 @@
}
}
},
"domain.OtpProvider": {
"type": "string",
"enum": [
"twilio",
"aformessage"
],
"x-enum-varnames": [
"TwilioSms",
"AfroMessage"
]
},
"domain.OutcomeStatus": {
"type": "integer",
"enum": [
@ -5474,6 +5433,29 @@
}
}
},
"domain.TicketRes": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"id": {
"type": "integer",
"example": 1
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.TicketOutcome"
}
},
"total_odds": {
"type": "number",
"example": 4.22
}
}
},
"domain.UpcomingEvent": {
"type": "object",
"properties": {
@ -5543,51 +5525,6 @@
}
}
},
"domain.VeliCallback": {
"type": "object",
"properties": {
"amount": {
"description": "Transaction amount",
"type": "number"
},
"currency": {
"description": "e.g., \"USD\"",
"type": "string"
},
"event_type": {
"description": "\"bet_placed\", \"game_result\", etc.",
"type": "string"
},
"game_id": {
"description": "e.g., \"veli_aviator_v1\"",
"type": "string"
},
"multiplier": {
"description": "For games with multipliers (Aviator/Plinko)",
"type": "number"
},
"round_id": {
"description": "Unique round identifier (replaces transaction_id)",
"type": "string"
},
"session_id": {
"description": "Matches VirtualGameSession.SessionToken",
"type": "string"
},
"signature": {
"description": "HMAC-SHA256",
"type": "string"
},
"timestamp": {
"description": "Unix timestamp",
"type": "integer"
},
"user_id": {
"description": "Veli's user identifier",
"type": "string"
}
}
},
"domain.VirtualGame": {
"type": "object",
"properties": {
@ -5986,52 +5923,6 @@
}
}
},
"handlers.CreateTicketOutcomeReq": {
"type": "object",
"properties": {
"event_id": {
"description": "TicketID int64 `json:\"ticket_id\" example:\"1\"`",
"type": "integer",
"example": 1
},
"market_id": {
"type": "integer",
"example": 1
},
"odd_id": {
"type": "integer",
"example": 1
}
}
},
"handlers.CreateTicketReq": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.CreateTicketOutcomeReq"
}
}
}
},
"handlers.CreateTicketRes": {
"type": "object",
"properties": {
"created_number": {
"type": "integer",
"example": 3
},
"fast_code": {
"type": "integer",
"example": 1234
}
}
},
"handlers.CreateTransactionReq": {
"type": "object",
"properties": {
@ -6241,6 +6132,9 @@
},
"handlers.RegisterCodeReq": {
"type": "object",
"required": [
"provider"
],
"properties": {
"email": {
"type": "string",
@ -6249,11 +6143,22 @@
"phone_number": {
"type": "string",
"example": "1234567890"
},
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
}
}
},
"handlers.RegisterUserReq": {
"type": "object",
"required": [
"provider"
],
"properties": {
"email": {
"type": "string",
@ -6279,6 +6184,14 @@
"type": "string",
"example": "1234567890"
},
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
},
"referal_code": {
"type": "string",
"example": "ABC123"
@ -6287,6 +6200,9 @@
},
"handlers.ResetCodeReq": {
"type": "object",
"required": [
"provider"
],
"properties": {
"email": {
"type": "string",
@ -6295,6 +6211,14 @@
"phone_number": {
"type": "string",
"example": "1234567890"
},
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
}
}
},
@ -6363,29 +6287,6 @@
}
}
},
"handlers.TicketRes": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"id": {
"type": "integer",
"example": 1
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.TicketOutcome"
}
},
"total_odds": {
"type": "number",
"example": 4.22
}
}
},
"handlers.TransactionRes": {
"type": "object",
"properties": {

View File

@ -211,6 +211,38 @@ definitions:
- $ref: '#/definitions/domain.OutcomeStatus'
example: 1
type: object
domain.CreateTicketOutcomeReq:
properties:
event_id:
description: TicketID int64 `json:"ticket_id" example:"1"`
example: 1
type: integer
market_id:
example: 1
type: integer
odd_id:
example: 1
type: integer
type: object
domain.CreateTicketReq:
properties:
amount:
example: 100
type: number
outcomes:
items:
$ref: '#/definitions/domain.CreateTicketOutcomeReq'
type: array
type: object
domain.CreateTicketRes:
properties:
created_number:
example: 3
type: integer
fast_code:
example: 1234
type: integer
type: object
domain.DashboardSummary:
properties:
active_admins:
@ -337,6 +369,9 @@ definitions:
is_active:
example: false
type: boolean
is_featured:
example: false
type: boolean
name:
example: BPL
type: string
@ -378,6 +413,14 @@ definitions:
source:
type: string
type: object
domain.OtpProvider:
enum:
- twilio
- aformessage
type: string
x-enum-varnames:
- TwilioSms
- AfroMessage
domain.OutcomeStatus:
enum:
- 0
@ -583,6 +626,22 @@ definitions:
example: 1
type: integer
type: object
domain.TicketRes:
properties:
amount:
example: 100
type: number
id:
example: 1
type: integer
outcomes:
items:
$ref: '#/definitions/domain.TicketOutcome'
type: array
total_odds:
example: 4.22
type: number
type: object
domain.UpcomingEvent:
properties:
away_kit_image:
@ -632,39 +691,6 @@ definitions:
- $ref: '#/definitions/domain.EventStatus'
description: Match Status for event
type: object
domain.VeliCallback:
properties:
amount:
description: Transaction amount
type: number
currency:
description: e.g., "USD"
type: string
event_type:
description: '"bet_placed", "game_result", etc.'
type: string
game_id:
description: e.g., "veli_aviator_v1"
type: string
multiplier:
description: For games with multipliers (Aviator/Plinko)
type: number
round_id:
description: Unique round identifier (replaces transaction_id)
type: string
session_id:
description: Matches VirtualGameSession.SessionToken
type: string
signature:
description: HMAC-SHA256
type: string
timestamp:
description: Unix timestamp
type: integer
user_id:
description: Veli's user identifier
type: string
type: object
domain.VirtualGame:
properties:
category:
@ -946,38 +972,6 @@ definitions:
example: SportsBook
type: string
type: object
handlers.CreateTicketOutcomeReq:
properties:
event_id:
description: TicketID int64 `json:"ticket_id" example:"1"`
example: 1
type: integer
market_id:
example: 1
type: integer
odd_id:
example: 1
type: integer
type: object
handlers.CreateTicketReq:
properties:
amount:
example: 100
type: number
outcomes:
items:
$ref: '#/definitions/handlers.CreateTicketOutcomeReq'
type: array
type: object
handlers.CreateTicketRes:
properties:
created_number:
example: 3
type: integer
fast_code:
example: 1234
type: integer
type: object
handlers.CreateTransactionReq:
properties:
account_name:
@ -1126,6 +1120,12 @@ definitions:
phone_number:
example: "1234567890"
type: string
provider:
allOf:
- $ref: '#/definitions/domain.OtpProvider'
example: twilio
required:
- provider
type: object
handlers.RegisterUserReq:
properties:
@ -1147,9 +1147,15 @@ definitions:
phone_number:
example: "1234567890"
type: string
provider:
allOf:
- $ref: '#/definitions/domain.OtpProvider'
example: twilio
referal_code:
example: ABC123
type: string
required:
- provider
type: object
handlers.ResetCodeReq:
properties:
@ -1159,6 +1165,12 @@ definitions:
phone_number:
example: "1234567890"
type: string
provider:
allOf:
- $ref: '#/definitions/domain.OtpProvider'
example: twilio
required:
- provider
type: object
handlers.ResetPasswordReq:
properties:
@ -1205,22 +1217,6 @@ definitions:
example: SportsBook
type: string
type: object
handlers.TicketRes:
properties:
amount:
example: 100
type: number
id:
example: 1
type: integer
outcomes:
items:
$ref: '#/definitions/domain.TicketOutcome'
type: array
total_odds:
example: 4.22
type: number
type: object
handlers.TransactionRes:
properties:
account_name:
@ -2077,52 +2073,6 @@ paths:
summary: Process Alea Play game callback
tags:
- Alea Virtual Games
/api/veli/launch/{game_id}:
get:
description: Generates authenticated launch URL for Veli games
parameters:
- description: Game ID (e.g., veli_aviator_v1)
in: path
name: game_id
required: true
type: string
- default: USD
description: Currency code
in: query
name: currency
type: string
- default: real
description: Game mode
enum:
- real
- demo
in: query
name: mode
type: string
responses:
"200":
description: Returns launch URL
schema:
additionalProperties:
type: string
type: object
"400":
description: Invalid request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: Launch a Veli game
tags:
- Veli Games
/auth/login:
post:
consumes:
@ -3766,7 +3716,7 @@ paths:
description: OK
schema:
items:
$ref: '#/definitions/handlers.TicketRes'
$ref: '#/definitions/domain.TicketRes'
type: array
"400":
description: Bad Request
@ -3789,14 +3739,14 @@ paths:
name: createTicket
required: true
schema:
$ref: '#/definitions/handlers.CreateTicketReq'
$ref: '#/definitions/domain.CreateTicketReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.CreateTicketRes'
$ref: '#/definitions/domain.CreateTicketRes'
"400":
description: Bad Request
schema:
@ -3825,7 +3775,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.TicketRes'
$ref: '#/definitions/domain.TicketRes'
"400":
description: Bad Request
schema:
@ -3837,6 +3787,27 @@ paths:
summary: Get ticket by ID
tags:
- ticket
/top-leagues:
get:
consumes:
- application/json
description: Retrieve all top leagues
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.UpcomingEvent'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Retrieve all top leagues
tags:
- prematch
/transaction:
get:
consumes:
@ -4551,48 +4522,6 @@ paths:
summary: Activate and Deactivate Wallet
tags:
- wallet
/webhooks/veli:
post:
consumes:
- application/json
description: Processes game round settlements from Veli
parameters:
- description: Callback payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.VeliCallback'
produces:
- application/json
responses:
"200":
description: Callback processed
schema:
additionalProperties:
type: string
type: object
"400":
description: Invalid payload
schema:
additionalProperties:
type: string
type: object
"403":
description: Invalid signature
schema:
additionalProperties:
type: string
type: object
"500":
description: Processing error
schema:
additionalProperties:
type: string
type: object
summary: Veli Games webhook handler
tags:
- Veli Games
securityDefinitions:
Bearer:
in: header

View File

@ -324,6 +324,13 @@ type Result struct {
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type Setting struct {
Key string `json:"key"`
Value string `json:"value"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type SupportedOperation struct {
ID int64 `json:"id"`
Name string `json:"name"`

65
gen/db/settings.sql.go Normal file
View File

@ -0,0 +1,65 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// source: settings.sql
package dbgen
import (
"context"
)
const GetSettings = `-- name: GetSettings :many
SELECT key, value, created_at, updated_at
from settings
`
func (q *Queries) GetSettings(ctx context.Context) ([]Setting, error) {
rows, err := q.db.Query(ctx, GetSettings)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Setting
for rows.Next() {
var i Setting
if err := rows.Scan(
&i.Key,
&i.Value,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const SaveSetting = `-- name: SaveSetting :one
INSERT INTO settings (key, value, updated_at)
VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO
UPDATE
SET value = EXCLUDED.value
RETURNING key, value, created_at, updated_at
`
type SaveSettingParams struct {
Key string `json:"key"`
Value string `json:"value"`
}
func (q *Queries) SaveSetting(ctx context.Context, arg SaveSettingParams) (Setting, error) {
row := q.db.QueryRow(ctx, SaveSetting, arg.Key, arg.Value)
var i Setting
err := row.Scan(
&i.Key,
&i.Value,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@ -0,0 +1,15 @@
package domain
import "time"
type Setting struct {
Key string
Value string
UpdatedAt time.Time
}
type SettingRes struct {
Key string `json:"key"`
Value string `json:"value"`
UpdatedAt string `json:"updated_at"`
}

View File

@ -53,3 +53,31 @@ type CreateTicket struct {
TotalOdds float32
IP string
}
type CreateTicketOutcomeReq struct {
// TicketID int64 `json:"ticket_id" example:"1"`
EventID int64 `json:"event_id" example:"1"`
OddID int64 `json:"odd_id" example:"1"`
MarketID int64 `json:"market_id" example:"1"`
// HomeTeamName string `json:"home_team_name" example:"Manchester"`
// AwayTeamName string `json:"away_team_name" example:"Liverpool"`
// MarketName string `json:"market_name" example:"Fulltime Result"`
// Odd float32 `json:"odd" example:"1.5"`
// OddName string `json:"odd_name" example:"1"`
// Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
}
type CreateTicketReq struct {
Outcomes []CreateTicketOutcomeReq `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
}
type CreateTicketRes struct {
FastCode int64 `json:"fast_code" example:"1234"`
CreatedNumber int64 `json:"created_number" example:"3"`
}
type TicketRes struct {
ID int64 `json:"id" example:"1"`
Outcomes []TicketOutcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
}

View File

@ -0,0 +1,49 @@
package repository
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
func (s *Store) GetSettings(ctx context.Context) ([]domain.Setting, error) {
settings, err := s.queries.GetSettings(ctx)
if err != nil {
domain.MongoDBLogger.Error("failed to get all settings", zap.Error(err))
}
var result []domain.Setting = make([]domain.Setting, 0, len(settings))
for _, setting := range settings {
result = append(result, domain.Setting{
Key: setting.Key,
Value: setting.Value,
UpdatedAt: setting.UpdatedAt.Time,
})
}
return result, nil
}
func (s *Store) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) {
dbSetting, err := s.queries.SaveSetting(ctx, dbgen.SaveSettingParams{
Key: key,
Value: value,
})
if err != nil {
domain.MongoDBLogger.Error("failed to update setting", zap.String("key", key), zap.String("value", value), zap.Error(err))
return domain.Setting{}, err
}
setting := domain.Setting{
Key: dbSetting.Key,
Value: dbSetting.Value,
}
return setting, err
}

View File

@ -29,6 +29,11 @@ var (
ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events")
ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending")
ErrEventHasBeenRemoved = errors.New("Event has been removed")
ErrEventHasNotEnded = errors.New("Event has not ended yet")
ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid")
ErrBranchIDRequired = errors.New("Branch ID required for this role")
ErrOutcomeLimit = errors.New("Too many outcomes on a single bet")
)
type Service struct {
@ -41,7 +46,15 @@ type Service struct {
mongoLogger *zap.Logger
}
func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.ServiceImpl, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger, mongoLogger *zap.Logger) *Service {
func NewService(
betStore BetStore,
eventSvc event.Service,
prematchSvc odds.ServiceImpl,
walletSvc wallet.Service,
branchSvc branch.Service,
logger *slog.Logger,
mongoLogger *zap.Logger,
) *Service {
return &Service{
betStore: betStore,
eventSvc: eventSvc,
@ -53,13 +66,6 @@ func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.Serv
}
}
var (
ErrEventHasNotEnded = errors.New("Event has not ended yet")
ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid")
ErrBranchIDRequired = errors.New("Branch ID required for this role")
ErrOutcomeLimit = errors.New("Too many outcomes on a single bet")
)
func (s *Service) GenerateCashoutID() (string, error) {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
const length int = 13

View File

@ -0,0 +1,12 @@
package settings
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type SettingStore interface {
GetSettings(ctx context.Context) ([]domain.Setting, error)
SaveSetting(ctx context.Context, key, value string) (domain.Setting, error)
}

View File

@ -0,0 +1,25 @@
package settings
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Service struct {
settingStore SettingStore
}
func NewService(settingStore SettingStore) *Service {
return &Service{
settingStore: settingStore,
}
}
func (s *Service) GetSettings(ctx context.Context) ([]domain.Setting, error) {
return s.settingStore.GetSettings(ctx)
}
func (s *Service) SaveSetting(ctx context.Context, key, value string) (domain.Setting, error) {
return s.settingStore.SaveSetting(ctx, key, value)
}

View File

@ -2,24 +2,231 @@ package ticket
import (
"context"
"encoding/json"
"errors"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"go.uber.org/zap"
)
var (
// ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events")
// ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending")
ErrEventHasNotEnded = errors.New("Event has not ended yet")
ErrNoEventsAvailable = errors.New("Not enough events available with the given filters")
ErrEventHasBeenRemoved = errors.New("Event has been removed")
ErrTooManyOutcomesForTicket = errors.New("Too many odds/outcomes for a single ticket")
ErrTicketAmountTooHigh = errors.New("Cannot create a ticket with an amount above limit")
ErrTicketLimitForSingleUser = errors.New("Number of Ticket Limit reached")
ErrTicketWinningTooHigh = errors.New("Total Winnings over set limit")
ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid")
)
type Service struct {
ticketStore TicketStore
eventSvc event.Service
prematchSvc odds.ServiceImpl
mongoLogger *zap.Logger
}
func NewService(ticketStore TicketStore) *Service {
func NewService(
ticketStore TicketStore,
eventSvc event.Service,
prematchSvc odds.ServiceImpl,
mongoLogger *zap.Logger,
) *Service {
return &Service{
ticketStore: ticketStore,
eventSvc: eventSvc,
prematchSvc: prematchSvc,
mongoLogger: mongoLogger,
}
}
func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) {
return s.ticketStore.CreateTicket(ctx, ticket)
func (s *Service) GenerateTicketOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64) (domain.CreateTicketOutcome, error) {
eventIDStr := strconv.FormatInt(eventID, 10)
marketIDStr := strconv.FormatInt(marketID, 10)
oddIDStr := strconv.FormatInt(oddID, 10)
event, err := s.eventSvc.GetUpcomingEventByID(ctx, eventIDStr)
if err != nil {
s.mongoLogger.Error("failed to fetch upcoming event by ID",
zap.Int64("event_id", eventID),
zap.Error(err),
)
return domain.CreateTicketOutcome{}, ErrEventHasBeenRemoved
}
// Checking to make sure the event hasn't already started
currentTime := time.Now()
if event.StartTime.Before(currentTime) {
s.mongoLogger.Error("event has already started",
zap.Int64("event_id", eventID),
zap.Time("event_start_time", event.StartTime),
zap.Time("current_time", currentTime),
)
return domain.CreateTicketOutcome{}, ErrEventHasNotEnded
}
odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr)
if err != nil {
s.mongoLogger.Error("failed to get raw odds by market ID",
zap.Int64("event_id", eventID),
zap.Int64("market_id", marketID),
zap.Error(err),
)
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil)
}
type rawOddType struct {
ID string
Name string
Odds string
Header string
Handicap string
}
var selectedOdd rawOddType
var isOddFound bool = false
for _, raw := range odds.RawOdds {
var rawOdd rawOddType
rawBytes, err := json.Marshal(raw)
err = json.Unmarshal(rawBytes, &rawOdd)
if err != nil {
s.mongoLogger.Error("failed to unmarshal raw ods",
zap.Int64("event_id", eventID),
zap.String("rawOddID", rawOdd.ID),
zap.Error(err),
)
continue
}
if rawOdd.ID == oddIDStr {
selectedOdd = rawOdd
isOddFound = true
}
}
if !isOddFound {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil)
s.mongoLogger.Error("Invalid Odd ID",
zap.Int64("event_id", eventID),
zap.String("oddIDStr", oddIDStr),
)
return domain.CreateTicketOutcome{}, ErrRawOddInvalid
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
if err != nil {
s.mongoLogger.Error("failed to parse selected odd value",
zap.String("odd", selectedOdd.Odds),
zap.Int64("odd_id", oddID),
zap.Error(err),
)
return domain.CreateTicketOutcome{}, err
}
newOutcome := domain.CreateTicketOutcome{
EventID: eventID,
OddID: oddID,
MarketID: marketID,
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: event.StartTime,
}
// outcomes = append(outcomes, )
return newOutcome, nil
}
func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq, clientIP string) (domain.Ticket, int64, error) {
// TODO Validate Outcomes Here and make sure they didn't expire
// Validation for creating tickets
if len(req.Outcomes) > 30 {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil)
return domain.Ticket{}, 0, ErrTooManyOutcomesForTicket
}
if req.Amount > 100000 {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with an amount above 100,000 birr", nil, nil)
return domain.Ticket{}, 0, ErrTicketAmountTooHigh
}
count, err := s.CountTicketByIP(ctx, clientIP)
if err != nil {
// return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching user info", nil, nil)
return domain.Ticket{}, 0, err
}
if count > 50 {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Ticket Limit reached", nil, nil)
return domain.Ticket{}, 0, ErrTicketLimitForSingleUser
}
var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes))
var totalOdds float32 = 1
for _, outcomeReq := range req.Outcomes {
newOutcome, err := s.GenerateTicketOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
if err != nil {
s.mongoLogger.Error("failed to generate outcome",
zap.Int64("event_id", outcomeReq.EventID),
zap.Int64("market_id", outcomeReq.MarketID),
zap.Int64("odd_id", outcomeReq.OddID),
zap.Error(err),
)
return domain.Ticket{}, 0, err
}
totalOdds *= float32(newOutcome.Odd)
outcomes = append(outcomes, newOutcome)
}
totalWinnings := req.Amount * totalOdds
if totalWinnings > 1000000 {
s.mongoLogger.Error("Total Winnings over limit", zap.Float32("Total Odds", totalOdds), zap.Float32("amount", req.Amount))
// return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with 1,000,000 winnings", nil, nil)
return domain.Ticket{}, 0, ErrTicketWinningTooHigh
}
ticket, err := s.ticketStore.CreateTicket(ctx, domain.CreateTicket{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
IP: clientIP,
})
if err != nil {
s.mongoLogger.Error("Error Creating Ticket", zap.Float32("Total Odds", totalOdds), zap.Float32("amount", req.Amount))
return domain.Ticket{}, 0, err
}
// Add the ticket id now that it has fetched from the database
for index := range outcomes {
outcomes[index].TicketID = ticket.ID
}
rows, err := s.CreateTicketOutcome(ctx, outcomes)
if err != nil {
s.mongoLogger.Error("Error Creating Ticket Outcomes", zap.Any("outcomes", outcomes))
return domain.Ticket{}, rows, err
}
return ticket, rows, nil
}
// func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) {
// return s.ticketStore.CreateTicket(ctx, ticket)
// }
func (s *Service) CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) {
return s.ticketStore.CreateTicketOutcome(ctx, outcomes)
}

View File

@ -1,56 +1,27 @@
package handlers
import (
"encoding/json"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
type CreateTicketOutcomeReq struct {
// TicketID int64 `json:"ticket_id" example:"1"`
EventID int64 `json:"event_id" example:"1"`
OddID int64 `json:"odd_id" example:"1"`
MarketID int64 `json:"market_id" example:"1"`
// HomeTeamName string `json:"home_team_name" example:"Manchester"`
// AwayTeamName string `json:"away_team_name" example:"Liverpool"`
// MarketName string `json:"market_name" example:"Fulltime Result"`
// Odd float32 `json:"odd" example:"1.5"`
// OddName string `json:"odd_name" example:"1"`
// Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
}
type CreateTicketReq struct {
Outcomes []CreateTicketOutcomeReq `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
}
type CreateTicketRes struct {
FastCode int64 `json:"fast_code" example:"1234"`
CreatedNumber int64 `json:"created_number" example:"3"`
}
type TicketRes struct {
ID int64 `json:"id" example:"1"`
Outcomes []domain.TicketOutcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
}
// 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
// @Param createTicket body domain.CreateTicketReq true "Creates ticket"
// @Success 200 {object} domain.CreateTicketRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /ticket [post]
func (h *Handler) CreateTicket(c *fiber.Ctx) error {
var req CreateTicketReq
var req domain.CreateTicketReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse CreateTicket request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
@ -60,122 +31,17 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
// TODO Validate Outcomes Here and make sure they didn't expire
// Validation for creating tickets
if len(req.Outcomes) > 30 {
return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil)
}
if req.Amount > 100000 {
return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with an amount above 100,000 birr", nil, nil)
}
clientIP := c.IP()
count, err := h.ticketSvc.CountTicketByIP(c.Context(), clientIP)
if err != nil {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching user info", nil, nil)
}
if count > 50 {
return response.WriteJSON(c, fiber.StatusBadRequest, "Ticket Limit reached", nil, nil)
}
var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes))
var totalOdds float32 = 1
for _, outcome := range req.Outcomes {
eventIDStr := strconv.FormatInt(outcome.EventID, 10)
marketIDStr := strconv.FormatInt(outcome.MarketID, 10)
oddIDStr := strconv.FormatInt(outcome.OddID, 10)
event, err := h.eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr)
if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil)
}
// Checking to make sure the event hasn't already started
currentTime := time.Now()
if event.StartTime.Before(currentTime) {
return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil)
}
odds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr)
if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil)
}
type rawOddType struct {
ID string
Name string
Odds string
Header string
Handicap string
}
var selectedOdd rawOddType
var isOddFound bool = false
for _, raw := range odds.RawOdds {
var rawOdd rawOddType
rawBytes, err := json.Marshal(raw)
err = json.Unmarshal(rawBytes, &rawOdd)
if err != nil {
h.logger.Error("Failed to unmarshal raw odd:", "error", err)
continue
}
if rawOdd.ID == oddIDStr {
selectedOdd = rawOdd
isOddFound = true
}
}
if !isOddFound {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil)
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
totalOdds = totalOdds * float32(parsedOdd)
outcomes = append(outcomes, domain.CreateTicketOutcome{
EventID: outcome.EventID,
OddID: outcome.OddID,
MarketID: outcome.MarketID,
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: event.StartTime,
})
}
totalWinnings := req.Amount * totalOdds
if totalWinnings > 1000000 {
return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with 1,000,000 winnings", nil, nil)
}
ticket, err := h.ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
IP: clientIP,
})
if err != nil {
h.logger.Error("CreateTicketReq failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
// Add the ticket id now that it has fetched from the database
for index := range outcomes {
outcomes[index].TicketID = ticket.ID
}
rows, err := h.ticketSvc.CreateTicketOutcome(c.Context(), outcomes)
newTicket, rows, err := h.ticketSvc.CreateTicket(c.Context(), req, c.IP())
if err != nil {
h.logger.Error("CreateTicketReq failed to create outcomes", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
switch err {
case ticket.ErrEventHasBeenRemoved, ticket.ErrEventHasNotEnded, ticket.ErrRawOddInvalid:
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
res := CreateTicketRes{
FastCode: ticket.ID,
res := domain.CreateTicketRes{
FastCode: newTicket.ID,
CreatedNumber: rows,
}
return response.WriteJSON(c, fiber.StatusOK, "Ticket Created", res, nil)
@ -189,7 +55,7 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param id path int true "Ticket ID"
// @Success 200 {object} TicketRes
// @Success 200 {object} domain.TicketRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /ticket/{id} [get]
@ -207,7 +73,7 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve ticket")
}
res := TicketRes{
res := domain.TicketRes{
ID: ticket.ID,
Outcomes: ticket.Outcomes,
Amount: ticket.Amount.Float32(),
@ -222,7 +88,7 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error {
// @Tags ticket
// @Accept json
// @Produce json
// @Success 200 {array} TicketRes
// @Success 200 {array} domain.TicketRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /ticket [get]
@ -234,9 +100,9 @@ func (h *Handler) GetAllTickets(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve tickets")
}
res := make([]TicketRes, len(tickets))
res := make([]domain.TicketRes, len(tickets))
for i, ticket := range tickets {
res[i] = TicketRes{
res[i] = domain.TicketRes{
ID: ticket.ID,
Outcomes: ticket.Outcomes,
Amount: ticket.Amount.Float32(),