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
|
||||
);
|
||||
|
||||
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_transactions_session_id ON virtual_game_transactions(session_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
|
||||
) 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
|
||||
SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
|
||||
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": {
|
||||
"post": {
|
||||
"description": "Generate a random bet",
|
||||
|
|
@ -4314,7 +4393,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"virtual-game"
|
||||
"Virtual Games - PopOK"
|
||||
],
|
||||
"summary": "Handle PopOK game callback",
|
||||
"parameters": [
|
||||
|
|
@ -4365,7 +4444,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"virtual-game"
|
||||
"Virtual Games - PopOK"
|
||||
],
|
||||
"summary": "Launch a PopOK virtual game",
|
||||
"parameters": [
|
||||
|
|
@ -5057,6 +5136,30 @@ const docTemplate = `{
|
|||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"post": {
|
||||
"description": "Generate a random bet",
|
||||
|
|
@ -4306,7 +4385,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"virtual-game"
|
||||
"Virtual Games - PopOK"
|
||||
],
|
||||
"summary": "Handle PopOK game callback",
|
||||
"parameters": [
|
||||
|
|
@ -4357,7 +4436,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"virtual-game"
|
||||
"Virtual Games - PopOK"
|
||||
],
|
||||
"summary": "Launch a PopOK virtual game",
|
||||
"parameters": [
|
||||
|
|
@ -5049,6 +5128,30 @@
|
|||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
|
|||
|
|
@ -355,6 +355,22 @@ definitions:
|
|||
- STATUS_SUSPENDED
|
||||
- STATUS_DECIDED_BY_FA
|
||||
- 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:
|
||||
properties:
|
||||
bet365_id:
|
||||
|
|
@ -482,6 +498,21 @@ definitions:
|
|||
description: BET, WIN, REFUND, JACKPOT_WIN
|
||||
type: string
|
||||
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:
|
||||
properties:
|
||||
branch_id:
|
||||
|
|
@ -3444,6 +3475,58 @@ paths:
|
|||
summary: Create a operation
|
||||
tags:
|
||||
- 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:
|
||||
post:
|
||||
consumes:
|
||||
|
|
@ -4397,7 +4480,7 @@ paths:
|
|||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Handle PopOK game callback
|
||||
tags:
|
||||
- virtual-game
|
||||
- Virtual Games - PopOK
|
||||
/virtual-game/launch:
|
||||
post:
|
||||
consumes:
|
||||
|
|
@ -4433,7 +4516,7 @@ paths:
|
|||
- Bearer: []
|
||||
summary: Launch a PopOK virtual game
|
||||
tags:
|
||||
- virtual-game
|
||||
- Virtual Games - PopOK
|
||||
/wallet:
|
||||
get:
|
||||
consumes:
|
||||
|
|
|
|||
|
|
@ -454,6 +454,22 @@ type VirtualGame struct {
|
|||
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 {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
|
|
|
|||
|
|
@ -11,6 +11,81 @@ import (
|
|||
"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
|
||||
INSERT INTO virtual_game_sessions (
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
ID int64 `json:"id"`
|
||||
SessionID int64 `json:"session_id"`
|
||||
|
|
@ -191,3 +207,27 @@ type GameSpecificData struct {
|
|||
RiskLevel string `json:"risk_level,omitempty"` // For Mines
|
||||
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
|
||||
|
||||
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 {
|
||||
|
|
@ -92,6 +94,21 @@ func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *
|
|||
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) {
|
||||
dbTx, err := r.store.queries.GetVirtualGameTransactionByExternalID(ctx, externalID)
|
||||
if err != nil {
|
||||
|
|
@ -153,6 +170,24 @@ func (r *VirtualGameRepo) GetGameCounts(ctx context.Context, filter domain.Repor
|
|||
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 {
|
||||
// _, tx, err := r.store.BeginTx(ctx)
|
||||
// if err != nil {
|
||||
|
|
|
|||
|
|
@ -15,4 +15,6 @@ type VirtualGameService interface {
|
|||
ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
|
|
@ -8,7 +9,12 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
|
|
@ -43,14 +49,14 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
|
|||
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(
|
||||
userID,
|
||||
user.PhoneNumber,
|
||||
currency,
|
||||
"en",
|
||||
mode,
|
||||
sessionToken,
|
||||
sessionId,
|
||||
s.config.PopOK.SecretKey,
|
||||
24*time.Hour,
|
||||
)
|
||||
|
|
@ -59,19 +65,31 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
|
|||
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(
|
||||
"partnerId=%s&gameId=%s&gameMode=%s&lang=en&platform=%s&externalToken=%s",
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
|
||||
// if err != nil {
|
||||
// s.logger.Error("Failed to parse JWT", "error", err)
|
||||
// return nil, fmt.Errorf("invalid token")
|
||||
// }
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to parse JWT", "error", err)
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID)
|
||||
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) {
|
||||
// Validate token and get user ID
|
||||
claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("invalid token")
|
||||
// }
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// Convert amount to cents (assuming wallet uses cents)
|
||||
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) {
|
||||
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
|
||||
// @Summary Launch a PopOK virtual game
|
||||
// @Description Generates a URL to launch a PopOK game
|
||||
// @Tags virtual-game
|
||||
// @Tags Virtual Games - PopOK
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
|
|
@ -60,7 +60,7 @@ func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error {
|
|||
// HandleVirtualGameCallback godoc
|
||||
// @Summary Handle PopOK game callback
|
||||
// @Description Processes callbacks from PopOK for game events
|
||||
// @Tags virtual-game
|
||||
// @Tags Virtual Games - PopOK
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, PopOKClaim{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "fortune-bet",
|
||||
Issuer: "github.com/lafetz/snippitstash",
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Audience: jwt.ClaimStrings{"popokgaming.com"},
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
|
|
|
|||
|
|
@ -271,6 +271,8 @@ func (a *App) initAppRoutes() {
|
|||
a.fiber.Post("/bet", h.HandleBet)
|
||||
a.fiber.Post("/win", h.HandleWin)
|
||||
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