feat: popok itegratio
This commit is contained in:
parent
8670fba6a4
commit
02e6f6ee6f
|
|
@ -125,6 +125,7 @@
|
||||||
│ ├── bet_handler.go
|
│ ├── bet_handler.go
|
||||||
│ ├── handlers.go
|
│ ├── handlers.go
|
||||||
│ ├── notification_handler.go
|
│ ├── notification_handler.go
|
||||||
|
│ ├── referal_handlers.go
|
||||||
│ ├── ticket_handler.go
|
│ ├── ticket_handler.go
|
||||||
│ ├── transaction_handler.go
|
│ ├── transaction_handler.go
|
||||||
│ ├── user.go
|
│ ├── user.go
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
||||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
|
|
@ -65,14 +66,16 @@ func main() {
|
||||||
|
|
||||||
notificationRepo := repository.NewNotificationRepository(store)
|
notificationRepo := repository.NewNotificationRepository(store)
|
||||||
referalRepo := repository.NewReferralRepository(store)
|
referalRepo := repository.NewReferralRepository(store)
|
||||||
|
vitualGameRepo := repository.NewVirtualGameRepository(store)
|
||||||
|
|
||||||
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
|
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
|
||||||
referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger)
|
referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger)
|
||||||
|
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
|
||||||
|
|
||||||
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
|
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
|
||||||
JwtAccessKey: cfg.JwtKey,
|
JwtAccessKey: cfg.JwtKey,
|
||||||
JwtAccessExpiry: cfg.AccessExpiry,
|
JwtAccessExpiry: cfg.AccessExpiry,
|
||||||
}, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, notificationSvc, referalSvc)
|
}, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, notificationSvc, referalSvc, virtualGameSvc)
|
||||||
logger.Info("Starting server", "port", cfg.Port)
|
logger.Info("Starting server", "port", cfg.Port)
|
||||||
|
|
||||||
if err := app.Run(); err != nil {
|
if err := app.Run(); err != nil {
|
||||||
|
|
|
||||||
3
db/migrations/000004_virtual_game_Sessios.down.sql
Normal file
3
db/migrations/000004_virtual_game_Sessios.down.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
DROP TABLE IF EXISTS virtual_game_transactions;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS virtual_game_sessions;
|
||||||
29
db/migrations/000004_virtual_game_Sessios.up.sql
Normal file
29
db/migrations/000004_virtual_game_Sessios.up.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
CREATE TABLE virtual_game_sessions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||||
|
game_id VARCHAR(50) NOT NULL,
|
||||||
|
session_token VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
currency VARCHAR(3) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, COMPLETED, FAILED
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE virtual_game_transactions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id),
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||||
|
wallet_id BIGINT NOT NULL REFERENCES wallets(id),
|
||||||
|
transaction_type VARCHAR(20) NOT NULL,
|
||||||
|
amount BIGINT NOT NULL,
|
||||||
|
currency VARCHAR(3) NOT NULL,
|
||||||
|
external_transaction_id VARCHAR(100) NOT NULL UNIQUE, -- PopOK transaction ID
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING, COMPLETED, FAILED
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
33
db/query/virtual_games.sql
Normal file
33
db/query/virtual_games.sql
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
-- name: CreateVirtualGameSession :one
|
||||||
|
INSERT INTO virtual_game_sessions (
|
||||||
|
user_id, game_id, session_token, currency, status, expires_at
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6
|
||||||
|
) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at;
|
||||||
|
|
||||||
|
-- name: GetVirtualGameSessionByToken :one
|
||||||
|
SELECT id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at
|
||||||
|
FROM virtual_game_sessions
|
||||||
|
WHERE session_token = $1;
|
||||||
|
|
||||||
|
-- name: UpdateVirtualGameSessionStatus :exec
|
||||||
|
UPDATE virtual_game_sessions
|
||||||
|
SET status = $2, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: CreateVirtualGameTransaction :one
|
||||||
|
INSERT INTO virtual_game_transactions (
|
||||||
|
session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status
|
||||||
|
) VALUES (
|
||||||
|
$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: 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
|
||||||
|
WHERE external_transaction_id = $1;
|
||||||
|
|
||||||
|
-- name: UpdateVirtualGameTransactionStatus :exec
|
||||||
|
UPDATE virtual_game_transactions
|
||||||
|
SET status = $2, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1;
|
||||||
|
|
@ -189,6 +189,32 @@ type User struct {
|
||||||
ReferredBy pgtype.Text
|
ReferredBy pgtype.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VirtualGameSession struct {
|
||||||
|
ID int64
|
||||||
|
UserID int64
|
||||||
|
GameID string
|
||||||
|
SessionToken string
|
||||||
|
Currency string
|
||||||
|
Status string
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
ExpiresAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualGameTransaction struct {
|
||||||
|
ID int64
|
||||||
|
SessionID int64
|
||||||
|
UserID int64
|
||||||
|
WalletID int64
|
||||||
|
TransactionType string
|
||||||
|
Amount int64
|
||||||
|
Currency string
|
||||||
|
ExternalTransactionID string
|
||||||
|
Status string
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
type Wallet struct {
|
type Wallet struct {
|
||||||
ID int64
|
ID int64
|
||||||
Balance int64
|
Balance int64
|
||||||
|
|
|
||||||
|
|
@ -184,8 +184,8 @@ WHERE referrer_id = $1
|
||||||
type GetReferralStatsRow struct {
|
type GetReferralStatsRow struct {
|
||||||
TotalReferrals int64
|
TotalReferrals int64
|
||||||
CompletedReferrals int64
|
CompletedReferrals int64
|
||||||
TotalRewardEarned float64
|
TotalRewardEarned interface{}
|
||||||
PendingRewards float64
|
PendingRewards interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetReferralStats(ctx context.Context, referrerID string) (GetReferralStatsRow, error) {
|
func (q *Queries) GetReferralStats(ctx context.Context, referrerID string) (GetReferralStatsRow, error) {
|
||||||
|
|
|
||||||
180
gen/db/virtual_games.sql.go
Normal file
180
gen/db/virtual_games.sql.go
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: virtual_games.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one
|
||||||
|
INSERT INTO virtual_game_sessions (
|
||||||
|
user_id, game_id, session_token, currency, status, expires_at
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6
|
||||||
|
) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateVirtualGameSessionParams struct {
|
||||||
|
UserID int64
|
||||||
|
GameID string
|
||||||
|
SessionToken string
|
||||||
|
Currency string
|
||||||
|
Status string
|
||||||
|
ExpiresAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtualGameSessionParams) (VirtualGameSession, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateVirtualGameSession,
|
||||||
|
arg.UserID,
|
||||||
|
arg.GameID,
|
||||||
|
arg.SessionToken,
|
||||||
|
arg.Currency,
|
||||||
|
arg.Status,
|
||||||
|
arg.ExpiresAt,
|
||||||
|
)
|
||||||
|
var i VirtualGameSession
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.GameID,
|
||||||
|
&i.SessionToken,
|
||||||
|
&i.Currency,
|
||||||
|
&i.Status,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateVirtualGameTransaction = `-- name: CreateVirtualGameTransaction :one
|
||||||
|
INSERT INTO virtual_game_transactions (
|
||||||
|
session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status
|
||||||
|
) VALUES (
|
||||||
|
$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
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateVirtualGameTransactionParams struct {
|
||||||
|
SessionID int64
|
||||||
|
UserID int64
|
||||||
|
WalletID int64
|
||||||
|
TransactionType string
|
||||||
|
Amount int64
|
||||||
|
Currency string
|
||||||
|
ExternalTransactionID string
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVirtualGameTransactionParams) (VirtualGameTransaction, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateVirtualGameTransaction,
|
||||||
|
arg.SessionID,
|
||||||
|
arg.UserID,
|
||||||
|
arg.WalletID,
|
||||||
|
arg.TransactionType,
|
||||||
|
arg.Amount,
|
||||||
|
arg.Currency,
|
||||||
|
arg.ExternalTransactionID,
|
||||||
|
arg.Status,
|
||||||
|
)
|
||||||
|
var i VirtualGameTransaction
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SessionID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.WalletID,
|
||||||
|
&i.TransactionType,
|
||||||
|
&i.Amount,
|
||||||
|
&i.Currency,
|
||||||
|
&i.ExternalTransactionID,
|
||||||
|
&i.Status,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetVirtualGameSessionByToken = `-- name: GetVirtualGameSessionByToken :one
|
||||||
|
SELECT id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at
|
||||||
|
FROM virtual_game_sessions
|
||||||
|
WHERE session_token = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken string) (VirtualGameSession, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetVirtualGameSessionByToken, sessionToken)
|
||||||
|
var i VirtualGameSession
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.GameID,
|
||||||
|
&i.SessionToken,
|
||||||
|
&i.Currency,
|
||||||
|
&i.Status,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetVirtualGameTransactionByExternalID = `-- 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
|
||||||
|
WHERE external_transaction_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, externalTransactionID string) (VirtualGameTransaction, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetVirtualGameTransactionByExternalID, externalTransactionID)
|
||||||
|
var i VirtualGameTransaction
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SessionID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.WalletID,
|
||||||
|
&i.TransactionType,
|
||||||
|
&i.Amount,
|
||||||
|
&i.Currency,
|
||||||
|
&i.ExternalTransactionID,
|
||||||
|
&i.Status,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec
|
||||||
|
UPDATE virtual_game_sessions
|
||||||
|
SET status = $2, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateVirtualGameSessionStatusParams struct {
|
||||||
|
ID int64
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateVirtualGameSessionStatus(ctx context.Context, arg UpdateVirtualGameSessionStatusParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, UpdateVirtualGameSessionStatus, arg.ID, arg.Status)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateVirtualGameTransactionStatus = `-- name: UpdateVirtualGameTransactionStatus :exec
|
||||||
|
UPDATE virtual_game_transactions
|
||||||
|
SET status = $2, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateVirtualGameTransactionStatusParams struct {
|
||||||
|
ID int64
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateVirtualGameTransactionStatus(ctx context.Context, arg UpdateVirtualGameTransactionStatusParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, UpdateVirtualGameTransactionStatus, arg.ID, arg.Status)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -6,20 +6,25 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidDbUrl = errors.New("db url is invalid")
|
ErrInvalidDbUrl = errors.New("db url is invalid")
|
||||||
ErrInvalidPort = errors.New("port number is invalid")
|
ErrInvalidPort = errors.New("port number is invalid")
|
||||||
ErrRefreshExpiry = errors.New("refresh token expiry is invalid")
|
ErrRefreshExpiry = errors.New("refresh token expiry is invalid")
|
||||||
ErrAccessExpiry = errors.New("access token expiry is invalid")
|
ErrAccessExpiry = errors.New("access token expiry is invalid")
|
||||||
ErrInvalidJwtKey = errors.New("jwt key is invalid")
|
ErrInvalidJwtKey = errors.New("jwt key is invalid")
|
||||||
ErrLogLevel = errors.New("log level not set")
|
ErrLogLevel = errors.New("log level not set")
|
||||||
ErrInvalidLevel = errors.New("invalid log level")
|
ErrInvalidLevel = errors.New("invalid log level")
|
||||||
ErrInvalidEnv = errors.New("env not set or invalid")
|
ErrInvalidEnv = errors.New("env not set or invalid")
|
||||||
ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid")
|
ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid")
|
||||||
|
ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid")
|
||||||
|
ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid")
|
||||||
|
ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid")
|
||||||
|
ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|
@ -34,6 +39,7 @@ type Config struct {
|
||||||
AFRO_SMS_SENDER_NAME string
|
AFRO_SMS_SENDER_NAME string
|
||||||
AFRO_SMS_RECEIVER_PHONE_NUMBER string
|
AFRO_SMS_RECEIVER_PHONE_NUMBER string
|
||||||
ADRO_SMS_HOST_URL string
|
ADRO_SMS_HOST_URL string
|
||||||
|
PopOK domain.PopOKConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() (*Config, error) {
|
func NewConfig() (*Config, error) {
|
||||||
|
|
@ -125,6 +131,31 @@ func (c *Config) loadEnv() error {
|
||||||
if c.ADRO_SMS_HOST_URL == "" {
|
if c.ADRO_SMS_HOST_URL == "" {
|
||||||
c.ADRO_SMS_HOST_URL = "https://api.afrosms.com"
|
c.ADRO_SMS_HOST_URL = "https://api.afrosms.com"
|
||||||
}
|
}
|
||||||
|
popOKClientID := os.Getenv("POPOK_CLIENT_ID")
|
||||||
|
if popOKClientID == "" {
|
||||||
|
return ErrInvalidPopOKClientID
|
||||||
|
}
|
||||||
|
|
||||||
|
popOKSecretKey := os.Getenv("POPOK_SECRET_KEY")
|
||||||
|
if popOKSecretKey == "" {
|
||||||
|
return ErrInvalidPopOKSecretKey
|
||||||
|
}
|
||||||
|
|
||||||
|
popOKBaseURL := os.Getenv("POPOK_BASE_URL")
|
||||||
|
if popOKBaseURL == "" {
|
||||||
|
return ErrInvalidPopOKBaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
popOKCallbackURL := os.Getenv("POPOK_CALLBACK_URL")
|
||||||
|
if popOKCallbackURL == "" {
|
||||||
|
return ErrInvalidPopOKCallbackURL
|
||||||
|
}
|
||||||
|
|
||||||
|
c.PopOK = domain.PopOKConfig{
|
||||||
|
ClientID: popOKClientID,
|
||||||
|
SecretKey: popOKSecretKey,
|
||||||
|
BaseURL: popOKBaseURL,
|
||||||
|
CallbackURL: popOKCallbackURL,
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
internal/domain/virtual_game.go
Normal file
55
internal/domain/virtual_game.go
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VirtualGameSession struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
GameID string `json:"game_id"`
|
||||||
|
SessionToken string `json:"session_token"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Status string `json:"status"` // ACTIVE, COMPLETED, FAILED
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualGameTransaction struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
SessionID int64 `json:"session_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
WalletID int64 `json:"wallet_id"`
|
||||||
|
TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, JACKPOT_WIN
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
ExternalTransactionID string `json:"external_transaction_id"`
|
||||||
|
Status string `json:"status"` // PENDING, COMPLETED, FAILED
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateVirtualGameSession struct {
|
||||||
|
UserID int64
|
||||||
|
GameID string
|
||||||
|
Currency string
|
||||||
|
Mode string // REAL, DEMO
|
||||||
|
}
|
||||||
|
|
||||||
|
type PopOKConfig struct {
|
||||||
|
ClientID string
|
||||||
|
SecretKey string
|
||||||
|
BaseURL string
|
||||||
|
CallbackURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PopOKCallback struct {
|
||||||
|
TransactionID string `json:"transaction_id"`
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
Type string `json:"type"` // BET, WIN, REFUND, JACKPOT_WIN
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Signature string `json:"signature"` // HMAC-SHA256 signature for verification
|
||||||
|
}
|
||||||
|
|
@ -96,8 +96,8 @@ func (r *ReferralRepo) GetReferralStats(ctx context.Context, userID string) (*do
|
||||||
return &domain.ReferralStats{
|
return &domain.ReferralStats{
|
||||||
TotalReferrals: int(stats.TotalReferrals),
|
TotalReferrals: int(stats.TotalReferrals),
|
||||||
CompletedReferrals: int(stats.CompletedReferrals),
|
CompletedReferrals: int(stats.CompletedReferrals),
|
||||||
TotalRewardEarned: float64(stats.TotalRewardEarned),
|
TotalRewardEarned: stats.TotalRewardEarned.(float64),
|
||||||
PendingRewards: float64(stats.PendingRewards),
|
PendingRewards: stats.PendingRewards.(float64),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
114
internal/repository/virtual_game.go
Normal file
114
internal/repository/virtual_game.go
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VirtualGameRepository interface {
|
||||||
|
CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error
|
||||||
|
GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error)
|
||||||
|
UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error
|
||||||
|
CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error
|
||||||
|
GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error)
|
||||||
|
UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualGameRepo struct {
|
||||||
|
store *Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVirtualGameRepository(store *Store) VirtualGameRepository {
|
||||||
|
return &VirtualGameRepo{store: store}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VirtualGameRepo) CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error {
|
||||||
|
params := dbgen.CreateVirtualGameSessionParams{
|
||||||
|
UserID: session.UserID,
|
||||||
|
GameID: session.GameID,
|
||||||
|
SessionToken: session.SessionToken,
|
||||||
|
Currency: session.Currency,
|
||||||
|
Status: session.Status,
|
||||||
|
ExpiresAt: pgtype.Timestamptz{Time: session.ExpiresAt, Valid: true},
|
||||||
|
}
|
||||||
|
_, err := r.store.queries.CreateVirtualGameSession(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VirtualGameRepo) GetVirtualGameSessionByToken(ctx context.Context, token string) (*domain.VirtualGameSession, error) {
|
||||||
|
dbSession, err := r.store.queries.GetVirtualGameSessionByToken(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &domain.VirtualGameSession{
|
||||||
|
ID: dbSession.ID,
|
||||||
|
UserID: dbSession.UserID,
|
||||||
|
GameID: dbSession.GameID,
|
||||||
|
SessionToken: dbSession.SessionToken,
|
||||||
|
Currency: dbSession.Currency,
|
||||||
|
Status: dbSession.Status,
|
||||||
|
CreatedAt: dbSession.CreatedAt.Time,
|
||||||
|
UpdatedAt: dbSession.UpdatedAt.Time,
|
||||||
|
ExpiresAt: dbSession.ExpiresAt.Time,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VirtualGameRepo) UpdateVirtualGameSessionStatus(ctx context.Context, id int64, status string) error {
|
||||||
|
return r.store.queries.UpdateVirtualGameSessionStatus(ctx, dbgen.UpdateVirtualGameSessionStatusParams{
|
||||||
|
ID: id,
|
||||||
|
Status: status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx *domain.VirtualGameTransaction) error {
|
||||||
|
params := dbgen.CreateVirtualGameTransactionParams{
|
||||||
|
SessionID: tx.SessionID,
|
||||||
|
UserID: tx.UserID,
|
||||||
|
WalletID: tx.WalletID,
|
||||||
|
TransactionType: tx.TransactionType,
|
||||||
|
Amount: tx.Amount,
|
||||||
|
Currency: tx.Currency,
|
||||||
|
ExternalTransactionID: tx.ExternalTransactionID,
|
||||||
|
Status: tx.Status,
|
||||||
|
}
|
||||||
|
_, err := r.store.queries.CreateVirtualGameTransaction(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 {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &domain.VirtualGameTransaction{
|
||||||
|
ID: dbTx.ID,
|
||||||
|
SessionID: dbTx.SessionID,
|
||||||
|
UserID: dbTx.UserID,
|
||||||
|
WalletID: dbTx.WalletID,
|
||||||
|
TransactionType: dbTx.TransactionType,
|
||||||
|
Amount: dbTx.Amount,
|
||||||
|
Currency: dbTx.Currency,
|
||||||
|
ExternalTransactionID: dbTx.ExternalTransactionID,
|
||||||
|
Status: dbTx.Status,
|
||||||
|
CreatedAt: dbTx.CreatedAt.Time,
|
||||||
|
UpdatedAt: dbTx.UpdatedAt.Time,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VirtualGameRepo) UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error {
|
||||||
|
return r.store.queries.UpdateVirtualGameTransactionStatus(ctx, dbgen.UpdateVirtualGameTransactionStatusParams{
|
||||||
|
ID: id,
|
||||||
|
Status: status,
|
||||||
|
})
|
||||||
|
}
|
||||||
12
internal/services/virtualGame/port.go
Normal file
12
internal/services/virtualGame/port.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package virtualgameservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VirtualGameService interface {
|
||||||
|
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
|
||||||
|
HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error
|
||||||
|
}
|
||||||
169
internal/services/virtualGame/service.go
Normal file
169
internal/services/virtualGame/service.go
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
package virtualgameservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo repository.VirtualGameRepository
|
||||||
|
walletSvc wallet.Service
|
||||||
|
store *repository.Store
|
||||||
|
config *config.Config
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(repo repository.VirtualGameRepository, walletSvc wallet.Service, store *repository.Store, cfg *config.Config, logger *slog.Logger) VirtualGameService {
|
||||||
|
return &service{
|
||||||
|
repo: repo,
|
||||||
|
walletSvc: walletSvc,
|
||||||
|
store: store,
|
||||||
|
config: cfg,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) {
|
||||||
|
user, err := s.store.GetUserByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to get user", "userID", userID, "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionToken := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano())
|
||||||
|
token, err := jwtutil.CreatePopOKJwt(
|
||||||
|
userID,
|
||||||
|
user.PhoneNumber,
|
||||||
|
currency,
|
||||||
|
"en",
|
||||||
|
mode,
|
||||||
|
sessionToken,
|
||||||
|
s.config.PopOK.SecretKey,
|
||||||
|
24*time.Hour,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to create PopOK JWT", "userID", userID, "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
params := fmt.Sprintf(
|
||||||
|
"client_id=%s&game_id=%s¤cy=%s&lang=en&mode=%s&token=%s",
|
||||||
|
s.config.PopOK.ClientID, gameID, currency, mode, token,
|
||||||
|
)
|
||||||
|
signature := s.generateSignature(params)
|
||||||
|
return fmt.Sprintf("%s/game/launch?%s&signature=%s", s.config.PopOK.BaseURL, params, signature), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error {
|
||||||
|
s.logger.Info("Handling PopOK callback", "transactionID", callback.TransactionID, "type", callback.Type)
|
||||||
|
|
||||||
|
if !s.verifySignature(callback) {
|
||||||
|
s.logger.Error("Invalid callback signature", "transactionID", callback.TransactionID)
|
||||||
|
return errors.New("invalid signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.TransactionID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to check existing transaction", "transactionID", callback.TransactionID, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if existingTx != nil {
|
||||||
|
s.logger.Warn("Transaction already processed", "transactionID", callback.TransactionID)
|
||||||
|
return nil // Idempotency
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID)
|
||||||
|
if err != nil || session == nil {
|
||||||
|
s.logger.Error("Invalid or missing session", "sessionID", callback.SessionID, "error", err)
|
||||||
|
return errors.New("invalid session")
|
||||||
|
}
|
||||||
|
|
||||||
|
wallets, err := s.walletSvc.GetWalletsByUser(ctx, session.UserID)
|
||||||
|
if err != nil || len(wallets) == 0 {
|
||||||
|
s.logger.Error("Failed to get wallets or no wallet found", "userID", session.UserID, "error", err)
|
||||||
|
return errors.New("user has no wallet")
|
||||||
|
}
|
||||||
|
|
||||||
|
walletID := wallets[0].ID
|
||||||
|
amount := int64(callback.Amount * 100) // Convert to cents
|
||||||
|
transactionType := callback.Type
|
||||||
|
|
||||||
|
switch transactionType {
|
||||||
|
case "BET":
|
||||||
|
amount = -amount // Debit for bets
|
||||||
|
case "WIN", "JACKPOT_WIN", "REFUND":
|
||||||
|
default:
|
||||||
|
s.logger.Error("Unknown transaction type", "transactionID", callback.TransactionID, "type", transactionType)
|
||||||
|
return errors.New("unknown transaction type")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.walletSvc.Add(ctx, walletID, domain.Currency(amount))
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to update wallet", "walletID", walletID, "userID", session.UserID, "amount", amount, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record transaction
|
||||||
|
tx := &domain.VirtualGameTransaction{
|
||||||
|
SessionID: session.ID,
|
||||||
|
UserID: session.UserID,
|
||||||
|
WalletID: walletID,
|
||||||
|
TransactionType: transactionType,
|
||||||
|
Amount: amount,
|
||||||
|
Currency: callback.Currency,
|
||||||
|
ExternalTransactionID: callback.TransactionID,
|
||||||
|
Status: "COMPLETED",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil {
|
||||||
|
s.logger.Error("Failed to create transaction", "transactionID", callback.TransactionID, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Callback processed successfully", "transactionID", callback.TransactionID, "type", transactionType, "amount", callback.Amount)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) generateSignature(params string) string {
|
||||||
|
h := hmac.New(sha256.New, []byte(s.config.PopOK.SecretKey))
|
||||||
|
h.Write([]byte(params))
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) verifySignature(callback *domain.PopOKCallback) bool {
|
||||||
|
data, _ := json.Marshal(struct {
|
||||||
|
TransactionID string `json:"transaction_id"`
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
}{
|
||||||
|
TransactionID: callback.TransactionID,
|
||||||
|
SessionID: callback.SessionID,
|
||||||
|
Type: callback.Type,
|
||||||
|
Amount: callback.Amount,
|
||||||
|
Currency: callback.Currency,
|
||||||
|
Timestamp: callback.Timestamp,
|
||||||
|
})
|
||||||
|
|
||||||
|
h := hmac.New(sha256.New, []byte(s.config.PopOK.SecretKey))
|
||||||
|
h.Write(data)
|
||||||
|
expected := hex.EncodeToString(h.Sum(nil))
|
||||||
|
return expected == callback.Signature
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||||
|
|
@ -28,6 +29,7 @@ type App struct {
|
||||||
authSvc *authentication.Service
|
authSvc *authentication.Service
|
||||||
userSvc *user.Service
|
userSvc *user.Service
|
||||||
betSvc *bet.Service
|
betSvc *bet.Service
|
||||||
|
virtualGameSvc virtualgameservice.VirtualGameService
|
||||||
walletSvc *wallet.Service
|
walletSvc *wallet.Service
|
||||||
transactionSvc *transaction.Service
|
transactionSvc *transaction.Service
|
||||||
ticketSvc *ticket.Service
|
ticketSvc *ticket.Service
|
||||||
|
|
@ -48,6 +50,7 @@ func NewApp(
|
||||||
transactionSvc *transaction.Service,
|
transactionSvc *transaction.Service,
|
||||||
notidicationStore notificationservice.NotificationStore,
|
notidicationStore notificationservice.NotificationStore,
|
||||||
referralSvc referralservice.ReferralStore,
|
referralSvc referralservice.ReferralStore,
|
||||||
|
virtualGameSvc virtualgameservice.VirtualGameService,
|
||||||
) *App {
|
) *App {
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
CaseSensitive: true,
|
CaseSensitive: true,
|
||||||
|
|
@ -70,6 +73,7 @@ func NewApp(
|
||||||
NotidicationStore: notidicationStore,
|
NotidicationStore: notidicationStore,
|
||||||
referralSvc: referralSvc,
|
referralSvc: referralSvc,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
|
virtualGameSvc: virtualGameSvc,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.initAppRoutes()
|
s.initAppRoutes()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||||
|
|
@ -24,13 +25,14 @@ type Handler struct {
|
||||||
transactionSvc *transaction.Service
|
transactionSvc *transaction.Service
|
||||||
ticketSvc *ticket.Service
|
ticketSvc *ticket.Service
|
||||||
betSvc *bet.Service
|
betSvc *bet.Service
|
||||||
|
virtualGameSvc virtualgameservice.VirtualGameService
|
||||||
authSvc *authentication.Service
|
authSvc *authentication.Service
|
||||||
jwtConfig jwtutil.JwtConfig
|
jwtConfig jwtutil.JwtConfig
|
||||||
validator *customvalidator.CustomValidator
|
validator *customvalidator.CustomValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger *slog.Logger, notificationSvc notificationservice.NotificationStore, validator *customvalidator.CustomValidator, walletSvc *wallet.Service,
|
func New(logger *slog.Logger, notificationSvc notificationservice.NotificationStore, validator *customvalidator.CustomValidator, walletSvc *wallet.Service,
|
||||||
referralSvc referralservice.ReferralStore, userSvc *user.Service, transactionSvc *transaction.Service, ticketSvc *ticket.Service, betSvc *bet.Service, authSvc *authentication.Service, jwtConfig jwtutil.JwtConfig) *Handler {
|
referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, userSvc *user.Service, transactionSvc *transaction.Service, ticketSvc *ticket.Service, betSvc *bet.Service, authSvc *authentication.Service, jwtConfig jwtutil.JwtConfig) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
notificationSvc: notificationSvc,
|
notificationSvc: notificationSvc,
|
||||||
|
|
@ -41,6 +43,7 @@ func New(logger *slog.Logger, notificationSvc notificationservice.NotificationSt
|
||||||
transactionSvc: transactionSvc,
|
transactionSvc: transactionSvc,
|
||||||
ticketSvc: ticketSvc,
|
ticketSvc: ticketSvc,
|
||||||
betSvc: betSvc,
|
betSvc: betSvc,
|
||||||
|
virtualGameSvc: virtualGameSvc,
|
||||||
authSvc: authSvc,
|
authSvc: authSvc,
|
||||||
jwtConfig: jwtConfig,
|
jwtConfig: jwtConfig,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
83
internal/web_server/handlers/virtual_games_hadlers.go
Normal file
83
internal/web_server/handlers/virtual_games_hadlers.go
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LaunchVirtualGame godoc
|
||||||
|
// @Summary Launch a PopOK virtual game
|
||||||
|
// @Description Generates a URL to launch a PopOK game
|
||||||
|
// @Tags virtual-game
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param launch body launchVirtualGameReq true "Game launch details"
|
||||||
|
// @Success 200 {object} launchVirtualGameRes
|
||||||
|
// @Failure 400 {object} response.APIResponse
|
||||||
|
// @Failure 401 {object} response.APIResponse
|
||||||
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Router /virtual-game/launch [post]
|
||||||
|
func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error {
|
||||||
|
type launchVirtualGameReq struct {
|
||||||
|
GameID string `json:"game_id" validate:"required" example:"crash_001"`
|
||||||
|
Currency string `json:"currency" validate:"required,len=3" example:"USD"`
|
||||||
|
Mode string `json:"mode" validate:"required,oneof=REAL DEMO" example:"REAL"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type launchVirtualGameRes struct {
|
||||||
|
LaunchURL string `json:"launch_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, ok := c.Locals("user_id").(int64)
|
||||||
|
if !ok || userID == 0 {
|
||||||
|
h.logger.Error("Invalid user ID in context")
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req launchVirtualGameReq
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
h.logger.Error("Failed to parse LaunchVirtualGame request", "error", err)
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||||
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := h.virtualGameSvc.GenerateGameLaunchURL(c.Context(), userID, req.GameID, req.Currency, req.Mode)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("Failed to generate game launch URL", "userID", userID, "gameID", req.GameID, "error", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to launch game")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := launchVirtualGameRes{LaunchURL: url}
|
||||||
|
return response.WriteJSON(c, fiber.StatusOK, "Game launched successfully", res, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleVirtualGameCallback godoc
|
||||||
|
// @Summary Handle PopOK game callback
|
||||||
|
// @Description Processes callbacks from PopOK for game events
|
||||||
|
// @Tags virtual-game
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param callback body domain.PopOKCallback true "Callback data"
|
||||||
|
// @Success 200 {object} response.APIResponse
|
||||||
|
// @Failure 400 {object} response.APIResponse
|
||||||
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Router /virtual-game/callback [post]
|
||||||
|
func (h *Handler) HandleVirtualGameCallback(c *fiber.Ctx) error {
|
||||||
|
var callback domain.PopOKCallback
|
||||||
|
if err := c.BodyParser(&callback); err != nil {
|
||||||
|
h.logger.Error("Failed to parse callback", "error", err)
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid callback data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.virtualGameSvc.HandleCallback(c.Context(), &callback); err != nil {
|
||||||
|
h.logger.Error("Failed to handle callback", "transactionID", callback.TransactionID, "error", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to process callback")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.WriteJSON(c, fiber.StatusOK, "Callback processed successfully", nil, nil)
|
||||||
|
}
|
||||||
|
|
@ -8,14 +8,11 @@ import (
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// type UserToken struct {
|
|
||||||
// UserId string
|
|
||||||
// }
|
|
||||||
var (
|
var (
|
||||||
ErrExpiredToken = errors.New("token expired")
|
ErrExpiredToken = errors.New("token expired")
|
||||||
ErrMalformedToken = errors.New("token malformed")
|
ErrMalformedToken = errors.New("token malformed")
|
||||||
ErrTokenNotExpired = errors.New("token not expired")
|
ErrTokenNotExpired = errors.New("token not expired")
|
||||||
ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again
|
ErrRefreshTokenNotFound = errors.New("refresh token not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserClaim struct {
|
type UserClaim struct {
|
||||||
|
|
@ -23,23 +20,61 @@ type UserClaim struct {
|
||||||
UserId int64
|
UserId int64
|
||||||
Role domain.Role
|
Role domain.Role
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PopOKClaim struct {
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Lang string `json:"lang"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
}
|
||||||
|
|
||||||
type JwtConfig struct {
|
type JwtConfig struct {
|
||||||
JwtAccessKey string
|
JwtAccessKey string
|
||||||
JwtAccessExpiry int
|
JwtAccessExpiry int
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateJwt(userId int64, Role domain.Role, key string, expiry int) (string, error) {
|
func CreateJwt(userId int64, Role domain.Role, key string, expiry int) (string, error) {
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{RegisteredClaims: jwt.RegisteredClaims{Issuer: "github.com/lafetz/snippitstash",
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
Audience: jwt.ClaimStrings{"fortune.com"},
|
Issuer: "github.com/lafetz/snippitstash",
|
||||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiry) * time.Second))},
|
Audience: jwt.ClaimStrings{"fortune.com"},
|
||||||
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiry) * time.Second)),
|
||||||
|
},
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
Role: Role,
|
Role: Role,
|
||||||
})
|
})
|
||||||
jwtToken, err := token.SignedString([]byte(key)) //
|
jwtToken, err := token.SignedString([]byte(key))
|
||||||
return jwtToken, err
|
return jwtToken, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
Audience: jwt.ClaimStrings{"popokgaming.com"},
|
||||||
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiry)),
|
||||||
|
},
|
||||||
|
UserID: userID,
|
||||||
|
Username: username,
|
||||||
|
Currency: currency,
|
||||||
|
Lang: lang,
|
||||||
|
Mode: mode,
|
||||||
|
SessionID: sessionID,
|
||||||
|
})
|
||||||
|
jwtToken, err := token.SignedString([]byte(key))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return jwtToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ParseJwt(jwtToken string, key string) (*UserClaim, error) {
|
func ParseJwt(jwtToken string, key string) (*UserClaim, error) {
|
||||||
token, err := jwt.ParseWithClaims(jwtToken, &UserClaim{}, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(jwtToken, &UserClaim{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
return []byte(key), nil
|
return []byte(key), nil
|
||||||
|
|
@ -56,5 +91,24 @@ func ParseJwt(jwtToken string, key string) (*UserClaim, error) {
|
||||||
if claims, ok := token.Claims.(*UserClaim); ok && token.Valid {
|
if claims, ok := token.Claims.(*UserClaim); ok && token.Valid {
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, errors.New("invalid token claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParsePopOKJwt(jwtToken string, key string) (*PopOKClaim, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(jwtToken, &PopOKClaim{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte(key), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||||
|
return nil, ErrExpiredToken
|
||||||
|
}
|
||||||
|
if errors.Is(err, jwt.ErrTokenMalformed) {
|
||||||
|
return nil, ErrMalformedToken
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if claims, ok := token.Claims.(*PopOKClaim); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid PopOK token claims")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ func (a *App) initAppRoutes() {
|
||||||
a.validator,
|
a.validator,
|
||||||
a.walletSvc,
|
a.walletSvc,
|
||||||
a.referralSvc,
|
a.referralSvc,
|
||||||
|
a.virtualGameSvc,
|
||||||
a.userSvc,
|
a.userSvc,
|
||||||
a.transactionSvc,
|
a.transactionSvc,
|
||||||
a.ticketSvc,
|
a.ticketSvc,
|
||||||
|
|
@ -101,6 +102,10 @@ func (a *App) initAppRoutes() {
|
||||||
a.fiber.Get("/notifications/ws/connect/:recipientID", h.ConnectSocket)
|
a.fiber.Get("/notifications/ws/connect/:recipientID", h.ConnectSocket)
|
||||||
a.fiber.Post("/notifications/mark-as-read", h.MarkNotificationAsRead)
|
a.fiber.Post("/notifications/mark-as-read", h.MarkNotificationAsRead)
|
||||||
a.fiber.Post("/notifications/create", h.CreateAndSendNotification)
|
a.fiber.Post("/notifications/create", h.CreateAndSendNotification)
|
||||||
|
|
||||||
|
// Virtual Game Routes
|
||||||
|
a.fiber.Post("/virtual-game/launch", a.authMiddleware, h.LaunchVirtualGame)
|
||||||
|
a.fiber.Post("/virtual-game/callback", h.HandleVirtualGameCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Router /user/resetPassword [post]
|
// @Router /user/resetPassword [post]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user