Merge branch 'main' into ticket-bet
This commit is contained in:
commit
1e49afc5ee
|
|
@ -40,6 +40,28 @@ CREATE TABLE virtual_game_transactions (
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE virtual_game_histories (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
session_id VARCHAR(100), -- nullable
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
wallet_id BIGINT, -- nullable
|
||||||
|
game_id BIGINT, -- nullable
|
||||||
|
transaction_type VARCHAR(20) NOT NULL, -- e.g., BET, WIN, CANCEL
|
||||||
|
amount BIGINT NOT NULL, -- in cents or smallest currency unit
|
||||||
|
currency VARCHAR(10) NOT NULL,
|
||||||
|
external_transaction_id VARCHAR(100) NOT NULL,
|
||||||
|
reference_transaction_id VARCHAR(100), -- nullable, for cancel/refund
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'COMPLETED', -- transaction status
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Optional: Indexes for performance
|
||||||
|
CREATE INDEX idx_virtual_game_user_id ON virtual_game_histories(user_id);
|
||||||
|
CREATE INDEX idx_virtual_game_transaction_type ON virtual_game_histories(transaction_type);
|
||||||
|
CREATE INDEX idx_virtual_game_game_id ON virtual_game_histories(game_id);
|
||||||
|
CREATE INDEX idx_virtual_game_external_transaction_id ON virtual_game_histories(external_transaction_id);
|
||||||
|
|
||||||
CREATE INDEX idx_virtual_game_sessions_user_id ON virtual_game_sessions(user_id);
|
CREATE INDEX idx_virtual_game_sessions_user_id ON virtual_game_sessions(user_id);
|
||||||
CREATE INDEX idx_virtual_game_transactions_session_id ON virtual_game_transactions(session_id);
|
CREATE INDEX idx_virtual_game_transactions_session_id ON virtual_game_transactions(session_id);
|
||||||
CREATE INDEX idx_virtual_game_transactions_user_id ON virtual_game_transactions(user_id);
|
CREATE INDEX idx_virtual_game_transactions_user_id ON virtual_game_transactions(user_id);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,36 @@ INSERT INTO virtual_game_transactions (
|
||||||
$1, $2, $3, $4, $5, $6, $7, $8
|
$1, $2, $3, $4, $5, $6, $7, $8
|
||||||
) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at;
|
) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at;
|
||||||
|
|
||||||
|
-- name: CreateVirtualGameHistory :one
|
||||||
|
INSERT INTO virtual_game_histories (
|
||||||
|
session_id,
|
||||||
|
user_id,
|
||||||
|
wallet_id,
|
||||||
|
game_id,
|
||||||
|
transaction_type,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
external_transaction_id,
|
||||||
|
reference_transaction_id,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
|
||||||
|
) RETURNING
|
||||||
|
id,
|
||||||
|
session_id,
|
||||||
|
user_id,
|
||||||
|
wallet_id,
|
||||||
|
game_id,
|
||||||
|
transaction_type,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
external_transaction_id,
|
||||||
|
reference_transaction_id,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at;
|
||||||
|
|
||||||
|
|
||||||
-- name: GetVirtualGameTransactionByExternalID :one
|
-- name: GetVirtualGameTransactionByExternalID :one
|
||||||
SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
|
SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
|
||||||
FROM virtual_game_transactions
|
FROM virtual_game_transactions
|
||||||
|
|
|
||||||
130
docs/docs.go
130
docs/docs.go
|
|
@ -2887,6 +2887,85 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/popok/games": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieves the list of available PopOK slot games",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Virtual Games - PopOK"
|
||||||
|
],
|
||||||
|
"summary": "Get PopOK Games List",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"default": "USD",
|
||||||
|
"description": "Currency (e.g. USD, ETB)",
|
||||||
|
"name": "currency",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.PopOKGame"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"502": {
|
||||||
|
"description": "Bad Gateway",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/popok/games/recommend": {
|
||||||
|
"get": {
|
||||||
|
"description": "Recommends games based on user history or randomly",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Virtual Games - PopOK"
|
||||||
|
],
|
||||||
|
"summary": "Recommend virtual games",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "User ID",
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.GameRecommendation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/random/bet": {
|
"/random/bet": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Generate a random bet",
|
"description": "Generate a random bet",
|
||||||
|
|
@ -4314,7 +4393,7 @@ const docTemplate = `{
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"virtual-game"
|
"Virtual Games - PopOK"
|
||||||
],
|
],
|
||||||
"summary": "Handle PopOK game callback",
|
"summary": "Handle PopOK game callback",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
|
@ -4365,7 +4444,7 @@ const docTemplate = `{
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"virtual-game"
|
"Virtual Games - PopOK"
|
||||||
],
|
],
|
||||||
"summary": "Launch a PopOK virtual game",
|
"summary": "Launch a PopOK virtual game",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
|
@ -5057,6 +5136,30 @@ const docTemplate = `{
|
||||||
"STATUS_REMOVED"
|
"STATUS_REMOVED"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"domain.GameRecommendation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"bets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"game_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"game_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"description": "e.g., \"Based on your activity\", \"Popular\", \"Random pick\"",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.League": {
|
"domain.League": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -5232,6 +5335,29 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.PopOKGame": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"bets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gameName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.RandomBetReq": {
|
"domain.RandomBetReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -2879,6 +2879,85 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/popok/games": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieves the list of available PopOK slot games",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Virtual Games - PopOK"
|
||||||
|
],
|
||||||
|
"summary": "Get PopOK Games List",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"default": "USD",
|
||||||
|
"description": "Currency (e.g. USD, ETB)",
|
||||||
|
"name": "currency",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.PopOKGame"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"502": {
|
||||||
|
"description": "Bad Gateway",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/popok/games/recommend": {
|
||||||
|
"get": {
|
||||||
|
"description": "Recommends games based on user history or randomly",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Virtual Games - PopOK"
|
||||||
|
],
|
||||||
|
"summary": "Recommend virtual games",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "User ID",
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "query",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.GameRecommendation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/random/bet": {
|
"/random/bet": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Generate a random bet",
|
"description": "Generate a random bet",
|
||||||
|
|
@ -4306,7 +4385,7 @@
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"virtual-game"
|
"Virtual Games - PopOK"
|
||||||
],
|
],
|
||||||
"summary": "Handle PopOK game callback",
|
"summary": "Handle PopOK game callback",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
|
@ -4357,7 +4436,7 @@
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"virtual-game"
|
"Virtual Games - PopOK"
|
||||||
],
|
],
|
||||||
"summary": "Launch a PopOK virtual game",
|
"summary": "Launch a PopOK virtual game",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
|
@ -5049,6 +5128,30 @@
|
||||||
"STATUS_REMOVED"
|
"STATUS_REMOVED"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"domain.GameRecommendation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"bets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"game_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"game_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"description": "e.g., \"Based on your activity\", \"Popular\", \"Random pick\"",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.League": {
|
"domain.League": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -5224,6 +5327,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.PopOKGame": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"bets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gameName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.RandomBetReq": {
|
"domain.RandomBetReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,22 @@ definitions:
|
||||||
- STATUS_SUSPENDED
|
- STATUS_SUSPENDED
|
||||||
- STATUS_DECIDED_BY_FA
|
- STATUS_DECIDED_BY_FA
|
||||||
- STATUS_REMOVED
|
- STATUS_REMOVED
|
||||||
|
domain.GameRecommendation:
|
||||||
|
properties:
|
||||||
|
bets:
|
||||||
|
items:
|
||||||
|
type: number
|
||||||
|
type: array
|
||||||
|
game_id:
|
||||||
|
type: integer
|
||||||
|
game_name:
|
||||||
|
type: string
|
||||||
|
reason:
|
||||||
|
description: e.g., "Based on your activity", "Popular", "Random pick"
|
||||||
|
type: string
|
||||||
|
thumbnail:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.League:
|
domain.League:
|
||||||
properties:
|
properties:
|
||||||
bet365_id:
|
bet365_id:
|
||||||
|
|
@ -482,6 +498,21 @@ definitions:
|
||||||
description: BET, WIN, REFUND, JACKPOT_WIN
|
description: BET, WIN, REFUND, JACKPOT_WIN
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
domain.PopOKGame:
|
||||||
|
properties:
|
||||||
|
bets:
|
||||||
|
items:
|
||||||
|
type: number
|
||||||
|
type: array
|
||||||
|
gameName:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
thumbnail:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.RandomBetReq:
|
domain.RandomBetReq:
|
||||||
properties:
|
properties:
|
||||||
branch_id:
|
branch_id:
|
||||||
|
|
@ -3444,6 +3475,58 @@ paths:
|
||||||
summary: Create a operation
|
summary: Create a operation
|
||||||
tags:
|
tags:
|
||||||
- branch
|
- branch
|
||||||
|
/popok/games:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Retrieves the list of available PopOK slot games
|
||||||
|
parameters:
|
||||||
|
- default: USD
|
||||||
|
description: Currency (e.g. USD, ETB)
|
||||||
|
in: query
|
||||||
|
name: currency
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/domain.PopOKGame'
|
||||||
|
type: array
|
||||||
|
"502":
|
||||||
|
description: Bad Gateway
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Get PopOK Games List
|
||||||
|
tags:
|
||||||
|
- Virtual Games - PopOK
|
||||||
|
/popok/games/recommend:
|
||||||
|
get:
|
||||||
|
description: Recommends games based on user history or randomly
|
||||||
|
parameters:
|
||||||
|
- description: User ID
|
||||||
|
in: query
|
||||||
|
name: user_id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/domain.GameRecommendation'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Recommend virtual games
|
||||||
|
tags:
|
||||||
|
- Virtual Games - PopOK
|
||||||
/random/bet:
|
/random/bet:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -4397,7 +4480,7 @@ paths:
|
||||||
$ref: '#/definitions/response.APIResponse'
|
$ref: '#/definitions/response.APIResponse'
|
||||||
summary: Handle PopOK game callback
|
summary: Handle PopOK game callback
|
||||||
tags:
|
tags:
|
||||||
- virtual-game
|
- Virtual Games - PopOK
|
||||||
/virtual-game/launch:
|
/virtual-game/launch:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -4433,7 +4516,7 @@ paths:
|
||||||
- Bearer: []
|
- Bearer: []
|
||||||
summary: Launch a PopOK virtual game
|
summary: Launch a PopOK virtual game
|
||||||
tags:
|
tags:
|
||||||
- virtual-game
|
- Virtual Games - PopOK
|
||||||
/wallet:
|
/wallet:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
|
|
@ -454,6 +454,22 @@ type VirtualGame struct {
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VirtualGameHistory struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
SessionID pgtype.Text `json:"session_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
WalletID pgtype.Int8 `json:"wallet_id"`
|
||||||
|
GameID pgtype.Int8 `json:"game_id"`
|
||||||
|
TransactionType string `json:"transaction_type"`
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
ExternalTransactionID string `json:"external_transaction_id"`
|
||||||
|
ReferenceTransactionID pgtype.Text `json:"reference_transaction_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type VirtualGameSession struct {
|
type VirtualGameSession struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,81 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CreateVirtualGameHistory = `-- name: CreateVirtualGameHistory :one
|
||||||
|
INSERT INTO virtual_game_histories (
|
||||||
|
session_id,
|
||||||
|
user_id,
|
||||||
|
wallet_id,
|
||||||
|
game_id,
|
||||||
|
transaction_type,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
external_transaction_id,
|
||||||
|
reference_transaction_id,
|
||||||
|
status
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
|
||||||
|
) RETURNING
|
||||||
|
id,
|
||||||
|
session_id,
|
||||||
|
user_id,
|
||||||
|
wallet_id,
|
||||||
|
game_id,
|
||||||
|
transaction_type,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
external_transaction_id,
|
||||||
|
reference_transaction_id,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateVirtualGameHistoryParams struct {
|
||||||
|
SessionID pgtype.Text `json:"session_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
WalletID pgtype.Int8 `json:"wallet_id"`
|
||||||
|
GameID pgtype.Int8 `json:"game_id"`
|
||||||
|
TransactionType string `json:"transaction_type"`
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
ExternalTransactionID string `json:"external_transaction_id"`
|
||||||
|
ReferenceTransactionID pgtype.Text `json:"reference_transaction_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtualGameHistoryParams) (VirtualGameHistory, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateVirtualGameHistory,
|
||||||
|
arg.SessionID,
|
||||||
|
arg.UserID,
|
||||||
|
arg.WalletID,
|
||||||
|
arg.GameID,
|
||||||
|
arg.TransactionType,
|
||||||
|
arg.Amount,
|
||||||
|
arg.Currency,
|
||||||
|
arg.ExternalTransactionID,
|
||||||
|
arg.ReferenceTransactionID,
|
||||||
|
arg.Status,
|
||||||
|
)
|
||||||
|
var i VirtualGameHistory
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SessionID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.WalletID,
|
||||||
|
&i.GameID,
|
||||||
|
&i.TransactionType,
|
||||||
|
&i.Amount,
|
||||||
|
&i.Currency,
|
||||||
|
&i.ExternalTransactionID,
|
||||||
|
&i.ReferenceTransactionID,
|
||||||
|
&i.Status,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one
|
const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one
|
||||||
INSERT INTO virtual_game_sessions (
|
INSERT INTO virtual_game_sessions (
|
||||||
user_id, game_id, session_token, currency, status, expires_at
|
user_id, game_id, session_token, currency, status, expires_at
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,22 @@ type VirtualGameSession struct {
|
||||||
GameMode string `json:"game_mode"` // real, demo, tournament
|
GameMode string `json:"game_mode"` // real, demo, tournament
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VirtualGameHistory struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
SessionID string `json:"session_id,omitempty"` // Optional, if session tracking is used
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
WalletID *int64 `json:"wallet_id,omitempty"` // Optional if wallet detail is needed
|
||||||
|
GameID *int64 `json:"game_id,omitempty"` // Optional for game-level analysis
|
||||||
|
TransactionType string `json:"transaction_type"` // BET, WIN, CANCEL, etc.
|
||||||
|
Amount int64 `json:"amount"` // Stored in minor units (e.g. cents)
|
||||||
|
Currency string `json:"currency"` // e.g., ETB, USD
|
||||||
|
ExternalTransactionID string `json:"external_transaction_id"` // Provider transaction ID
|
||||||
|
ReferenceTransactionID string `json:"reference_transaction_id,omitempty"` // For CANCELs pointing to BETs
|
||||||
|
Status string `json:"status"` // COMPLETED, CANCELLED, FAILED, etc.
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type VirtualGameTransaction struct {
|
type VirtualGameTransaction struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
SessionID int64 `json:"session_id"`
|
SessionID int64 `json:"session_id"`
|
||||||
|
|
@ -191,3 +207,27 @@ type GameSpecificData struct {
|
||||||
RiskLevel string `json:"risk_level,omitempty"` // For Mines
|
RiskLevel string `json:"risk_level,omitempty"` // For Mines
|
||||||
BucketIndex int `json:"bucket_index,omitempty"` // For Plinko
|
BucketIndex int `json:"bucket_index,omitempty"` // For Plinko
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PopOKGame struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
GameName string `json:"gameName"`
|
||||||
|
Bets []float64 `json:"bets"`
|
||||||
|
Thumbnail string `json:"thumbnail"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PopOKGameListResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data struct {
|
||||||
|
Slots []PopOKGame `json:"slots"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameRecommendation struct {
|
||||||
|
GameID int `json:"game_id"`
|
||||||
|
GameName string `json:"game_name"`
|
||||||
|
Thumbnail string `json:"thumbnail"`
|
||||||
|
Bets []float64 `json:"bets"`
|
||||||
|
Reason string `json:"reason"` // e.g., "Based on your activity", "Popular", "Random pick"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ type VirtualGameRepository interface {
|
||||||
// WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error
|
// WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error
|
||||||
|
|
||||||
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
||||||
|
GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error)
|
||||||
|
CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type VirtualGameRepo struct {
|
type VirtualGameRepo struct {
|
||||||
|
|
@ -92,6 +94,21 @@ func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *VirtualGameRepo) CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error {
|
||||||
|
params := dbgen.CreateVirtualGameHistoryParams{
|
||||||
|
SessionID: pgtype.Text{String: his.SessionID, Valid: true},
|
||||||
|
UserID: his.UserID,
|
||||||
|
WalletID: pgtype.Int8{Int64: *his.WalletID, Valid: true},
|
||||||
|
TransactionType: his.TransactionType,
|
||||||
|
Amount: his.Amount,
|
||||||
|
Currency: his.Currency,
|
||||||
|
ExternalTransactionID: his.ExternalTransactionID,
|
||||||
|
Status: his.Status,
|
||||||
|
}
|
||||||
|
_, err := r.store.queries.CreateVirtualGameHistory(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *VirtualGameRepo) GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) {
|
func (r *VirtualGameRepo) GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) {
|
||||||
dbTx, err := r.store.queries.GetVirtualGameTransactionByExternalID(ctx, externalID)
|
dbTx, err := r.store.queries.GetVirtualGameTransactionByExternalID(ctx, externalID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -153,6 +170,24 @@ func (r *VirtualGameRepo) GetGameCounts(ctx context.Context, filter domain.Repor
|
||||||
return total, active, inactive, nil
|
return total, active, inactive, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *VirtualGameRepo) GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error) {
|
||||||
|
query := `SELECT game_id FROM virtual_game_histories WHERE user_id = $1 AND transaction_type = 'BET' ORDER BY created_at DESC LIMIT 100`
|
||||||
|
rows, err := r.store.conn.Query(ctx, query, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var history []domain.VirtualGameHistory
|
||||||
|
for rows.Next() {
|
||||||
|
var tx domain.VirtualGameHistory
|
||||||
|
if err := rows.Scan(&tx.GameID); err == nil {
|
||||||
|
history = append(history, tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return history, nil
|
||||||
|
}
|
||||||
|
|
||||||
// func (r *VirtualGameRepo) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
|
// func (r *VirtualGameRepo) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||||
// _, tx, err := r.store.BeginTx(ctx)
|
// _, tx, err := r.store.BeginTx(ctx)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,6 @@ type VirtualGameService interface {
|
||||||
ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error)
|
ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error)
|
||||||
|
|
||||||
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
|
||||||
|
ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error)
|
||||||
|
RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package virtualgameservice
|
package virtualgameservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
|
@ -8,7 +9,12 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"math/rand/v2"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
|
|
@ -43,14 +49,14 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionToken := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano())
|
sessionId := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano())
|
||||||
token, err := jwtutil.CreatePopOKJwt(
|
token, err := jwtutil.CreatePopOKJwt(
|
||||||
userID,
|
userID,
|
||||||
user.PhoneNumber,
|
user.PhoneNumber,
|
||||||
currency,
|
currency,
|
||||||
"en",
|
"en",
|
||||||
mode,
|
mode,
|
||||||
sessionToken,
|
sessionId,
|
||||||
s.config.PopOK.SecretKey,
|
s.config.PopOK.SecretKey,
|
||||||
24*time.Hour,
|
24*time.Hour,
|
||||||
)
|
)
|
||||||
|
|
@ -59,19 +65,31 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record game launch as a transaction (for history and recommendation purposes)
|
||||||
|
tx := &domain.VirtualGameHistory{
|
||||||
|
SessionID: sessionId, // Optional: populate if session tracking is implemented
|
||||||
|
UserID: userID,
|
||||||
|
GameID: toInt64Ptr(gameID),
|
||||||
|
TransactionType: "LAUNCH",
|
||||||
|
Amount: 0,
|
||||||
|
Currency: currency,
|
||||||
|
ExternalTransactionID: sessionId,
|
||||||
|
Status: "COMPLETED",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.CreateVirtualGameHistory(ctx, tx); err != nil {
|
||||||
|
s.logger.Error("Failed to record game launch transaction", "error", err)
|
||||||
|
// Do not fail game launch on logging error — just log and continue
|
||||||
|
}
|
||||||
|
|
||||||
params := fmt.Sprintf(
|
params := fmt.Sprintf(
|
||||||
"partnerId=%s&gameId=%s&gameMode=%s&lang=en&platform=%s&externalToken=%s",
|
"partnerId=%s&gameId=%s&gameMode=%s&lang=en&platform=%s&externalToken=%s",
|
||||||
s.config.PopOK.ClientID, gameID, mode, s.config.PopOK.Platform, token,
|
s.config.PopOK.ClientID, gameID, mode, s.config.PopOK.Platform, token,
|
||||||
)
|
)
|
||||||
|
|
||||||
// params = fmt.Sprintf(
|
|
||||||
// "partnerId=%s&gameId=%sgameMode=%s&lang=en&platform=%s",
|
|
||||||
// "1", "1", "fun", "111",
|
|
||||||
// )
|
|
||||||
|
|
||||||
// signature := s.generateSignature(params)
|
|
||||||
return fmt.Sprintf("%s?%s", s.config.PopOK.BaseURL, params), nil
|
return fmt.Sprintf("%s?%s", s.config.PopOK.BaseURL, params), nil
|
||||||
// return fmt.Sprintf("%s?%s", s.config.PopOK.BaseURL, params), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error {
|
func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error {
|
||||||
|
|
@ -148,10 +166,10 @@ func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCall
|
||||||
|
|
||||||
func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error) {
|
func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfoRequest) (*domain.PopOKPlayerInfoResponse, error) {
|
||||||
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
|
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// s.logger.Error("Failed to parse JWT", "error", err)
|
s.logger.Error("Failed to parse JWT", "error", err)
|
||||||
// return nil, fmt.Errorf("invalid token")
|
return nil, fmt.Errorf("invalid token")
|
||||||
// }
|
}
|
||||||
|
|
||||||
wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
|
wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
|
||||||
if err != nil || len(wallets) == 0 {
|
if err != nil || len(wallets) == 0 {
|
||||||
|
|
@ -170,9 +188,9 @@ func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfo
|
||||||
func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (*domain.PopOKBetResponse, error) {
|
func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (*domain.PopOKBetResponse, error) {
|
||||||
// Validate token and get user ID
|
// Validate token and get user ID
|
||||||
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
|
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// return nil, fmt.Errorf("invalid token")
|
return nil, fmt.Errorf("invalid token")
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Convert amount to cents (assuming wallet uses cents)
|
// Convert amount to cents (assuming wallet uses cents)
|
||||||
amountCents := int64(req.Amount * 100)
|
amountCents := int64(req.Amount * 100)
|
||||||
|
|
@ -399,3 +417,126 @@ func (s *service) verifySignature(callback *domain.PopOKCallback) bool {
|
||||||
func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
func (s *service) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
||||||
return s.repo.GetGameCounts(ctx, filter)
|
return s.repo.GetGameCounts(ctx, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) {
|
||||||
|
now := time.Now().Format("02-01-2006 15:04:05") // dd-mm-yyyy hh:mm:ss
|
||||||
|
|
||||||
|
// Calculate hash: sha256(privateKey + time)
|
||||||
|
rawHash := s.config.PopOK.SecretKey + now
|
||||||
|
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(rawHash)))
|
||||||
|
|
||||||
|
// Construct request payload
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"action": "gameList",
|
||||||
|
"platform": s.config.PopOK.Platform,
|
||||||
|
"partnerId": s.config.PopOK.ClientID,
|
||||||
|
"currency": currency,
|
||||||
|
"time": now,
|
||||||
|
"hash": hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to marshal game list request", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", s.config.PopOK.BaseURL+"/serviceApi.php", bytes.NewReader(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to create game list request", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to send game list request", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("PopOK game list failed with status %d: %s", resp.StatusCode, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
var gameList domain.PopOKGameListResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&gameList); err != nil {
|
||||||
|
s.logger.Error("Failed to decode game list response", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if gameList.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("PopOK error: %s", gameList.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gameList.Data.Slots, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) {
|
||||||
|
// Fetch all available games
|
||||||
|
games, err := s.ListGames(ctx, "ETB") // currency can be dynamic
|
||||||
|
if err != nil || len(games) == 0 {
|
||||||
|
return nil, fmt.Errorf("could not fetch games")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has existing interaction
|
||||||
|
history, err := s.repo.GetUserGameHistory(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("No previous game history", "userID", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendations := []domain.GameRecommendation{}
|
||||||
|
|
||||||
|
if len(history) > 0 {
|
||||||
|
// Score games based on interaction frequency
|
||||||
|
gameScores := map[int64]int{}
|
||||||
|
for _, h := range history {
|
||||||
|
if h.GameID != nil {
|
||||||
|
gameScores[*h.GameID]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by score descending
|
||||||
|
sort.SliceStable(games, func(i, j int) bool {
|
||||||
|
return gameScores[int64(games[i].ID)] > gameScores[int64(games[j].ID)]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Pick top 3
|
||||||
|
for _, g := range games[:min(3, len(games))] {
|
||||||
|
recommendations = append(recommendations, domain.GameRecommendation{
|
||||||
|
GameID: g.ID,
|
||||||
|
GameName: g.GameName,
|
||||||
|
Thumbnail: g.Thumbnail,
|
||||||
|
Bets: g.Bets,
|
||||||
|
Reason: "Based on your activity",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pick 3 random games for new users
|
||||||
|
rand.Shuffle(len(games), func(i, j int) {
|
||||||
|
games[i], games[j] = games[j], games[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, g := range games[:min(3, len(games))] {
|
||||||
|
recommendations = append(recommendations, domain.GameRecommendation{
|
||||||
|
GameID: g.ID,
|
||||||
|
GameName: g.GameName,
|
||||||
|
Thumbnail: g.Thumbnail,
|
||||||
|
Bets: g.Bets,
|
||||||
|
Reason: "Random pick",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommendations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInt64Ptr(s string) *int64 {
|
||||||
|
id, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &id
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ type launchVirtualGameRes struct {
|
||||||
// LaunchVirtualGame godoc
|
// LaunchVirtualGame godoc
|
||||||
// @Summary Launch a PopOK virtual game
|
// @Summary Launch a PopOK virtual game
|
||||||
// @Description Generates a URL to launch a PopOK game
|
// @Description Generates a URL to launch a PopOK game
|
||||||
// @Tags virtual-game
|
// @Tags Virtual Games - PopOK
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
|
|
@ -60,7 +60,7 @@ func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error {
|
||||||
// HandleVirtualGameCallback godoc
|
// HandleVirtualGameCallback godoc
|
||||||
// @Summary Handle PopOK game callback
|
// @Summary Handle PopOK game callback
|
||||||
// @Description Processes callbacks from PopOK for game events
|
// @Description Processes callbacks from PopOK for game events
|
||||||
// @Tags virtual-game
|
// @Tags Virtual Games - PopOK
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param callback body domain.PopOKCallback true "Callback data"
|
// @Param callback body domain.PopOKCallback true "Callback data"
|
||||||
|
|
@ -155,3 +155,47 @@ func (h *Handler) HandleCancel(c *fiber.Ctx) error {
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Cancel processed", resp, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "Cancel processed", resp, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGameList godoc
|
||||||
|
// @Summary Get PopOK Games List
|
||||||
|
// @Description Retrieves the list of available PopOK slot games
|
||||||
|
// @Tags Virtual Games - PopOK
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param currency query string false "Currency (e.g. USD, ETB)" default(USD)
|
||||||
|
// @Success 200 {array} domain.PopOKGame
|
||||||
|
// @Failure 502 {object} domain.ErrorResponse
|
||||||
|
// @Router /popok/games [get]
|
||||||
|
func (h *Handler) GetGameList(c *fiber.Ctx) error {
|
||||||
|
currency := c.Query("currency", "ETB") // fallback default
|
||||||
|
|
||||||
|
games, err := h.virtualGameSvc.ListGames(c.Context(), currency)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadGateway, "failed to fetch games")
|
||||||
|
}
|
||||||
|
return c.JSON(games)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecommendGames godoc
|
||||||
|
// @Summary Recommend virtual games
|
||||||
|
// @Description Recommends games based on user history or randomly
|
||||||
|
// @Tags Virtual Games - PopOK
|
||||||
|
// @Produce json
|
||||||
|
// @Param user_id query int true "User ID"
|
||||||
|
// @Success 200 {array} domain.GameRecommendation
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /popok/games/recommend [get]
|
||||||
|
func (h *Handler) RecommendGames(c *fiber.Ctx) error {
|
||||||
|
userIDVal := c.Locals("user_id")
|
||||||
|
userID, ok := userIDVal.(int64)
|
||||||
|
if !ok || userID == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "invalid user ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendations, err := h.virtualGameSvc.RecommendGames(c.Context(), userID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to recommend games")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(recommendations)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ func CreateJwt(userId int64, Role domain.Role, CompanyID domain.ValidInt64, key
|
||||||
func CreatePopOKJwt(userID int64, username, currency, lang, mode, sessionID, key string, expiry time.Duration) (string, error) {
|
func CreatePopOKJwt(userID int64, username, currency, lang, mode, sessionID, key string, expiry time.Duration) (string, error) {
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, PopOKClaim{
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, PopOKClaim{
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
Issuer: "fortune-bet",
|
Issuer: "github.com/lafetz/snippitstash",
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
Audience: jwt.ClaimStrings{"popokgaming.com"},
|
Audience: jwt.ClaimStrings{"popokgaming.com"},
|
||||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,8 @@ func (a *App) initAppRoutes() {
|
||||||
a.fiber.Post("/bet", h.HandleBet)
|
a.fiber.Post("/bet", h.HandleBet)
|
||||||
a.fiber.Post("/win", h.HandleWin)
|
a.fiber.Post("/win", h.HandleWin)
|
||||||
a.fiber.Post("/cancel", h.HandleCancel)
|
a.fiber.Post("/cancel", h.HandleCancel)
|
||||||
|
a.fiber.Get("/popok/games", h.GetGameList)
|
||||||
|
a.fiber.Get("/popok/games/recommend", a.authMiddleware, h.RecommendGames)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user