game list
This commit is contained in:
parent
22ec5d3ff8
commit
7c70b23a3d
|
|
@ -262,6 +262,7 @@ CREATE TABLE teams (
|
|||
bet365_id INT,
|
||||
logo_url TEXT
|
||||
);
|
||||
|
||||
-- Views
|
||||
CREATE VIEW companies_details AS
|
||||
SELECT companies.*,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
353
docs/docs.go
353
docs/docs.go
|
|
@ -811,76 +811,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/veli/launch/{game_id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Generates authenticated launch URL for Veli games",
|
||||
"tags": [
|
||||
"Veli Games"
|
||||
],
|
||||
"summary": "Launch a Veli game",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Game ID (e.g., veli_aviator_v1)",
|
||||
"name": "game_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"default": "USD",
|
||||
"description": "Currency code",
|
||||
"name": "currency",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"real",
|
||||
"demo"
|
||||
],
|
||||
"type": "string",
|
||||
"default": "real",
|
||||
"description": "Game mode",
|
||||
"name": "mode",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns launch URL",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"description": "Login customer",
|
||||
|
|
@ -2957,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",
|
||||
|
|
@ -4352,7 +4361,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"virtual-game"
|
||||
"Virtual Games - PopOK"
|
||||
],
|
||||
"summary": "Handle PopOK game callback",
|
||||
"parameters": [
|
||||
|
|
@ -4403,7 +4412,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"virtual-game"
|
||||
"Virtual Games - PopOK"
|
||||
],
|
||||
"summary": "Launch a PopOK virtual game",
|
||||
"parameters": [
|
||||
|
|
@ -4577,70 +4586,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/webhooks/veli": {
|
||||
"post": {
|
||||
"description": "Processes game round settlements from Veli",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Veli Games"
|
||||
],
|
||||
"summary": "Veli Games webhook handler",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Callback payload",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.VeliCallback"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Callback processed",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid payload",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid signature",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Processing error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
|
|
@ -5113,6 +5058,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": {
|
||||
|
|
@ -5193,6 +5162,17 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.OtpProvider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"twilio",
|
||||
"aformessage"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TwilioSms",
|
||||
"AfroMessage"
|
||||
]
|
||||
},
|
||||
"domain.OutcomeStatus": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
@ -5273,6 +5253,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": [
|
||||
|
|
@ -5551,51 +5554,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.VeliCallback": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"description": "Transaction amount",
|
||||
"type": "number"
|
||||
},
|
||||
"currency": {
|
||||
"description": "e.g., \"USD\"",
|
||||
"type": "string"
|
||||
},
|
||||
"event_type": {
|
||||
"description": "\"bet_placed\", \"game_result\", etc.",
|
||||
"type": "string"
|
||||
},
|
||||
"game_id": {
|
||||
"description": "e.g., \"veli_aviator_v1\"",
|
||||
"type": "string"
|
||||
},
|
||||
"multiplier": {
|
||||
"description": "For games with multipliers (Aviator/Plinko)",
|
||||
"type": "number"
|
||||
},
|
||||
"round_id": {
|
||||
"description": "Unique round identifier (replaces transaction_id)",
|
||||
"type": "string"
|
||||
},
|
||||
"session_id": {
|
||||
"description": "Matches VirtualGameSession.SessionToken",
|
||||
"type": "string"
|
||||
},
|
||||
"signature": {
|
||||
"description": "HMAC-SHA256",
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"description": "Unix timestamp",
|
||||
"type": "integer"
|
||||
},
|
||||
"user_id": {
|
||||
"description": "Veli's user identifier",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.VirtualGame": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -6249,6 +6207,9 @@ const docTemplate = `{
|
|||
},
|
||||
"handlers.RegisterCodeReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
|
|
@ -6257,11 +6218,22 @@ const docTemplate = `{
|
|||
"phone_number": {
|
||||
"type": "string",
|
||||
"example": "1234567890"
|
||||
},
|
||||
"provider": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.OtpProvider"
|
||||
}
|
||||
],
|
||||
"example": "twilio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.RegisterUserReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
|
|
@ -6287,6 +6259,14 @@ const docTemplate = `{
|
|||
"type": "string",
|
||||
"example": "1234567890"
|
||||
},
|
||||
"provider": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.OtpProvider"
|
||||
}
|
||||
],
|
||||
"example": "twilio"
|
||||
},
|
||||
"referal_code": {
|
||||
"type": "string",
|
||||
"example": "ABC123"
|
||||
|
|
@ -6295,6 +6275,9 @@ const docTemplate = `{
|
|||
},
|
||||
"handlers.ResetCodeReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
|
|
@ -6303,6 +6286,14 @@ const docTemplate = `{
|
|||
"phone_number": {
|
||||
"type": "string",
|
||||
"example": "1234567890"
|
||||
},
|
||||
"provider": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.OtpProvider"
|
||||
}
|
||||
],
|
||||
"example": "twilio"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -803,76 +803,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/veli/launch/{game_id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Generates authenticated launch URL for Veli games",
|
||||
"tags": [
|
||||
"Veli Games"
|
||||
],
|
||||
"summary": "Launch a Veli game",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Game ID (e.g., veli_aviator_v1)",
|
||||
"name": "game_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"default": "USD",
|
||||
"description": "Currency code",
|
||||
"name": "currency",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"enum": [
|
||||
"real",
|
||||
"demo"
|
||||
],
|
||||
"type": "string",
|
||||
"default": "real",
|
||||
"description": "Game mode",
|
||||
"name": "mode",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns launch URL",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"description": "Login customer",
|
||||
|
|
@ -2949,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",
|
||||
|
|
@ -4344,7 +4353,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"virtual-game"
|
||||
"Virtual Games - PopOK"
|
||||
],
|
||||
"summary": "Handle PopOK game callback",
|
||||
"parameters": [
|
||||
|
|
@ -4395,7 +4404,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"virtual-game"
|
||||
"Virtual Games - PopOK"
|
||||
],
|
||||
"summary": "Launch a PopOK virtual game",
|
||||
"parameters": [
|
||||
|
|
@ -4569,70 +4578,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/webhooks/veli": {
|
||||
"post": {
|
||||
"description": "Processes game round settlements from Veli",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Veli Games"
|
||||
],
|
||||
"summary": "Veli Games webhook handler",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Callback payload",
|
||||
"name": "payload",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.VeliCallback"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Callback processed",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid payload",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid signature",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Processing error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
|
|
@ -5105,6 +5050,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": {
|
||||
|
|
@ -5185,6 +5154,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.OtpProvider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"twilio",
|
||||
"aformessage"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TwilioSms",
|
||||
"AfroMessage"
|
||||
]
|
||||
},
|
||||
"domain.OutcomeStatus": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
@ -5265,6 +5245,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": [
|
||||
|
|
@ -5543,51 +5546,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.VeliCallback": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"description": "Transaction amount",
|
||||
"type": "number"
|
||||
},
|
||||
"currency": {
|
||||
"description": "e.g., \"USD\"",
|
||||
"type": "string"
|
||||
},
|
||||
"event_type": {
|
||||
"description": "\"bet_placed\", \"game_result\", etc.",
|
||||
"type": "string"
|
||||
},
|
||||
"game_id": {
|
||||
"description": "e.g., \"veli_aviator_v1\"",
|
||||
"type": "string"
|
||||
},
|
||||
"multiplier": {
|
||||
"description": "For games with multipliers (Aviator/Plinko)",
|
||||
"type": "number"
|
||||
},
|
||||
"round_id": {
|
||||
"description": "Unique round identifier (replaces transaction_id)",
|
||||
"type": "string"
|
||||
},
|
||||
"session_id": {
|
||||
"description": "Matches VirtualGameSession.SessionToken",
|
||||
"type": "string"
|
||||
},
|
||||
"signature": {
|
||||
"description": "HMAC-SHA256",
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"description": "Unix timestamp",
|
||||
"type": "integer"
|
||||
},
|
||||
"user_id": {
|
||||
"description": "Veli's user identifier",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.VirtualGame": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -6241,6 +6199,9 @@
|
|||
},
|
||||
"handlers.RegisterCodeReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
|
|
@ -6249,11 +6210,22 @@
|
|||
"phone_number": {
|
||||
"type": "string",
|
||||
"example": "1234567890"
|
||||
},
|
||||
"provider": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.OtpProvider"
|
||||
}
|
||||
],
|
||||
"example": "twilio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers.RegisterUserReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
|
|
@ -6279,6 +6251,14 @@
|
|||
"type": "string",
|
||||
"example": "1234567890"
|
||||
},
|
||||
"provider": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.OtpProvider"
|
||||
}
|
||||
],
|
||||
"example": "twilio"
|
||||
},
|
||||
"referal_code": {
|
||||
"type": "string",
|
||||
"example": "ABC123"
|
||||
|
|
@ -6287,6 +6267,9 @@
|
|||
},
|
||||
"handlers.ResetCodeReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
|
|
@ -6295,6 +6278,14 @@
|
|||
"phone_number": {
|
||||
"type": "string",
|
||||
"example": "1234567890"
|
||||
},
|
||||
"provider": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.OtpProvider"
|
||||
}
|
||||
],
|
||||
"example": "twilio"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -323,6 +323,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:
|
||||
|
|
@ -378,6 +394,14 @@ definitions:
|
|||
source:
|
||||
type: string
|
||||
type: object
|
||||
domain.OtpProvider:
|
||||
enum:
|
||||
- twilio
|
||||
- aformessage
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- TwilioSms
|
||||
- AfroMessage
|
||||
domain.OutcomeStatus:
|
||||
enum:
|
||||
- 0
|
||||
|
|
@ -439,6 +463,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:
|
||||
|
|
@ -632,39 +671,6 @@ definitions:
|
|||
- $ref: '#/definitions/domain.EventStatus'
|
||||
description: Match Status for event
|
||||
type: object
|
||||
domain.VeliCallback:
|
||||
properties:
|
||||
amount:
|
||||
description: Transaction amount
|
||||
type: number
|
||||
currency:
|
||||
description: e.g., "USD"
|
||||
type: string
|
||||
event_type:
|
||||
description: '"bet_placed", "game_result", etc.'
|
||||
type: string
|
||||
game_id:
|
||||
description: e.g., "veli_aviator_v1"
|
||||
type: string
|
||||
multiplier:
|
||||
description: For games with multipliers (Aviator/Plinko)
|
||||
type: number
|
||||
round_id:
|
||||
description: Unique round identifier (replaces transaction_id)
|
||||
type: string
|
||||
session_id:
|
||||
description: Matches VirtualGameSession.SessionToken
|
||||
type: string
|
||||
signature:
|
||||
description: HMAC-SHA256
|
||||
type: string
|
||||
timestamp:
|
||||
description: Unix timestamp
|
||||
type: integer
|
||||
user_id:
|
||||
description: Veli's user identifier
|
||||
type: string
|
||||
type: object
|
||||
domain.VirtualGame:
|
||||
properties:
|
||||
category:
|
||||
|
|
@ -1126,6 +1132,12 @@ definitions:
|
|||
phone_number:
|
||||
example: "1234567890"
|
||||
type: string
|
||||
provider:
|
||||
allOf:
|
||||
- $ref: '#/definitions/domain.OtpProvider'
|
||||
example: twilio
|
||||
required:
|
||||
- provider
|
||||
type: object
|
||||
handlers.RegisterUserReq:
|
||||
properties:
|
||||
|
|
@ -1147,9 +1159,15 @@ definitions:
|
|||
phone_number:
|
||||
example: "1234567890"
|
||||
type: string
|
||||
provider:
|
||||
allOf:
|
||||
- $ref: '#/definitions/domain.OtpProvider'
|
||||
example: twilio
|
||||
referal_code:
|
||||
example: ABC123
|
||||
type: string
|
||||
required:
|
||||
- provider
|
||||
type: object
|
||||
handlers.ResetCodeReq:
|
||||
properties:
|
||||
|
|
@ -1159,6 +1177,12 @@ definitions:
|
|||
phone_number:
|
||||
example: "1234567890"
|
||||
type: string
|
||||
provider:
|
||||
allOf:
|
||||
- $ref: '#/definitions/domain.OtpProvider'
|
||||
example: twilio
|
||||
required:
|
||||
- provider
|
||||
type: object
|
||||
handlers.ResetPasswordReq:
|
||||
properties:
|
||||
|
|
@ -2077,52 +2101,6 @@ paths:
|
|||
summary: Process Alea Play game callback
|
||||
tags:
|
||||
- Alea Virtual Games
|
||||
/api/veli/launch/{game_id}:
|
||||
get:
|
||||
description: Generates authenticated launch URL for Veli games
|
||||
parameters:
|
||||
- description: Game ID (e.g., veli_aviator_v1)
|
||||
in: path
|
||||
name: game_id
|
||||
required: true
|
||||
type: string
|
||||
- default: USD
|
||||
description: Currency code
|
||||
in: query
|
||||
name: currency
|
||||
type: string
|
||||
- default: real
|
||||
description: Game mode
|
||||
enum:
|
||||
- real
|
||||
- demo
|
||||
in: query
|
||||
name: mode
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Returns launch URL
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Launch a Veli game
|
||||
tags:
|
||||
- Veli Games
|
||||
/auth/login:
|
||||
post:
|
||||
consumes:
|
||||
|
|
@ -3494,6 +3472,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:
|
||||
|
|
@ -4426,7 +4456,7 @@ paths:
|
|||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Handle PopOK game callback
|
||||
tags:
|
||||
- virtual-game
|
||||
- Virtual Games - PopOK
|
||||
/virtual-game/launch:
|
||||
post:
|
||||
consumes:
|
||||
|
|
@ -4462,7 +4492,7 @@ paths:
|
|||
- Bearer: []
|
||||
summary: Launch a PopOK virtual game
|
||||
tags:
|
||||
- virtual-game
|
||||
- Virtual Games - PopOK
|
||||
/wallet:
|
||||
get:
|
||||
consumes:
|
||||
|
|
@ -4551,48 +4581,6 @@ paths:
|
|||
summary: Activate and Deactivate Wallet
|
||||
tags:
|
||||
- wallet
|
||||
/webhooks/veli:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Processes game round settlements from Veli
|
||||
parameters:
|
||||
- description: Callback payload
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.VeliCallback'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Callback processed
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid payload
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"403":
|
||||
description: Invalid signature
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Processing error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Veli Games webhook handler
|
||||
tags:
|
||||
- Veli Games
|
||||
securityDefinitions:
|
||||
Bearer:
|
||||
in: header
|
||||
|
|
|
|||
|
|
@ -445,6 +445,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()),
|
||||
|
|
|
|||
|
|
@ -270,6 +270,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