game list

This commit is contained in:
Yared Yemane 2025-06-19 15:02:28 +03:00
parent 22ec5d3ff8
commit 7c70b23a3d
15 changed files with 882 additions and 504 deletions

View File

@ -262,6 +262,7 @@ CREATE TABLE teams (
bet365_id INT, bet365_id INT,
logo_url TEXT logo_url TEXT
); );
-- Views -- Views
CREATE VIEW companies_details AS CREATE VIEW companies_details AS
SELECT companies.*, SELECT companies.*,

View File

@ -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);

View File

@ -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

View File

@ -811,76 +811,6 @@ const docTemplate = `{
} }
} }
}, },
"/api/veli/launch/{game_id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Generates authenticated launch URL for Veli games",
"tags": [
"Veli Games"
],
"summary": "Launch a Veli game",
"parameters": [
{
"type": "string",
"description": "Game ID (e.g., veli_aviator_v1)",
"name": "game_id",
"in": "path",
"required": true
},
{
"type": "string",
"default": "USD",
"description": "Currency code",
"name": "currency",
"in": "query"
},
{
"enum": [
"real",
"demo"
],
"type": "string",
"default": "real",
"description": "Game mode",
"name": "mode",
"in": "query"
}
],
"responses": {
"200": {
"description": "Returns launch URL",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/login": { "/auth/login": {
"post": { "post": {
"description": "Login customer", "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": { "/random/bet": {
"post": { "post": {
"description": "Generate a random bet", "description": "Generate a random bet",
@ -4352,7 +4361,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": [
@ -4403,7 +4412,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": [
@ -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": { "definitions": {
@ -5113,6 +5058,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": {
@ -5193,6 +5162,17 @@ const docTemplate = `{
} }
} }
}, },
"domain.OtpProvider": {
"type": "string",
"enum": [
"twilio",
"aformessage"
],
"x-enum-varnames": [
"TwilioSms",
"AfroMessage"
]
},
"domain.OutcomeStatus": { "domain.OutcomeStatus": {
"type": "integer", "type": "integer",
"enum": [ "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": { "domain.RandomBetReq": {
"type": "object", "type": "object",
"required": [ "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": { "domain.VirtualGame": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -6249,6 +6207,9 @@ const docTemplate = `{
}, },
"handlers.RegisterCodeReq": { "handlers.RegisterCodeReq": {
"type": "object", "type": "object",
"required": [
"provider"
],
"properties": { "properties": {
"email": { "email": {
"type": "string", "type": "string",
@ -6257,11 +6218,22 @@ const docTemplate = `{
"phone_number": { "phone_number": {
"type": "string", "type": "string",
"example": "1234567890" "example": "1234567890"
},
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
} }
} }
}, },
"handlers.RegisterUserReq": { "handlers.RegisterUserReq": {
"type": "object", "type": "object",
"required": [
"provider"
],
"properties": { "properties": {
"email": { "email": {
"type": "string", "type": "string",
@ -6287,6 +6259,14 @@ const docTemplate = `{
"type": "string", "type": "string",
"example": "1234567890" "example": "1234567890"
}, },
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
},
"referal_code": { "referal_code": {
"type": "string", "type": "string",
"example": "ABC123" "example": "ABC123"
@ -6295,6 +6275,9 @@ const docTemplate = `{
}, },
"handlers.ResetCodeReq": { "handlers.ResetCodeReq": {
"type": "object", "type": "object",
"required": [
"provider"
],
"properties": { "properties": {
"email": { "email": {
"type": "string", "type": "string",
@ -6303,6 +6286,14 @@ const docTemplate = `{
"phone_number": { "phone_number": {
"type": "string", "type": "string",
"example": "1234567890" "example": "1234567890"
},
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
} }
} }
}, },

View File

@ -803,76 +803,6 @@
} }
} }
}, },
"/api/veli/launch/{game_id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Generates authenticated launch URL for Veli games",
"tags": [
"Veli Games"
],
"summary": "Launch a Veli game",
"parameters": [
{
"type": "string",
"description": "Game ID (e.g., veli_aviator_v1)",
"name": "game_id",
"in": "path",
"required": true
},
{
"type": "string",
"default": "USD",
"description": "Currency code",
"name": "currency",
"in": "query"
},
{
"enum": [
"real",
"demo"
],
"type": "string",
"default": "real",
"description": "Game mode",
"name": "mode",
"in": "query"
}
],
"responses": {
"200": {
"description": "Returns launch URL",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/login": { "/auth/login": {
"post": { "post": {
"description": "Login customer", "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": { "/random/bet": {
"post": { "post": {
"description": "Generate a random bet", "description": "Generate a random bet",
@ -4344,7 +4353,7 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"virtual-game" "Virtual Games - PopOK"
], ],
"summary": "Handle PopOK game callback", "summary": "Handle PopOK game callback",
"parameters": [ "parameters": [
@ -4395,7 +4404,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": [
@ -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": { "definitions": {
@ -5105,6 +5050,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": {
@ -5185,6 +5154,17 @@
} }
} }
}, },
"domain.OtpProvider": {
"type": "string",
"enum": [
"twilio",
"aformessage"
],
"x-enum-varnames": [
"TwilioSms",
"AfroMessage"
]
},
"domain.OutcomeStatus": { "domain.OutcomeStatus": {
"type": "integer", "type": "integer",
"enum": [ "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": { "domain.RandomBetReq": {
"type": "object", "type": "object",
"required": [ "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": { "domain.VirtualGame": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -6241,6 +6199,9 @@
}, },
"handlers.RegisterCodeReq": { "handlers.RegisterCodeReq": {
"type": "object", "type": "object",
"required": [
"provider"
],
"properties": { "properties": {
"email": { "email": {
"type": "string", "type": "string",
@ -6249,11 +6210,22 @@
"phone_number": { "phone_number": {
"type": "string", "type": "string",
"example": "1234567890" "example": "1234567890"
},
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
} }
} }
}, },
"handlers.RegisterUserReq": { "handlers.RegisterUserReq": {
"type": "object", "type": "object",
"required": [
"provider"
],
"properties": { "properties": {
"email": { "email": {
"type": "string", "type": "string",
@ -6279,6 +6251,14 @@
"type": "string", "type": "string",
"example": "1234567890" "example": "1234567890"
}, },
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
},
"referal_code": { "referal_code": {
"type": "string", "type": "string",
"example": "ABC123" "example": "ABC123"
@ -6287,6 +6267,9 @@
}, },
"handlers.ResetCodeReq": { "handlers.ResetCodeReq": {
"type": "object", "type": "object",
"required": [
"provider"
],
"properties": { "properties": {
"email": { "email": {
"type": "string", "type": "string",
@ -6295,6 +6278,14 @@
"phone_number": { "phone_number": {
"type": "string", "type": "string",
"example": "1234567890" "example": "1234567890"
},
"provider": {
"allOf": [
{
"$ref": "#/definitions/domain.OtpProvider"
}
],
"example": "twilio"
} }
} }
}, },

View File

@ -323,6 +323,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:
@ -378,6 +394,14 @@ definitions:
source: source:
type: string type: string
type: object type: object
domain.OtpProvider:
enum:
- twilio
- aformessage
type: string
x-enum-varnames:
- TwilioSms
- AfroMessage
domain.OutcomeStatus: domain.OutcomeStatus:
enum: enum:
- 0 - 0
@ -439,6 +463,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:
@ -632,39 +671,6 @@ definitions:
- $ref: '#/definitions/domain.EventStatus' - $ref: '#/definitions/domain.EventStatus'
description: Match Status for event description: Match Status for event
type: object 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: domain.VirtualGame:
properties: properties:
category: category:
@ -1126,6 +1132,12 @@ definitions:
phone_number: phone_number:
example: "1234567890" example: "1234567890"
type: string type: string
provider:
allOf:
- $ref: '#/definitions/domain.OtpProvider'
example: twilio
required:
- provider
type: object type: object
handlers.RegisterUserReq: handlers.RegisterUserReq:
properties: properties:
@ -1147,9 +1159,15 @@ definitions:
phone_number: phone_number:
example: "1234567890" example: "1234567890"
type: string type: string
provider:
allOf:
- $ref: '#/definitions/domain.OtpProvider'
example: twilio
referal_code: referal_code:
example: ABC123 example: ABC123
type: string type: string
required:
- provider
type: object type: object
handlers.ResetCodeReq: handlers.ResetCodeReq:
properties: properties:
@ -1159,6 +1177,12 @@ definitions:
phone_number: phone_number:
example: "1234567890" example: "1234567890"
type: string type: string
provider:
allOf:
- $ref: '#/definitions/domain.OtpProvider'
example: twilio
required:
- provider
type: object type: object
handlers.ResetPasswordReq: handlers.ResetPasswordReq:
properties: properties:
@ -2077,52 +2101,6 @@ paths:
summary: Process Alea Play game callback summary: Process Alea Play game callback
tags: tags:
- Alea Virtual Games - 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: /auth/login:
post: post:
consumes: consumes:
@ -3494,6 +3472,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:
@ -4426,7 +4456,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:
@ -4462,7 +4492,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:
@ -4551,48 +4581,6 @@ paths:
summary: Activate and Deactivate Wallet summary: Activate and Deactivate Wallet
tags: tags:
- wallet - 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: securityDefinitions:
Bearer: Bearer:
in: header in: header

View File

@ -445,6 +445,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"`

View File

@ -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

View File

@ -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"
}

View File

@ -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 {

View File

@ -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)
} }

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()),

View File

@ -270,6 +270,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)
} }