adding result

This commit is contained in:
Samuel Tariku 2025-04-25 15:01:30 +03:00
parent fcd926223a
commit 14de6859b3
25 changed files with 1499 additions and 76 deletions

View File

@ -21,6 +21,7 @@ import (
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
@ -69,7 +70,7 @@ func main() {
eventSvc := event.New(cfg.Bet365Token, store)
oddsSvc := odds.New(cfg.Bet365Token, store)
resultSvc := result.NewService(cfg.Bet365Token,store)
ticketSvc := ticket.NewService(store)
betSvc := bet.NewService(store)
walletSvc := wallet.NewService(store, store)
@ -85,7 +86,7 @@ func main() {
referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger)
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc)
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc)
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey,

View File

@ -240,6 +240,7 @@ CREATE TABLE odds (
UNIQUE (event_id, market_id, name, handicap),
UNIQUE (event_id, market_id)
);
CREATE TABLE companies (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
@ -386,3 +387,12 @@ VALUES (
CURRENT_TIMESTAMP
);
--------------------------------------------------Bet365 Data Fetching + Event Managment------------------------------------------------
CREATE TABLE results (
id SERIAL PRIMARY KEY,
event_id TEXT UNIQUE,
full_time_score TEXT,
half_time_score TEXT,
ss TEXT,
scores JSONB,
fetched_at TIMESTAMPTZ DEFAULT now()
);

View File

@ -205,3 +205,10 @@ WHERE id = $1
AND is_live = false
AND status = 'upcoming'
LIMIT 1;
-- name: UpdateMatchResult :exec
UPDATE events
SET score = $1,
status = $2,
fetched_at = NOW()
WHERE id = $3;

8
db/query/result.sql Normal file
View File

@ -0,0 +1,8 @@
-- name: InsertResult :one
INSERT INTO results (event_id, full_time_score, half_time_score, ss, scores)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, event_id, full_time_score, half_time_score, ss, scores, fetched_at;
-- name: GetResultByEventID :one
SELECT * FROM results WHERE event_id = $1;

View File

@ -1803,6 +1803,147 @@ const docTemplate = `{
}
}
},
"/referral/settings": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Retrieves current referral settings (admin only)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"referral"
],
"summary": "Get referral settings",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.ReferralSettings"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"put": {
"security": [
{
"Bearer": []
}
],
"description": "Updates referral settings (admin only)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"referral"
],
"summary": "Update referral settings",
"parameters": [
{
"description": "Referral settings",
"name": "settings",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ReferralSettings"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/referral/stats": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Retrieves referral statistics for the authenticated user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"referral"
],
"summary": "Get referral statistics",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.ReferralStats"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/search/branch": {
"get": {
"description": "Search branches by name or location",
@ -2184,7 +2325,7 @@ const docTemplate = `{
}
},
"patch": {
"description": "Updates the cashed out field",
"description": "Updates the verified status of a transaction",
"consumes": [
"application/json"
],
@ -2194,7 +2335,7 @@ const docTemplate = `{
"tags": [
"transaction"
],
"summary": "Updates the cashed out field",
"summary": "Updates the verified field of a transaction",
"parameters": [
{
"type": "integer",
@ -2205,7 +2346,7 @@ const docTemplate = `{
},
{
"description": "Updates Transaction Verification",
"name": "updateCashOut",
"name": "updateVerified",
"in": "body",
"required": true,
"schema": {
@ -2738,6 +2879,109 @@ const docTemplate = `{
}
}
},
"/virtual-game/callback": {
"post": {
"description": "Processes callbacks from PopOK for game events",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"virtual-game"
],
"summary": "Handle PopOK game callback",
"parameters": [
{
"description": "Callback data",
"name": "callback",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.PopOKCallback"
}
}
],
"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"
}
}
}
}
},
"/virtual-game/launch": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Generates a URL to launch a PopOK game",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"virtual-game"
],
"summary": "Launch a PopOK virtual game",
"parameters": [
{
"description": "Game launch details",
"name": "launch",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.launchVirtualGameReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.launchVirtualGameRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/wallet": {
"get": {
"description": "Retrieve all wallets",
@ -3019,6 +3263,34 @@ const docTemplate = `{
"BANK"
]
},
"domain.PopOKCallback": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"currency": {
"type": "string"
},
"session_id": {
"type": "string"
},
"signature": {
"description": "HMAC-SHA256 signature for verification",
"type": "string"
},
"timestamp": {
"type": "integer"
},
"transaction_id": {
"type": "string"
},
"type": {
"description": "BET, WIN, REFUND, JACKPOT_WIN",
"type": "string"
}
}
},
"domain.RawOddsByMarketID": {
"type": "object",
"properties": {
@ -3040,6 +3312,58 @@ const docTemplate = `{
}
}
},
"domain.ReferralSettings": {
"type": "object",
"properties": {
"betReferralBonusPercentage": {
"type": "number"
},
"cashbackPercentage": {
"type": "number"
},
"createdAt": {
"type": "string"
},
"expiresAfterDays": {
"type": "integer"
},
"id": {
"type": "integer"
},
"maxReferrals": {
"type": "integer"
},
"referralRewardAmount": {
"type": "number"
},
"updatedAt": {
"type": "string"
},
"updatedBy": {
"type": "string"
},
"version": {
"type": "integer"
}
}
},
"domain.ReferralStats": {
"type": "object",
"properties": {
"completedReferrals": {
"type": "integer"
},
"pendingRewards": {
"type": "number"
},
"totalReferrals": {
"type": "integer"
},
"totalRewardEarned": {
"type": "number"
}
}
},
"domain.Role": {
"type": "string",
"enum": [
@ -3374,7 +3698,6 @@ const docTemplate = `{
"type": "object",
"properties": {
"event_id": {
"description": "BetID int64 ` + "`" + `json:\"bet_id\" example:\"1\"` + "`" + `",
"type": "integer",
"example": 1
},
@ -3952,17 +4275,25 @@ const docTemplate = `{
},
"handlers.UpdateTransactionVerifiedReq": {
"type": "object",
"required": [
"verified"
],
"properties": {
"verified": {
"type": "boolean"
"type": "boolean",
"example": true
}
}
},
"handlers.UpdateWalletActiveReq": {
"type": "object",
"required": [
"is_active"
],
"properties": {
"isActive": {
"type": "boolean"
"is_active": {
"type": "boolean",
"example": true
}
}
},
@ -4046,8 +4377,45 @@ const docTemplate = `{
}
}
},
"handlers.launchVirtualGameReq": {
"type": "object",
"required": [
"currency",
"game_id",
"mode"
],
"properties": {
"currency": {
"type": "string",
"example": "USD"
},
"game_id": {
"type": "string",
"example": "crash_001"
},
"mode": {
"type": "string",
"enum": [
"REAL",
"DEMO"
],
"example": "REAL"
}
}
},
"handlers.launchVirtualGameRes": {
"type": "object",
"properties": {
"launch_url": {
"type": "string"
}
}
},
"handlers.loginCustomerReq": {
"type": "object",
"required": [
"password"
],
"properties": {
"email": {
"type": "string",
@ -4079,20 +4447,30 @@ const docTemplate = `{
},
"handlers.logoutReq": {
"type": "object",
"required": [
"refresh_token"
],
"properties": {
"refresh_token": {
"type": "string"
"type": "string",
"example": "\u003crefresh-token\u003e"
}
}
},
"handlers.refreshToken": {
"type": "object",
"required": [
"access_token",
"refresh_token"
],
"properties": {
"access_token": {
"type": "string"
"type": "string",
"example": "\u003cjwt-token\u003e"
},
"refresh_token": {
"type": "string"
"type": "string",
"example": "\u003crefresh-token\u003e"
}
}
},

View File

@ -1795,6 +1795,147 @@
}
}
},
"/referral/settings": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Retrieves current referral settings (admin only)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"referral"
],
"summary": "Get referral settings",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.ReferralSettings"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"put": {
"security": [
{
"Bearer": []
}
],
"description": "Updates referral settings (admin only)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"referral"
],
"summary": "Update referral settings",
"parameters": [
{
"description": "Referral settings",
"name": "settings",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ReferralSettings"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/referral/stats": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Retrieves referral statistics for the authenticated user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"referral"
],
"summary": "Get referral statistics",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.ReferralStats"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/search/branch": {
"get": {
"description": "Search branches by name or location",
@ -2176,7 +2317,7 @@
}
},
"patch": {
"description": "Updates the cashed out field",
"description": "Updates the verified status of a transaction",
"consumes": [
"application/json"
],
@ -2186,7 +2327,7 @@
"tags": [
"transaction"
],
"summary": "Updates the cashed out field",
"summary": "Updates the verified field of a transaction",
"parameters": [
{
"type": "integer",
@ -2197,7 +2338,7 @@
},
{
"description": "Updates Transaction Verification",
"name": "updateCashOut",
"name": "updateVerified",
"in": "body",
"required": true,
"schema": {
@ -2730,6 +2871,109 @@
}
}
},
"/virtual-game/callback": {
"post": {
"description": "Processes callbacks from PopOK for game events",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"virtual-game"
],
"summary": "Handle PopOK game callback",
"parameters": [
{
"description": "Callback data",
"name": "callback",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.PopOKCallback"
}
}
],
"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"
}
}
}
}
},
"/virtual-game/launch": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Generates a URL to launch a PopOK game",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"virtual-game"
],
"summary": "Launch a PopOK virtual game",
"parameters": [
{
"description": "Game launch details",
"name": "launch",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.launchVirtualGameReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.launchVirtualGameRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/wallet": {
"get": {
"description": "Retrieve all wallets",
@ -3011,6 +3255,34 @@
"BANK"
]
},
"domain.PopOKCallback": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"currency": {
"type": "string"
},
"session_id": {
"type": "string"
},
"signature": {
"description": "HMAC-SHA256 signature for verification",
"type": "string"
},
"timestamp": {
"type": "integer"
},
"transaction_id": {
"type": "string"
},
"type": {
"description": "BET, WIN, REFUND, JACKPOT_WIN",
"type": "string"
}
}
},
"domain.RawOddsByMarketID": {
"type": "object",
"properties": {
@ -3032,6 +3304,58 @@
}
}
},
"domain.ReferralSettings": {
"type": "object",
"properties": {
"betReferralBonusPercentage": {
"type": "number"
},
"cashbackPercentage": {
"type": "number"
},
"createdAt": {
"type": "string"
},
"expiresAfterDays": {
"type": "integer"
},
"id": {
"type": "integer"
},
"maxReferrals": {
"type": "integer"
},
"referralRewardAmount": {
"type": "number"
},
"updatedAt": {
"type": "string"
},
"updatedBy": {
"type": "string"
},
"version": {
"type": "integer"
}
}
},
"domain.ReferralStats": {
"type": "object",
"properties": {
"completedReferrals": {
"type": "integer"
},
"pendingRewards": {
"type": "number"
},
"totalReferrals": {
"type": "integer"
},
"totalRewardEarned": {
"type": "number"
}
}
},
"domain.Role": {
"type": "string",
"enum": [
@ -3366,7 +3690,6 @@
"type": "object",
"properties": {
"event_id": {
"description": "BetID int64 `json:\"bet_id\" example:\"1\"`",
"type": "integer",
"example": 1
},
@ -3944,17 +4267,25 @@
},
"handlers.UpdateTransactionVerifiedReq": {
"type": "object",
"required": [
"verified"
],
"properties": {
"verified": {
"type": "boolean"
"type": "boolean",
"example": true
}
}
},
"handlers.UpdateWalletActiveReq": {
"type": "object",
"required": [
"is_active"
],
"properties": {
"isActive": {
"type": "boolean"
"is_active": {
"type": "boolean",
"example": true
}
}
},
@ -4038,8 +4369,45 @@
}
}
},
"handlers.launchVirtualGameReq": {
"type": "object",
"required": [
"currency",
"game_id",
"mode"
],
"properties": {
"currency": {
"type": "string",
"example": "USD"
},
"game_id": {
"type": "string",
"example": "crash_001"
},
"mode": {
"type": "string",
"enum": [
"REAL",
"DEMO"
],
"example": "REAL"
}
}
},
"handlers.launchVirtualGameRes": {
"type": "object",
"properties": {
"launch_url": {
"type": "string"
}
}
},
"handlers.loginCustomerReq": {
"type": "object",
"required": [
"password"
],
"properties": {
"email": {
"type": "string",
@ -4071,20 +4439,30 @@
},
"handlers.logoutReq": {
"type": "object",
"required": [
"refresh_token"
],
"properties": {
"refresh_token": {
"type": "string"
"type": "string",
"example": "\u003crefresh-token\u003e"
}
}
},
"handlers.refreshToken": {
"type": "object",
"required": [
"access_token",
"refresh_token"
],
"properties": {
"access_token": {
"type": "string"
"type": "string",
"example": "\u003cjwt-token\u003e"
},
"refresh_token": {
"type": "string"
"type": "string",
"example": "\u003crefresh-token\u003e"
}
}
},

View File

@ -103,6 +103,25 @@ definitions:
- TELEBIRR_TRANSACTION
- ARIFPAY_TRANSACTION
- BANK
domain.PopOKCallback:
properties:
amount:
type: number
currency:
type: string
session_id:
type: string
signature:
description: HMAC-SHA256 signature for verification
type: string
timestamp:
type: integer
transaction_id:
type: string
type:
description: BET, WIN, REFUND, JACKPOT_WIN
type: string
type: object
domain.RawOddsByMarketID:
properties:
fetched_at:
@ -117,6 +136,40 @@ definitions:
items: {}
type: array
type: object
domain.ReferralSettings:
properties:
betReferralBonusPercentage:
type: number
cashbackPercentage:
type: number
createdAt:
type: string
expiresAfterDays:
type: integer
id:
type: integer
maxReferrals:
type: integer
referralRewardAmount:
type: number
updatedAt:
type: string
updatedBy:
type: string
version:
type: integer
type: object
domain.ReferralStats:
properties:
completedReferrals:
type: integer
pendingRewards:
type: number
totalReferrals:
type: integer
totalRewardEarned:
type: number
type: object
domain.Role:
enum:
- super_admin
@ -357,7 +410,6 @@ definitions:
handlers.CreateBetOutcomeReq:
properties:
event_id:
description: BetID int64 `json:"bet_id" example:"1"`
example: 1
type: integer
market_id:
@ -762,12 +814,18 @@ definitions:
handlers.UpdateTransactionVerifiedReq:
properties:
verified:
example: true
type: boolean
required:
- verified
type: object
handlers.UpdateWalletActiveReq:
properties:
isActive:
is_active:
example: true
type: boolean
required:
- is_active
type: object
handlers.UserProfileRes:
properties:
@ -824,6 +882,30 @@ definitions:
example: 1
type: integer
type: object
handlers.launchVirtualGameReq:
properties:
currency:
example: USD
type: string
game_id:
example: crash_001
type: string
mode:
enum:
- REAL
- DEMO
example: REAL
type: string
required:
- currency
- game_id
- mode
type: object
handlers.launchVirtualGameRes:
properties:
launch_url:
type: string
type: object
handlers.loginCustomerReq:
properties:
email:
@ -835,6 +917,8 @@ definitions:
phone_number:
example: "1234567890"
type: string
required:
- password
type: object
handlers.loginCustomerRes:
properties:
@ -848,14 +932,22 @@ definitions:
handlers.logoutReq:
properties:
refresh_token:
example: <refresh-token>
type: string
required:
- refresh_token
type: object
handlers.refreshToken:
properties:
access_token:
example: <jwt-token>
type: string
refresh_token:
example: <refresh-token>
type: string
required:
- access_token
- refresh_token
type: object
handlers.updateUserReq:
properties:
@ -2079,6 +2171,95 @@ paths:
summary: Retrieve raw odds by Market ID
tags:
- prematch
/referral/settings:
get:
consumes:
- application/json
description: Retrieves current referral settings (admin only)
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.ReferralSettings'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- Bearer: []
summary: Get referral settings
tags:
- referral
put:
consumes:
- application/json
description: Updates referral settings (admin only)
parameters:
- description: Referral settings
in: body
name: settings
required: true
schema:
$ref: '#/definitions/domain.ReferralSettings'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- Bearer: []
summary: Update referral settings
tags:
- referral
/referral/stats:
get:
consumes:
- application/json
description: Retrieves referral statistics for the authenticated user
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.ReferralStats'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- Bearer: []
summary: Get referral statistics
tags:
- referral
/search/branch:
get:
consumes:
@ -2333,7 +2514,7 @@ paths:
patch:
consumes:
- application/json
description: Updates the cashed out field
description: Updates the verified status of a transaction
parameters:
- description: Transaction ID
in: path
@ -2342,7 +2523,7 @@ paths:
type: integer
- description: Updates Transaction Verification
in: body
name: updateCashOut
name: updateVerified
required: true
schema:
$ref: '#/definitions/handlers.UpdateTransactionVerifiedReq'
@ -2361,7 +2542,7 @@ paths:
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Updates the cashed out field
summary: Updates the verified field of a transaction
tags:
- transaction
/transfer/refill/:id:
@ -2690,6 +2871,72 @@ paths:
summary: Get customer wallet
tags:
- wallet
/virtual-game/callback:
post:
consumes:
- application/json
description: Processes callbacks from PopOK for game events
parameters:
- description: Callback data
in: body
name: callback
required: true
schema:
$ref: '#/definitions/domain.PopOKCallback'
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: Handle PopOK game callback
tags:
- virtual-game
/virtual-game/launch:
post:
consumes:
- application/json
description: Generates a URL to launch a PopOK game
parameters:
- description: Game launch details
in: body
name: launch
required: true
schema:
$ref: '#/definitions/handlers.launchVirtualGameReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.launchVirtualGameRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- Bearer: []
summary: Launch a PopOK virtual game
tags:
- virtual-game
/wallet:
get:
consumes:

View File

@ -514,3 +514,22 @@ func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) {
}
return items, nil
}
const UpdateMatchResult = `-- name: UpdateMatchResult :exec
UPDATE events
SET score = $1,
status = $2,
fetched_at = NOW()
WHERE id = $3
`
type UpdateMatchResultParams struct {
Score pgtype.Text `json:"score"`
Status pgtype.Text `json:"status"`
ID string `json:"id"`
}
func (q *Queries) UpdateMatchResult(ctx context.Context, arg UpdateMatchResultParams) error {
_, err := q.db.Exec(ctx, UpdateMatchResult, arg.Score, arg.Status, arg.ID)
return err
}

View File

@ -269,6 +269,16 @@ type RefreshToken struct {
Revoked bool `json:"revoked"`
}
type Result struct {
ID int32 `json:"id"`
EventID pgtype.Text `json:"event_id"`
FullTimeScore pgtype.Text `json:"full_time_score"`
HalfTimeScore pgtype.Text `json:"half_time_score"`
Ss pgtype.Text `json:"ss"`
Scores []byte `json:"scores"`
FetchedAt pgtype.Timestamptz `json:"fetched_at"`
}
type SupportedOperation struct {
ID int64 `json:"id"`
Name string `json:"name"`

66
gen/db/result.sql.go Normal file
View File

@ -0,0 +1,66 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// source: result.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const GetResultByEventID = `-- name: GetResultByEventID :one
SELECT id, event_id, full_time_score, half_time_score, ss, scores, fetched_at FROM results WHERE event_id = $1
`
func (q *Queries) GetResultByEventID(ctx context.Context, eventID pgtype.Text) (Result, error) {
row := q.db.QueryRow(ctx, GetResultByEventID, eventID)
var i Result
err := row.Scan(
&i.ID,
&i.EventID,
&i.FullTimeScore,
&i.HalfTimeScore,
&i.Ss,
&i.Scores,
&i.FetchedAt,
)
return i, err
}
const InsertResult = `-- name: InsertResult :one
INSERT INTO results (event_id, full_time_score, half_time_score, ss, scores)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, event_id, full_time_score, half_time_score, ss, scores, fetched_at
`
type InsertResultParams struct {
EventID pgtype.Text `json:"event_id"`
FullTimeScore pgtype.Text `json:"full_time_score"`
HalfTimeScore pgtype.Text `json:"half_time_score"`
Ss pgtype.Text `json:"ss"`
Scores []byte `json:"scores"`
}
func (q *Queries) InsertResult(ctx context.Context, arg InsertResultParams) (Result, error) {
row := q.db.QueryRow(ctx, InsertResult,
arg.EventID,
arg.FullTimeScore,
arg.HalfTimeScore,
arg.Ss,
arg.Scores,
)
var i Result
err := row.Scan(
&i.ID,
&i.EventID,
&i.FullTimeScore,
&i.HalfTimeScore,
&i.Ss,
&i.Scores,
&i.FetchedAt,
)
return i, err
}

2
go.mod
View File

@ -3,6 +3,7 @@ module github.com/SamuelTariku/FortuneBet-Backend
go 1.24.1
require (
github.com/amanuelabay/afrosms-go v1.0.6
github.com/bytedance/sonic v1.13.2
github.com/go-playground/validator/v10 v10.26.0
github.com/gofiber/fiber/v2 v2.52.6
@ -19,7 +20,6 @@ require (
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/amanuelabay/afrosms-go v1.0.6 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect

View File

@ -39,3 +39,28 @@ type UpcomingEvent struct {
LeagueCC string // League country code
StartTime time.Time // Converted from "time" field in UNIX format
}
type MatchResult struct {
EventID string
FullScore string
HalfScore string
Status string
Scores map[string]map[string]string
}
type Result struct {
EventID string
FullTimeScore string
HalfTimeScore string
SS string
Scores map[string]Score // "1": {"home": "0", "away": "1"}
}
type Score struct {
Home string `json:"home"`
Away string `json:"away"`
}
type Odds struct {
ID int64 `json:"id"`
EventID string `json:"event_id"`
MarketType string `json:"market_type"`
Name string `json:"name"`
HitStatus string `json:"hit_status"`
}

View File

@ -2,6 +2,7 @@ package repository
import (
"context"
"fmt"
"math"
"time"
@ -163,3 +164,17 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc
StartTime: event.StartTime.Time.UTC(),
}, nil
}
func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore, status string) error {
params := dbgen.UpdateMatchResultParams{
Score: pgtype.Text{String: fullScore, Valid: true},
Status: pgtype.Text{String: status, Valid: true},
ID: eventID,
}
err := s.queries.UpdateMatchResult(ctx, params)
if err != nil {
return fmt.Errorf("failed to update final score for event %s: %w", eventID, err)
}
return nil
}

View File

@ -0,0 +1,61 @@
package repository
import (
"context"
"encoding/json"
"fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Store) InsertResult(ctx context.Context, result domain.Result) error {
scoresJSON, err := json.Marshal(result.Scores)
if err != nil {
return fmt.Errorf("failed to marshal Scores: %w", err)
}
_, err = s.queries.InsertResult(ctx, dbgen.InsertResultParams{
EventID: pgtype.Text{String: result.EventID, Valid: true},
FullTimeScore: pgtype.Text{String: result.FullTimeScore, Valid: true},
HalfTimeScore: pgtype.Text{String: result.HalfTimeScore, Valid: true},
Ss: pgtype.Text{String: result.SS, Valid: true},
Scores: scoresJSON,
})
if err != nil {
return fmt.Errorf("failed to insert result: %w", err)
}
return nil
}
func (s *Store) GetResultByEventID(ctx context.Context, eventID string) (domain.Result, error) {
eventIDText := pgtype.Text{String: eventID, Valid: true}
result, err := s.queries.GetResultByEventID(ctx, eventIDText)
if err != nil {
return domain.Result{}, fmt.Errorf("failed to get result by event ID: %w", err)
}
var rawScores map[string]map[string]string
if err := json.Unmarshal(result.Scores, &rawScores); err != nil {
return domain.Result{}, fmt.Errorf("failed to unmarshal scores: %w", err)
}
scores := make(map[string]domain.Score)
for key, value := range rawScores {
scores[key] = domain.Score{
Home: value["home"],
Away: value["away"],
}
}
return domain.Result{
EventID: result.EventID.String,
FullTimeScore: result.FullTimeScore.String,
HalfTimeScore: result.HalfTimeScore.String,
SS: result.Ss.String,
Scores: scores,
}, nil
}

View File

@ -12,4 +12,6 @@ type Service interface {
GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error)
GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32, leagueID domain.ValidString, sportID domain.ValidString) ([]domain.UpcomingEvent, int64, error)
GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error)
// GetAndStoreMatchResult(ctx context.Context, eventID string) error
}

View File

@ -185,3 +185,39 @@ func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, o
func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {
return s.store.GetUpcomingEventByID(ctx, ID)
}
// func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error {
// url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID)
// resp, err := http.Get(url)
// if err != nil {
// return fmt.Errorf("failed to fetch result: %w", err)
// }
// defer resp.Body.Close()
// body, _ := io.ReadAll(resp.Body)
// // Parse the API response
// var apiResp struct {
// Results []struct {
// ID string `json:"id"`
// Ss string `json:"ss"` // Full-time score
// Status string `json:"time_status"`
// } `json:"results"`
// }
// err = json.Unmarshal(body, &apiResp)
// if err != nil || len(apiResp.Results) == 0 {
// return fmt.Errorf("invalid response or no results found")
// }
// result := apiResp.Results[0]
// err = s.store.UpdateFinalScore(ctx, result.ID, result.Ss, result.Status)
// if err != nil {
// return fmt.Errorf("failed to update final score in database: %w", err)
// }
// return nil
// }

View File

@ -0,0 +1,11 @@
package result
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Store interface {
InsertResult(ctx context.Context, result domain.Result) error
GetResultByEventID(ctx context.Context, eventID string) (domain.Result, error)
}

View File

@ -0,0 +1,68 @@
package result
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
)
type Service interface {
FetchAndStoreResult(ctx context.Context, eventID string) error
}
type service struct {
token string
store *repository.Store
}
func NewService(token string, store *repository.Store) Service {
return &service{
token: token,
store: store,
}
}
func (s *service) FetchAndStoreResult(ctx context.Context, eventID string) error {
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID)
res, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to fetch result: %w", err)
}
defer res.Body.Close()
var apiResp struct {
Results []struct {
SS string
Scores map[string]domain.Score `json:"scores"`
} `json:"results"`
}
if err := json.NewDecoder(res.Body).Decode(&apiResp); err != nil {
return fmt.Errorf("failed to decode result: %w", err)
}
if len(apiResp.Results) == 0 {
return fmt.Errorf("no result returned from API")
}
r := apiResp.Results[0]
halfScore := ""
if s1, ok := r.Scores["1"]; ok {
halfScore = fmt.Sprintf("%s-%s", s1.Home, s1.Away)
}
result := domain.Result{
EventID: eventID,
FullTimeScore: r.SS,
HalfTimeScore: halfScore,
SS: r.SS,
Scores: r.Scores,
}
return s.store.InsertResult(ctx, result)
}

View File

@ -1,24 +1,24 @@
package httpserver
import (
"fmt"
"context"
"log"
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/robfig/cron/v3"
)
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service) {
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service, resultService resultsvc.Service) {
c := cron.New(cron.WithSeconds())
schedule := []struct {
spec string
task func()
}{
// {
// spec: "0 0 * * * *", // Every hour
// spec: "*/5 * * * * *", // Every 5 seconds
// task: func() {
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
// log.Printf("FetchUpcomingEvents error: %v", err)
@ -34,7 +34,6 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
// }
// },
// },
// {
// spec: "0 */15 * * * *", // Every 15 minutes
// task: func() {
@ -43,11 +42,29 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
// }
// },
// },
{
spec: "*/10 * * * * *",
task: func() {
log.Println("🔄 Fetching results for upcoming events...")
upcomingEvents, err := eventService.GetAllUpcomingEvents(context.Background())
if err != nil {
log.Printf("❌ Failed to fetch upcoming events: %v", err)
return
}
for _, event := range upcomingEvents {
if err := resultService.FetchAndStoreResult(context.Background(), event.ID); err != nil {
log.Printf("❌ Failed to fetch/store result for event %s: %v", event.ID, err)
} else {
log.Printf("✅ Successfully stored result for event %s", event.ID)
}
}
},
},
}
for _, job := range schedule {
job.task()
fmt.Printf("here at")
if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
}

View File

@ -9,8 +9,19 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
// loginCustomerReq represents the request body for the LoginCustomer endpoint.
type loginCustomerReq struct {
Email string `json:"email" validate:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Password string `json:"password" validate:"required" example:"password123"`
}
// loginCustomerRes represents the response body for the LoginCustomer endpoint.
type loginCustomerRes struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
Role string `json:"role"`
}
// LoginCustomer godoc
// @Summary Login customer
// @Description Login customer
@ -71,6 +82,10 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil)
}
type refreshToken struct {
AccessToken string `json:"access_token" validate:"required" example:"<jwt-token>"`
RefreshToken string `json:"refresh_token" validate:"required" example:"<refresh-token>"`
}
// RefreshToken godoc
// @Summary Refresh token
@ -135,6 +150,9 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Refresh successful", res, nil)
}
type logoutReq struct {
RefreshToken string `json:"refresh_token" validate:"required" example:"<refresh-token>"`
}
// LogOutCustomer godoc
// @Summary Logout customer
// @Description Logout customer

View File

@ -190,3 +190,4 @@ func (h *Handler) GetPrematchOddsByUpcomingID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil)
}

View File

@ -232,6 +232,9 @@ func (h *Handler) GetTransactionByID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Transaction retrieved successfully", res, nil)
}
type UpdateTransactionVerifiedReq struct {
Verified bool `json:"verified" validate:"required" example:"true"`
}
// UpdateTransactionVerified godoc
// @Summary Updates the verified field of a transaction
// @Description Updates the verified status of a transaction

View File

@ -9,7 +9,14 @@ import (
"github.com/gofiber/fiber/v2"
)
type CheckPhoneEmailExistReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
type CheckPhoneEmailExistRes struct {
EmailExist bool `json:"email_exist"`
PhoneNumberExist bool `json:"phone_number_exist"`
}
// CheckPhoneEmailExist godoc
// @Summary Check if phone number or email exist
// @Description Check if phone number or email exist
@ -53,7 +60,10 @@ func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error {
}
return response.WriteJSON(c, fiber.StatusOK, "Check successful", res, nil)
}
type RegisterCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
// SendRegisterCode godoc
// @Summary Send register code
// @Description Send register code
@ -100,7 +110,18 @@ func (h *Handler) SendRegisterCode(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
}
type RegisterUserReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
Password string `json:"password" example:"password123"`
//Role string
Otp string `json:"otp" example:"123456"`
ReferalCode string `json:"referal_code" example:"ABC123"`
//
}
// RegisterUser godoc
// @Summary Register user
// @Description Register user
@ -192,6 +213,10 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Registration successful", nil, nil)
}
type ResetCodeReq struct {
Email string `json:"email" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
}
// SendResetCode godoc
// @Summary Send reset code
// @Description Send reset code
@ -238,7 +263,12 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil)
}
type ResetPasswordReq struct {
Email string
PhoneNumber string
Password string
Otp string
}
// ResetPassword godoc
// @Summary Reset password
// @Description Reset password

View File

@ -5,6 +5,15 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
type launchVirtualGameReq struct {
GameID string `json:"game_id" validate:"required" example:"crash_001"`
Currency string `json:"currency" validate:"required,len=3" example:"USD"`
Mode string `json:"mode" validate:"required,oneof=REAL DEMO" example:"REAL"`
}
type launchVirtualGameRes struct {
LaunchURL string `json:"launch_url"`
}
// LaunchVirtualGame godoc
// @Summary Launch a PopOK virtual game

View File

@ -9,6 +9,9 @@ import (
"github.com/gofiber/fiber/v2"
)
type UpdateWalletActiveReq struct {
IsActive bool `json:"is_active" validate:"required" example:"true"`
}
type WalletRes struct {
ID int64 `json:"id" example:"1"`
Balance float32 `json:"amount" example:"100.0"`