Merge branch 'main' onto origin
This commit is contained in:
commit
9c849fe50d
23
cmd/main.go
23
cmd/main.go
|
|
@ -7,12 +7,15 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
// "github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
|
||||
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||
|
||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/router"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
||||
|
|
@ -26,7 +29,11 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
|
||||
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||
|
||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/utils"
|
||||
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
|
|
@ -46,6 +53,7 @@ import (
|
|||
// @name Authorization
|
||||
// @BasePath /
|
||||
func main() {
|
||||
|
||||
cfg, err := config.NewConfig()
|
||||
if err != nil {
|
||||
slog.Error(" Config error:", "err", err)
|
||||
|
|
@ -84,6 +92,19 @@ func main() {
|
|||
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
|
||||
referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger)
|
||||
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
|
||||
aleaService := alea.NewAleaPlayService(
|
||||
vitualGameRepo,
|
||||
*walletSvc,
|
||||
cfg,
|
||||
logger,
|
||||
)
|
||||
|
||||
veliService := veli.NewVeliPlayService(
|
||||
vitualGameRepo,
|
||||
*walletSvc,
|
||||
cfg,
|
||||
logger,
|
||||
)
|
||||
|
||||
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc)
|
||||
httpserver.StartTicketCrons(*ticketSvc)
|
||||
|
|
@ -92,7 +113,7 @@ func main() {
|
|||
JwtAccessKey: cfg.JwtKey,
|
||||
JwtAccessExpiry: cfg.AccessExpiry,
|
||||
}, userSvc,
|
||||
ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, resultSvc)
|
||||
ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, veliService, resultSvc, cfg)
|
||||
logger.Info("Starting server", "port", cfg.Port)
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
|
|
|
|||
1045
docs/docs.go
1045
docs/docs.go
File diff suppressed because it is too large
Load Diff
1045
docs/swagger.json
1045
docs/swagger.json
File diff suppressed because it is too large
Load Diff
|
|
@ -1,4 +1,36 @@
|
|||
definitions:
|
||||
domain.AleaPlayCallback:
|
||||
properties:
|
||||
amount:
|
||||
type: number
|
||||
currency:
|
||||
type: string
|
||||
event_id:
|
||||
type: string
|
||||
game_id:
|
||||
type: string
|
||||
is_free_round:
|
||||
type: boolean
|
||||
multiplier:
|
||||
type: number
|
||||
operator_id:
|
||||
type: string
|
||||
round_id:
|
||||
type: string
|
||||
session_id:
|
||||
type: string
|
||||
signature:
|
||||
type: string
|
||||
timestamp:
|
||||
type: integer
|
||||
transaction_id:
|
||||
type: string
|
||||
type:
|
||||
description: BET, WIN, CASHOUT, etc.
|
||||
type: string
|
||||
user_id:
|
||||
type: string
|
||||
type: object
|
||||
domain.BetOutcome:
|
||||
properties:
|
||||
away_team_name:
|
||||
|
|
@ -89,6 +121,50 @@ definitions:
|
|||
example: 2
|
||||
type: integer
|
||||
type: object
|
||||
domain.ChapaSupportedBank:
|
||||
properties:
|
||||
acct_length:
|
||||
type: integer
|
||||
acct_number_regex:
|
||||
type: string
|
||||
active:
|
||||
type: integer
|
||||
country_id:
|
||||
type: integer
|
||||
created_at:
|
||||
type: string
|
||||
currency:
|
||||
type: string
|
||||
example_value:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
is_24hrs:
|
||||
type: integer
|
||||
is_active:
|
||||
type: integer
|
||||
is_mobilemoney:
|
||||
type: integer
|
||||
is_rtgs:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
slug:
|
||||
type: string
|
||||
swift:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
domain.ChapaSupportedBanksResponse:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/domain.ChapaSupportedBank'
|
||||
type: array
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
domain.CreateBetOutcomeReq:
|
||||
properties:
|
||||
event_id:
|
||||
|
|
@ -124,6 +200,52 @@ definitions:
|
|||
- $ref: '#/definitions/domain.OutcomeStatus'
|
||||
example: 1
|
||||
type: object
|
||||
domain.CreateTransferResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/domain.TransferData'
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
domain.InitPaymentData:
|
||||
properties:
|
||||
checkout_url:
|
||||
type: string
|
||||
tx_ref:
|
||||
type: string
|
||||
type: object
|
||||
domain.InitPaymentRequest:
|
||||
properties:
|
||||
amount:
|
||||
type: string
|
||||
callback_url:
|
||||
type: string
|
||||
currency:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
first_name:
|
||||
type: string
|
||||
last_name:
|
||||
type: string
|
||||
return_url:
|
||||
type: string
|
||||
tx_ref:
|
||||
type: string
|
||||
type: object
|
||||
domain.InitPaymentResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/domain.InitPaymentData'
|
||||
message:
|
||||
description: e.g., "Payment initialized"
|
||||
type: string
|
||||
status:
|
||||
description: '"success"'
|
||||
type: string
|
||||
type: object
|
||||
domain.Odd:
|
||||
properties:
|
||||
category:
|
||||
|
|
@ -329,6 +451,58 @@ definitions:
|
|||
example: 1
|
||||
type: integer
|
||||
type: object
|
||||
domain.TransactionData:
|
||||
properties:
|
||||
amount:
|
||||
type: string
|
||||
currency:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
tx_ref:
|
||||
type: string
|
||||
type: object
|
||||
domain.TransferData:
|
||||
properties:
|
||||
amount:
|
||||
type: string
|
||||
currency:
|
||||
type: string
|
||||
reference:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
domain.TransferRequest:
|
||||
properties:
|
||||
account_number:
|
||||
type: string
|
||||
amount:
|
||||
type: string
|
||||
bank_code:
|
||||
type: string
|
||||
currency:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
recipient_name:
|
||||
type: string
|
||||
reference:
|
||||
type: string
|
||||
type: object
|
||||
domain.TransferVerificationData:
|
||||
properties:
|
||||
account_name:
|
||||
type: string
|
||||
bank_code:
|
||||
type: string
|
||||
reference:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
domain.UpcomingEvent:
|
||||
properties:
|
||||
awayKitImage:
|
||||
|
|
@ -371,6 +545,57 @@ definitions:
|
|||
description: Converted from "time" field in UNIX format
|
||||
type: string
|
||||
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.VerifyTransactionResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/domain.TransactionData'
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
domain.VerifyTransferResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/domain.TransferVerificationData'
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
handlers.AdminRes:
|
||||
properties:
|
||||
created_at:
|
||||
|
|
@ -699,9 +924,6 @@ definitions:
|
|||
type: object
|
||||
handlers.CustomerWalletRes:
|
||||
properties:
|
||||
company_id:
|
||||
example: 1
|
||||
type: integer
|
||||
created_at:
|
||||
type: string
|
||||
customer_id:
|
||||
|
|
@ -729,6 +951,8 @@ definitions:
|
|||
type: object
|
||||
handlers.GetCashierRes:
|
||||
properties:
|
||||
branch_id:
|
||||
type: integer
|
||||
created_at:
|
||||
type: string
|
||||
email:
|
||||
|
|
@ -848,8 +1072,10 @@ definitions:
|
|||
type: object
|
||||
handlers.SearchUserByNameOrPhoneReq:
|
||||
properties:
|
||||
searchString:
|
||||
query:
|
||||
type: string
|
||||
role:
|
||||
$ref: '#/definitions/domain.Role'
|
||||
type: object
|
||||
handlers.SupportedOperationRes:
|
||||
properties:
|
||||
|
|
@ -1152,8 +1378,38 @@ definitions:
|
|||
- access_token
|
||||
- refresh_token
|
||||
type: object
|
||||
handlers.updateUserReq:
|
||||
handlers.updateAdminReq:
|
||||
properties:
|
||||
company_id:
|
||||
example: 1
|
||||
type: integer
|
||||
first_name:
|
||||
example: John
|
||||
type: string
|
||||
last_name:
|
||||
example: Doe
|
||||
type: string
|
||||
suspended:
|
||||
example: false
|
||||
type: boolean
|
||||
type: object
|
||||
handlers.updateCashierReq:
|
||||
properties:
|
||||
first_name:
|
||||
example: John
|
||||
type: string
|
||||
last_name:
|
||||
example: Doe
|
||||
type: string
|
||||
suspended:
|
||||
example: false
|
||||
type: boolean
|
||||
type: object
|
||||
handlers.updateManagerReq:
|
||||
properties:
|
||||
company_id:
|
||||
example: 1
|
||||
type: integer
|
||||
first_name:
|
||||
example: John
|
||||
type: string
|
||||
|
|
@ -1269,6 +1525,318 @@ paths:
|
|||
summary: Create Admin
|
||||
tags:
|
||||
- admin
|
||||
/admin/{id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get a single admin by id
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.AdminRes'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Get admin by id
|
||||
tags:
|
||||
- admin
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Update Admin
|
||||
parameters:
|
||||
- description: Update Admin
|
||||
in: body
|
||||
name: admin
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.updateAdminReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Update Admin
|
||||
tags:
|
||||
- admin
|
||||
/api/v1/alea-games/launch:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Generates an authenticated launch URL for Alea Play virtual games
|
||||
parameters:
|
||||
- description: Game identifier (e.g., 'aviator', 'plinko')
|
||||
in: query
|
||||
name: game_id
|
||||
required: true
|
||||
type: string
|
||||
- default: USD
|
||||
description: Currency code (ISO 4217)
|
||||
enum:
|
||||
- USD
|
||||
- EUR
|
||||
- GBP
|
||||
in: query
|
||||
name: currency
|
||||
type: string
|
||||
- default: real
|
||||
description: Game mode
|
||||
enum:
|
||||
- real
|
||||
- demo
|
||||
in: query
|
||||
name: mode
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Returns authenticated game launch URL
|
||||
schema:
|
||||
additionalProperties:
|
||||
allOf:
|
||||
- type: string
|
||||
- properties:
|
||||
launch_url:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Launch an Alea Play virtual game
|
||||
tags:
|
||||
- Alea Virtual Games
|
||||
/api/v1/chapa/banks:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Fetch all supported banks from Chapa
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ChapaSupportedBanksResponse'
|
||||
summary: Get list of banks
|
||||
tags:
|
||||
- Chapa
|
||||
/api/v1/chapa/payments/callback:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Endpoint to receive webhook payloads from Chapa
|
||||
parameters:
|
||||
- description: Webhook Payload (dynamic)
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: ok
|
||||
schema:
|
||||
type: string
|
||||
summary: Receive Chapa webhook
|
||||
tags:
|
||||
- Chapa
|
||||
/api/v1/chapa/payments/initialize:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Initiate a payment through Chapa
|
||||
parameters:
|
||||
- description: Payment initialization request
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.InitPaymentRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.InitPaymentResponse'
|
||||
summary: Initialize a payment transaction
|
||||
tags:
|
||||
- Chapa
|
||||
/api/v1/chapa/payments/verify/{tx_ref}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Verify the transaction status from Chapa using tx_ref
|
||||
parameters:
|
||||
- description: Transaction Reference
|
||||
in: path
|
||||
name: tx_ref
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.VerifyTransactionResponse'
|
||||
summary: Verify a payment transaction
|
||||
tags:
|
||||
- Chapa
|
||||
/api/v1/chapa/transfers:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Initiate a transfer request via Chapa
|
||||
parameters:
|
||||
- description: Transfer request body
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.TransferRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.CreateTransferResponse'
|
||||
summary: Create a money transfer
|
||||
tags:
|
||||
- Chapa
|
||||
/api/v1/chapa/transfers/verify/{transfer_ref}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Check the status of a money transfer via reference
|
||||
parameters:
|
||||
- description: Transfer Reference
|
||||
in: path
|
||||
name: transfer_ref
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.VerifyTransferResponse'
|
||||
summary: Verify a transfer
|
||||
tags:
|
||||
- Chapa
|
||||
/api/v1/webhooks/alea:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Handles webhook callbacks from Alea Play virtual games for bet
|
||||
settlement
|
||||
parameters:
|
||||
- description: Callback payload
|
||||
in: body
|
||||
name: callback
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.AleaPlayCallback'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Callback processed successfully
|
||||
schema:
|
||||
additionalProperties:
|
||||
allOf:
|
||||
- type: string
|
||||
- properties:
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
summary: Process Alea Play game callback
|
||||
tags:
|
||||
- Alea Virtual Games
|
||||
/api/veli/launch/{game_id}:
|
||||
get:
|
||||
description: Generates authenticated launch URL for Veli games
|
||||
parameters:
|
||||
- description: Game ID (e.g., veli_aviator_v1)
|
||||
in: path
|
||||
name: game_id
|
||||
required: true
|
||||
type: string
|
||||
- default: USD
|
||||
description: Currency code
|
||||
in: query
|
||||
name: currency
|
||||
type: string
|
||||
- default: real
|
||||
description: Game mode
|
||||
enum:
|
||||
- real
|
||||
- demo
|
||||
in: query
|
||||
name: mode
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Returns launch URL
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Launch a Veli game
|
||||
tags:
|
||||
- Veli Games
|
||||
/auth/login:
|
||||
post:
|
||||
consumes:
|
||||
|
|
@ -1836,6 +2404,39 @@ paths:
|
|||
summary: Get all branch wallets
|
||||
tags:
|
||||
- wallet
|
||||
/cashier/{id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get a single cashier by id
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.UserProfileRes'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Get cashier by id
|
||||
tags:
|
||||
- cashier
|
||||
/cashiers:
|
||||
get:
|
||||
consumes:
|
||||
|
|
@ -1921,7 +2522,7 @@ paths:
|
|||
name: cashier
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.updateUserReq'
|
||||
$ref: '#/definitions/handlers.updateCashierReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
@ -2221,6 +2822,38 @@ paths:
|
|||
tags:
|
||||
- manager
|
||||
/managers/{id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get a single manager by id
|
||||
parameters:
|
||||
- description: User ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.ManagersRes'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Get manager by id
|
||||
tags:
|
||||
- manager
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
|
|
@ -2231,7 +2864,7 @@ paths:
|
|||
name: Managers
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/handlers.updateUserReq'
|
||||
$ref: '#/definitions/handlers.updateManagerReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
@ -2253,7 +2886,7 @@ paths:
|
|||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Update Managers
|
||||
tags:
|
||||
- Managers
|
||||
- manager
|
||||
/operation:
|
||||
post:
|
||||
consumes:
|
||||
|
|
@ -3488,6 +4121,48 @@ paths:
|
|||
summary: Activate and Deactivate Wallet
|
||||
tags:
|
||||
- wallet
|
||||
/webhooks/veli:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Processes game round settlements from Veli
|
||||
parameters:
|
||||
- description: Callback payload
|
||||
in: body
|
||||
name: payload
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.VeliCallback'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Callback processed
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid payload
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"403":
|
||||
description: Invalid signature
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
"500":
|
||||
description: Processing error
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Veli Games webhook handler
|
||||
tags:
|
||||
- Virtual Games
|
||||
securityDefinitions:
|
||||
Bearer:
|
||||
in: header
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -26,6 +26,7 @@ require (
|
|||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
// github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package config
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
|
|
@ -26,8 +27,32 @@ var (
|
|||
ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid")
|
||||
ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid")
|
||||
ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid")
|
||||
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
|
||||
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
|
||||
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
|
||||
)
|
||||
|
||||
type AleaPlayConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
BaseURL string `mapstructure:"base_url"` // "https://api.aleaplay.com"
|
||||
OperatorID string `mapstructure:"operator_id"` // Your operator ID with Alea
|
||||
SecretKey string `mapstructure:"secret_key"` // API secret for signatures
|
||||
GameListURL string `mapstructure:"game_list_url"` // Endpoint to fetch available games
|
||||
DefaultCurrency string `mapstructure:"default_currency"` // "USD", "EUR", etc.
|
||||
SessionTimeout int `mapstructure:"session_timeout"` // In hours
|
||||
}
|
||||
|
||||
type VeliGamesConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
APIURL string `mapstructure:"api_url"`
|
||||
OperatorKey string `mapstructure:"operator_key"`
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
DefaultCurrency string `mapstructure:"default_currency"`
|
||||
GameIDs struct {
|
||||
Aviator string `mapstructure:"aviator"`
|
||||
} `mapstructure:"game_ids"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Port int
|
||||
DbUrl string
|
||||
|
|
@ -40,8 +65,16 @@ type Config struct {
|
|||
AFRO_SMS_SENDER_NAME string
|
||||
AFRO_SMS_RECEIVER_PHONE_NUMBER string
|
||||
ADRO_SMS_HOST_URL string
|
||||
CHAPA_SECRET_KEY string
|
||||
CHAPA_PUBLIC_KEY string
|
||||
CHAPA_BASE_URL string
|
||||
CHAPA_ENCRYPTION_KEY string
|
||||
CHAPA_CALLBACK_URL string
|
||||
CHAPA_RETURN_URL string
|
||||
Bet365Token string
|
||||
PopOK domain.PopOKConfig
|
||||
AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
|
||||
VeliGames VeliGamesConfig `mapstructure:"veli_games"`
|
||||
}
|
||||
|
||||
func NewConfig() (*Config, error) {
|
||||
|
|
@ -115,6 +148,96 @@ func (c *Config) loadEnv() error {
|
|||
if !ok {
|
||||
return ErrInvalidLevel
|
||||
}
|
||||
|
||||
//Chapa
|
||||
c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY")
|
||||
c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY")
|
||||
c.CHAPA_ENCRYPTION_KEY = os.Getenv("CHAPA_ENCRYPTION_KEY")
|
||||
c.CHAPA_BASE_URL = os.Getenv("CHAPA_BASE_URL")
|
||||
if c.CHAPA_BASE_URL == "" {
|
||||
c.CHAPA_BASE_URL = "https://api.chapa.co/v1"
|
||||
}
|
||||
c.CHAPA_CALLBACK_URL = os.Getenv("CHAPA_CALLBACK_URL")
|
||||
c.CHAPA_RETURN_URL = os.Getenv("CHAPA_RETURN_URL")
|
||||
|
||||
//Alea Play
|
||||
aleaEnabled := os.Getenv("ALEA_ENABLED")
|
||||
if aleaEnabled == "" {
|
||||
aleaEnabled = "false" // Default disabled
|
||||
}
|
||||
|
||||
if enabled, err := strconv.ParseBool(aleaEnabled); err != nil {
|
||||
return fmt.Errorf("invalid ALEA_ENABLED value: %w", err)
|
||||
} else {
|
||||
c.AleaPlay.Enabled = enabled
|
||||
}
|
||||
|
||||
c.AleaPlay.BaseURL = os.Getenv("ALEA_BASE_URL")
|
||||
if c.AleaPlay.BaseURL == "" && c.AleaPlay.Enabled {
|
||||
return errors.New("ALEA_BASE_URL is required when Alea is enabled")
|
||||
}
|
||||
|
||||
c.AleaPlay.OperatorID = os.Getenv("ALEA_OPERATOR_ID")
|
||||
if c.AleaPlay.OperatorID == "" && c.AleaPlay.Enabled {
|
||||
return errors.New("ALEA_OPERATOR_ID is required when Alea is enabled")
|
||||
}
|
||||
|
||||
c.AleaPlay.SecretKey = os.Getenv("ALEA_SECRET_KEY")
|
||||
if c.AleaPlay.SecretKey == "" && c.AleaPlay.Enabled {
|
||||
return errors.New("ALEA_SECRET_KEY is required when Alea is enabled")
|
||||
}
|
||||
|
||||
c.AleaPlay.GameListURL = os.Getenv("ALEA_GAME_LIST_URL")
|
||||
c.AleaPlay.DefaultCurrency = os.Getenv("ALEA_DEFAULT_CURRENCY")
|
||||
if c.AleaPlay.DefaultCurrency == "" {
|
||||
c.AleaPlay.DefaultCurrency = "USD"
|
||||
}
|
||||
|
||||
sessionTimeoutStr := os.Getenv("ALEA_SESSION_TIMEOUT")
|
||||
if sessionTimeoutStr != "" {
|
||||
timeout, err := strconv.Atoi(sessionTimeoutStr)
|
||||
if err == nil {
|
||||
c.AleaPlay.SessionTimeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
//Veli Games
|
||||
veliEnabled := os.Getenv("VELI_ENABLED")
|
||||
if veliEnabled == "" {
|
||||
veliEnabled = "false" // Default to disabled if not specified
|
||||
}
|
||||
|
||||
if enabled, err := strconv.ParseBool(veliEnabled); err != nil {
|
||||
return fmt.Errorf("invalid VELI_ENABLED value: %w", err)
|
||||
} else {
|
||||
c.VeliGames.Enabled = enabled
|
||||
}
|
||||
|
||||
apiURL := os.Getenv("VELI_API_URL")
|
||||
if apiURL == "" {
|
||||
apiURL = "https://api.velitech.games" // Default production URL
|
||||
}
|
||||
c.VeliGames.APIURL = apiURL
|
||||
|
||||
operatorKey := os.Getenv("VELI_OPERATOR_KEY")
|
||||
if operatorKey == "" && c.VeliGames.Enabled {
|
||||
return ErrInvalidVeliOperatorKey
|
||||
}
|
||||
c.VeliGames.OperatorKey = operatorKey
|
||||
|
||||
secretKey := os.Getenv("VELI_SECRET_KEY")
|
||||
if secretKey == "" && c.VeliGames.Enabled {
|
||||
return ErrInvalidVeliSecretKey
|
||||
}
|
||||
c.VeliGames.SecretKey = secretKey
|
||||
c.VeliGames.GameIDs.Aviator = os.Getenv("VELI_GAME_ID_AVIATOR")
|
||||
|
||||
defaultCurrency := os.Getenv("VELI_DEFAULT_CURRENCY")
|
||||
if defaultCurrency == "" {
|
||||
defaultCurrency = "USD" // Default currency
|
||||
}
|
||||
c.VeliGames.DefaultCurrency = defaultCurrency
|
||||
|
||||
c.LogLevel = lvl
|
||||
|
||||
c.AFRO_SMS_API_KEY = os.Getenv("AFRO_SMS_API_KEY")
|
||||
|
|
|
|||
|
|
@ -1 +1,107 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
var (
|
||||
ChapaSecret string
|
||||
ChapaBaseURL string
|
||||
)
|
||||
|
||||
type InitPaymentRequest struct {
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
TxRef string `json:"tx_ref"`
|
||||
CallbackURL string `json:"callback_url"`
|
||||
ReturnURL string `json:"return_url"`
|
||||
}
|
||||
|
||||
type TransferRequest struct {
|
||||
AccountNumber string `json:"account_number"`
|
||||
BankCode string `json:"bank_code"`
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Reference string `json:"reference"`
|
||||
Reason string `json:"reason"`
|
||||
RecipientName string `json:"recipient_name"`
|
||||
}
|
||||
|
||||
type ChapaSupportedBank struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Swift string `json:"swift"`
|
||||
Name string `json:"name"`
|
||||
AcctLength int `json:"acct_length"`
|
||||
AcctNumberRegex string `json:"acct_number_regex"`
|
||||
ExampleValue string `json:"example_value"`
|
||||
CountryId int `json:"country_id"`
|
||||
IsMobilemoney *int `json:"is_mobilemoney"`
|
||||
|
||||
IsActive int `json:"is_active"`
|
||||
IsRtgs *int `json:"is_rtgs"`
|
||||
Active int `json:"active"`
|
||||
Is24Hrs *int `json:"is_24hrs"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type ChapaSupportedBanksResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data []ChapaSupportedBank `json:"data"`
|
||||
}
|
||||
|
||||
type InitPaymentData struct {
|
||||
TxRef string `json:"tx_ref"`
|
||||
CheckoutURL string `json:"checkout_url"`
|
||||
}
|
||||
|
||||
type InitPaymentResponse struct {
|
||||
Status string `json:"status"` // "success"
|
||||
Message string `json:"message"` // e.g., "Payment initialized"
|
||||
Data InitPaymentData `json:"data"`
|
||||
}
|
||||
|
||||
type WebhookPayload map[string]interface{}
|
||||
|
||||
type TransactionData struct {
|
||||
TxRef string `json:"tx_ref"`
|
||||
Status string `json:"status"`
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
CustomerEmail string `json:"email"`
|
||||
}
|
||||
|
||||
type VerifyTransactionResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data TransactionData `json:"data"`
|
||||
}
|
||||
|
||||
type TransferData struct {
|
||||
Reference string `json:"reference"`
|
||||
Status string `json:"status"`
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type CreateTransferResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data TransferData `json:"data"`
|
||||
}
|
||||
|
||||
type TransferVerificationData struct {
|
||||
Reference string `json:"reference"`
|
||||
Status string `json:"status"`
|
||||
BankCode string `json:"bank_code"`
|
||||
AccountName string `json:"account_name"`
|
||||
}
|
||||
|
||||
type VerifyTransferResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data TransferVerificationData `json:"data"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ type VirtualGameSession struct {
|
|||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
|
||||
// Alea Play specific fields
|
||||
ExternalSessionID string `json:"external_session_id"` // Alea's session reference
|
||||
OperatorID string `json:"operator_id"` // Your operator ID with Alea
|
||||
GameMode string `json:"game_mode"` // real, demo, tournament
|
||||
}
|
||||
|
||||
type VirtualGameTransaction struct {
|
||||
|
|
@ -21,15 +26,38 @@ type VirtualGameTransaction struct {
|
|||
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"`
|
||||
TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc.
|
||||
Amount int64 `json:"amount"` // Always in cents
|
||||
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"`
|
||||
|
||||
// Alea Play specific fields
|
||||
GameRoundID string `json:"game_round_id"` // Round identifier
|
||||
Multiplier float64 `json:"multiplier"` // For games like Aviator
|
||||
IsFreeRound bool `json:"is_free_round"` // For bonus play
|
||||
OperatorID string `json:"operator_id"` // Your operator ID
|
||||
|
||||
// Veli specific fields
|
||||
GameSpecificData GameSpecificData `json:"game_specific_data"`
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -53,3 +81,39 @@ type PopOKCallback struct {
|
|||
Timestamp int64 `json:"timestamp"`
|
||||
Signature string `json:"signature"` // HMAC-SHA256 signature for verification
|
||||
}
|
||||
|
||||
type AleaPlayCallback struct {
|
||||
EventID string `json:"event_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
SessionID string `json:"session_id"`
|
||||
UserID string `json:"user_id"`
|
||||
GameID string `json:"game_id"`
|
||||
Type string `json:"type"` // BET, WIN, CASHOUT, etc.
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
RoundID string `json:"round_id"`
|
||||
Multiplier float64 `json:"multiplier"`
|
||||
IsFreeRound bool `json:"is_free_round"`
|
||||
OperatorID string `json:"operator_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type VeliCallback struct {
|
||||
EventType string `json:"event_type"` // "bet_placed", "game_result", etc.
|
||||
RoundID string `json:"round_id"` // Unique round identifier (replaces transaction_id)
|
||||
SessionID string `json:"session_id"` // Matches VirtualGameSession.SessionToken
|
||||
UserID string `json:"user_id"` // Veli's user identifier
|
||||
GameID string `json:"game_id"` // e.g., "veli_aviator_v1"
|
||||
Amount float64 `json:"amount"` // Transaction amount
|
||||
Multiplier float64 `json:"multiplier"` // For games with multipliers (Aviator/Plinko)
|
||||
Currency string `json:"currency"` // e.g., "USD"
|
||||
Timestamp int64 `json:"timestamp"` // Unix timestamp
|
||||
Signature string `json:"signature"` // HMAC-SHA256
|
||||
}
|
||||
|
||||
type GameSpecificData struct {
|
||||
Multiplier float64 `json:"multiplier,omitempty"`
|
||||
RiskLevel string `json:"risk_level,omitempty"` // For Mines
|
||||
BucketIndex int `json:"bucket_index,omitempty"` // For Plinko
|
||||
}
|
||||
|
|
|
|||
34
internal/middleware/alea.go
Normal file
34
internal/middleware/alea.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func AleaWebhookMiddleware(secretKey string) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Verify IP comes from Alea's allowed IPs
|
||||
// OR verify a signature header
|
||||
|
||||
// Example signature verification:
|
||||
receivedSig := c.Get("X-Alea-Signature")
|
||||
body := c.Body()
|
||||
|
||||
h := hmac.New(sha256.New, []byte(secretKey))
|
||||
h.Write(body)
|
||||
expectedSig := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
if receivedSig != expectedSig {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "invalid signature",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Then update your route:
|
||||
|
|
@ -14,6 +14,7 @@ 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
|
||||
// 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
|
||||
|
|
|
|||
12
internal/services/virtualGame/Alea/port.go
Normal file
12
internal/services/virtualGame/Alea/port.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package alea
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
)
|
||||
|
||||
type AleaVirtualGameService interface {
|
||||
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
|
||||
HandleCallback(ctx context.Context, callback *domain.AleaPlayCallback) error
|
||||
}
|
||||
159
internal/services/virtualGame/Alea/service.go
Normal file
159
internal/services/virtualGame/Alea/service.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
package alea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"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"
|
||||
)
|
||||
|
||||
type AleaPlayService struct {
|
||||
repo repository.VirtualGameRepository
|
||||
walletSvc wallet.Service
|
||||
config *config.AleaPlayConfig
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewAleaPlayService(
|
||||
repo repository.VirtualGameRepository,
|
||||
walletSvc wallet.Service,
|
||||
cfg *config.Config,
|
||||
logger *slog.Logger,
|
||||
) *AleaPlayService {
|
||||
return &AleaPlayService{
|
||||
repo: repo,
|
||||
walletSvc: walletSvc,
|
||||
config: &cfg.AleaPlay,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AleaPlayService) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) {
|
||||
session := &domain.VirtualGameSession{
|
||||
UserID: userID,
|
||||
GameID: gameID,
|
||||
SessionToken: generateSessionToken(userID),
|
||||
Currency: currency,
|
||||
Status: "ACTIVE",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil {
|
||||
return "", fmt.Errorf("failed to create game session: %w", err)
|
||||
}
|
||||
|
||||
params := url.Values{
|
||||
"operator_id": []string{s.config.OperatorID},
|
||||
"user_id": []string{fmt.Sprintf("%d", userID)},
|
||||
"game_id": []string{gameID},
|
||||
"currency": []string{currency},
|
||||
"session_token": []string{session.SessionToken},
|
||||
"mode": []string{mode},
|
||||
"timestamp": []string{fmt.Sprintf("%d", time.Now().Unix())},
|
||||
}
|
||||
|
||||
signature := s.generateSignature(params.Encode())
|
||||
params.Add("signature", signature)
|
||||
|
||||
return fmt.Sprintf("%s/launch?%s", s.config.BaseURL, params.Encode()), nil
|
||||
}
|
||||
|
||||
func (s *AleaPlayService) HandleCallback(ctx context.Context, callback *domain.AleaPlayCallback) error {
|
||||
if !s.verifyCallbackSignature(callback) {
|
||||
return errors.New("invalid callback signature")
|
||||
}
|
||||
|
||||
if existing, _ := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.TransactionID); existing != nil {
|
||||
s.logger.Warn("duplicate transaction detected", "tx_id", callback.TransactionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get game session: %w", err)
|
||||
}
|
||||
|
||||
tx := &domain.VirtualGameTransaction{
|
||||
SessionID: session.ID,
|
||||
UserID: session.UserID,
|
||||
TransactionType: callback.Type,
|
||||
Amount: convertAmount(callback.Amount, callback.Type),
|
||||
Currency: callback.Currency,
|
||||
ExternalTransactionID: callback.TransactionID,
|
||||
Status: "COMPLETED",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.processTransaction(ctx, tx, session.UserID); err != nil {
|
||||
return fmt.Errorf("failed to process transaction: %w", err)
|
||||
}
|
||||
|
||||
// Update session status using the proper repository method
|
||||
if callback.Type == "SESSION_END" {
|
||||
if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil {
|
||||
s.logger.Error("failed to update session status",
|
||||
"sessionID", session.ID,
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertAmount(amount float64, txType string) int64 {
|
||||
cents := int64(amount * 100)
|
||||
if txType == "BET" {
|
||||
return -cents
|
||||
}
|
||||
return cents
|
||||
}
|
||||
|
||||
func (s *AleaPlayService) processTransaction(ctx context.Context, tx *domain.VirtualGameTransaction, userID int64) error {
|
||||
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
|
||||
if err != nil || len(wallets) == 0 {
|
||||
return errors.New("no wallet available for user")
|
||||
}
|
||||
tx.WalletID = wallets[0].ID
|
||||
|
||||
if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil {
|
||||
return fmt.Errorf("wallet update failed: %w", err)
|
||||
}
|
||||
|
||||
return s.repo.CreateVirtualGameTransaction(ctx, tx)
|
||||
}
|
||||
|
||||
func (s *AleaPlayService) generateSignature(data string) string {
|
||||
h := hmac.New(sha256.New, []byte(s.config.SecretKey))
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (s *AleaPlayService) verifyCallbackSignature(cb *domain.AleaPlayCallback) bool {
|
||||
signData := fmt.Sprintf("%s%s%s%.2f%s%d",
|
||||
cb.TransactionID,
|
||||
cb.SessionID,
|
||||
cb.Type,
|
||||
cb.Amount,
|
||||
cb.Currency,
|
||||
cb.Timestamp,
|
||||
)
|
||||
expectedSig := s.generateSignature(signData)
|
||||
return expectedSig == cb.Signature
|
||||
}
|
||||
|
||||
func generateSessionToken(userID int64) string {
|
||||
return fmt.Sprintf("alea-%d-%d", userID, time.Now().UnixNano())
|
||||
}
|
||||
|
|
@ -10,3 +10,4 @@ type VirtualGameService interface {
|
|||
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
|
||||
HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ func New(repo repository.VirtualGameRepository, walletSvc wallet.Service, store
|
|||
walletSvc: walletSvc,
|
||||
store: store,
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
}
|
||||
logger: logger}
|
||||
}
|
||||
|
||||
func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) {
|
||||
|
|
|
|||
13
internal/services/virtualGame/veli/port.go
Normal file
13
internal/services/virtualGame/veli/port.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// services/veli/service.go
|
||||
package veli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
)
|
||||
|
||||
type VeliVirtualGameService interface {
|
||||
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
|
||||
HandleCallback(ctx context.Context, callback *domain.VeliCallback) error
|
||||
}
|
||||
161
internal/services/virtualGame/veli/service.go
Normal file
161
internal/services/virtualGame/veli/service.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package veli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"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"
|
||||
)
|
||||
|
||||
type VeliPlayService struct {
|
||||
repo repository.VirtualGameRepository
|
||||
walletSvc wallet.Service
|
||||
config *config.VeliGamesConfig
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewVeliPlayService(
|
||||
repo repository.VirtualGameRepository,
|
||||
walletSvc wallet.Service,
|
||||
cfg *config.Config,
|
||||
logger *slog.Logger,
|
||||
) *VeliPlayService {
|
||||
return &VeliPlayService{
|
||||
repo: repo,
|
||||
walletSvc: walletSvc,
|
||||
config: &cfg.VeliGames,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateGameLaunchURL mirrors Alea's pattern but uses Veli's auth requirements
|
||||
func (s *VeliPlayService) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) {
|
||||
session := &domain.VirtualGameSession{
|
||||
UserID: userID,
|
||||
GameID: gameID,
|
||||
SessionToken: generateSessionToken(userID),
|
||||
Currency: currency,
|
||||
Status: "ACTIVE",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
|
||||
if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil {
|
||||
return "", fmt.Errorf("failed to create game session: %w", err)
|
||||
}
|
||||
|
||||
// Veli-specific parameters
|
||||
params := url.Values{
|
||||
"operator_key": []string{s.config.OperatorKey}, // Different from Alea's operator_id
|
||||
"user_id": []string{fmt.Sprintf("%d", userID)},
|
||||
"game_id": []string{gameID},
|
||||
"currency": []string{currency},
|
||||
"mode": []string{mode},
|
||||
"timestamp": []string{fmt.Sprintf("%d", time.Now().Unix())},
|
||||
}
|
||||
|
||||
signature := s.generateSignature(params.Encode())
|
||||
params.Add("signature", signature)
|
||||
|
||||
return fmt.Sprintf("%s/launch?%s", s.config.APIURL, params.Encode()), nil
|
||||
}
|
||||
|
||||
// HandleCallback processes Veli's webhooks (similar structure to Alea)
|
||||
func (s *VeliPlayService) HandleCallback(ctx context.Context, callback *domain.VeliCallback) error {
|
||||
if !s.verifyCallbackSignature(callback) {
|
||||
return errors.New("invalid callback signature")
|
||||
}
|
||||
|
||||
// Veli uses round_id instead of transaction_id for idempotency
|
||||
existing, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.RoundID)
|
||||
if err != nil || existing != nil {
|
||||
s.logger.Warn("duplicate round detected", "round_id", callback.RoundID)
|
||||
return nil
|
||||
}
|
||||
|
||||
session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get game session: %w", err)
|
||||
}
|
||||
|
||||
// Convert amount based on event type (BET, WIN, etc.)
|
||||
amount := convertAmount(callback.Amount, callback.EventType)
|
||||
|
||||
tx := &domain.VirtualGameTransaction{
|
||||
SessionID: session.ID,
|
||||
UserID: session.UserID,
|
||||
TransactionType: callback.EventType, // e.g., "bet_placed", "game_result"
|
||||
Amount: amount,
|
||||
Currency: callback.Currency,
|
||||
ExternalTransactionID: callback.RoundID, // Veli uses round_id as the unique identifier
|
||||
Status: "COMPLETED",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
GameSpecificData: domain.GameSpecificData{
|
||||
Multiplier: callback.Multiplier, // Used for Aviator/Plinko
|
||||
},
|
||||
}
|
||||
|
||||
if err := s.processTransaction(ctx, tx, session.UserID); err != nil {
|
||||
return fmt.Errorf("failed to process transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shared helper methods (same pattern as Alea)
|
||||
func (s *VeliPlayService) generateSignature(data string) string {
|
||||
h := hmac.New(sha256.New, []byte(s.config.SecretKey))
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (s *VeliPlayService) verifyCallbackSignature(cb *domain.VeliCallback) bool {
|
||||
signData := fmt.Sprintf("%s%s%s%.2f%s%d",
|
||||
cb.RoundID, // Veli uses round_id instead of transaction_id
|
||||
cb.SessionID,
|
||||
cb.EventType,
|
||||
cb.Amount,
|
||||
cb.Currency,
|
||||
cb.Timestamp,
|
||||
)
|
||||
expectedSig := s.generateSignature(signData)
|
||||
return expectedSig == cb.Signature
|
||||
}
|
||||
|
||||
func convertAmount(amount float64, eventType string) int64 {
|
||||
cents := int64(amount * 100)
|
||||
if eventType == "bet_placed" {
|
||||
return -cents // Debit for bets
|
||||
}
|
||||
return cents // Credit for wins/results
|
||||
}
|
||||
|
||||
func generateSessionToken(userID int64) string {
|
||||
return fmt.Sprintf("veli-%d-%d", userID, time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (s *VeliPlayService) processTransaction(ctx context.Context, tx *domain.VirtualGameTransaction, userID int64) error {
|
||||
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
|
||||
if err != nil || len(wallets) == 0 {
|
||||
return errors.New("no wallet available for user")
|
||||
}
|
||||
tx.WalletID = wallets[0].ID
|
||||
|
||||
if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil {
|
||||
return fmt.Errorf("wallet update failed: %w", err)
|
||||
}
|
||||
|
||||
return s.repo.CreateVirtualGameTransaction(ctx, tx)
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
package wallet
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
||||
|
|
@ -16,6 +17,8 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
|
||||
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
|
|
@ -28,6 +31,9 @@ import (
|
|||
|
||||
type App struct {
|
||||
fiber *fiber.App
|
||||
aleaVirtualGameService alea.AleaVirtualGameService
|
||||
veliVirtualGameService veli.VeliVirtualGameService
|
||||
cfg *config.Config
|
||||
logger *slog.Logger
|
||||
NotidicationStore *notificationservice.Service
|
||||
referralSvc referralservice.ReferralStore
|
||||
|
|
@ -66,7 +72,10 @@ func NewApp(
|
|||
eventSvc event.Service,
|
||||
referralSvc referralservice.ReferralStore,
|
||||
virtualGameSvc virtualgameservice.VirtualGameService,
|
||||
aleaVirtualGameService alea.AleaVirtualGameService,
|
||||
veliVirtualGameService veli.VeliVirtualGameService,
|
||||
resultSvc *result.Service,
|
||||
cfg *config.Config,
|
||||
) *App {
|
||||
app := fiber.New(fiber.Config{
|
||||
CaseSensitive: true,
|
||||
|
|
@ -102,7 +111,10 @@ func NewApp(
|
|||
prematchSvc: prematchSvc,
|
||||
eventSvc: eventSvc,
|
||||
virtualGameSvc: virtualGameSvc,
|
||||
aleaVirtualGameService: aleaVirtualGameService,
|
||||
veliVirtualGameService: veliVirtualGameService,
|
||||
resultSvc: resultSvc,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
s.initAppRoutes()
|
||||
|
|
|
|||
70
internal/web_server/handlers/alea_games.go
Normal file
70
internal/web_server/handlers/alea_games.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// LaunchAleaGame godoc
|
||||
// @Summary Launch an Alea Play virtual game
|
||||
// @Description Generates an authenticated launch URL for Alea Play virtual games
|
||||
// @Tags Alea Virtual Games
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param game_id query string true "Game identifier (e.g., 'aviator', 'plinko')"
|
||||
// @Param currency query string false "Currency code (ISO 4217)" Enums(USD, EUR, GBP) default(USD)
|
||||
// @Param mode query string false "Game mode" Enums(real, demo) default(real)
|
||||
// @Success 200 {object} map[string]string{launch_url=string} "Returns authenticated game launch URL"
|
||||
// @Router /api/v1/alea-games/launch [get]
|
||||
func (h *Handler) LaunchAleaGame(c *fiber.Ctx) error {
|
||||
userID := c.Locals("user_id").(int64)
|
||||
gameID := c.Query("game_id")
|
||||
currency := c.Query("currency", "USD")
|
||||
mode := c.Query("mode", "real") // real or demo
|
||||
|
||||
launchURL, err := h.aleaVirtualGameSvc.GenerateGameLaunchURL(c.Context(), userID, gameID, currency, mode)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to generate Alea launch URL", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "failed to launch game",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"launch_url": launchURL,
|
||||
"message": "Game launched successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// HandleAleaCallback godoc
|
||||
// @Summary Process Alea Play game callback
|
||||
// @Description Handles webhook callbacks from Alea Play virtual games for bet settlement
|
||||
// @Tags Alea Virtual Games
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param callback body domain.AleaPlayCallback true "Callback payload"
|
||||
// @Success 200 {object} map[string]string{status=string} "Callback processed successfully"
|
||||
// @Router /api/v1/webhooks/alea [post]
|
||||
func (h *Handler) HandleAleaCallback(c *fiber.Ctx) error {
|
||||
var cb domain.AleaPlayCallback
|
||||
if err := c.BodyParser(&cb); err != nil {
|
||||
h.logger.Error("invalid Alea callback format", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "invalid callback format",
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.aleaVirtualGameSvc.HandleCallback(c.Context(), &cb); err != nil {
|
||||
h.logger.Error("failed to process Alea callback",
|
||||
"transactionID", cb.TransactionID,
|
||||
"error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "failed to process callback",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "processed",
|
||||
})
|
||||
}
|
||||
283
internal/web_server/handlers/chapa.go
Normal file
283
internal/web_server/handlers/chapa.go
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// GetBanks godoc
|
||||
// @Summary Get list of banks
|
||||
// @Description Fetch all supported banks from Chapa
|
||||
// @Tags Chapa
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.ChapaSupportedBanksResponse
|
||||
// @Router /api/v1/chapa/banks [get]
|
||||
func (h *Handler) GetBanks(c *fiber.Ctx) error {
|
||||
httpReq, err := http.NewRequest("GET", h.Cfg.CHAPA_BASE_URL+"/banks", nil)
|
||||
// log.Printf("\n\nbase url is: %v\n\n", h.Cfg.CHAPA_BASE_URL)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to create request", "details": err.Error()})
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to fetch banks", "details": err.Error()})
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": "Failed to read response", "details": err.Error()})
|
||||
}
|
||||
|
||||
return c.Status(resp.StatusCode).Type("json").Send(body)
|
||||
}
|
||||
|
||||
// InitializePayment godoc
|
||||
// @Summary Initialize a payment transaction
|
||||
// @Description Initiate a payment through Chapa
|
||||
// @Tags Chapa
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body domain.InitPaymentRequest true "Payment initialization request"
|
||||
// @Success 200 {object} domain.InitPaymentResponse
|
||||
// @Router /api/v1/chapa/payments/initialize [post]
|
||||
func (h *Handler) InitializePayment(c *fiber.Ctx) error {
|
||||
var req InitPaymentRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid request body",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Generate and assign a unique transaction reference
|
||||
req.TxRef = uuid.New().String()
|
||||
|
||||
payload, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{
|
||||
"error": "Failed to serialize request",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transaction/initialize", bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{
|
||||
"error": "Failed to create request",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{
|
||||
"error": "Failed to initialize payment",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{
|
||||
"error": "Failed to read response",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(resp.StatusCode).Type("json").Send(body)
|
||||
}
|
||||
|
||||
// VerifyTransaction godoc
|
||||
// @Summary Verify a payment transaction
|
||||
// @Description Verify the transaction status from Chapa using tx_ref
|
||||
// @Tags Chapa
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param tx_ref path string true "Transaction Reference"
|
||||
// @Success 200 {object} domain.VerifyTransactionResponse
|
||||
// @Router /api/v1/chapa/payments/verify/{tx_ref} [get]
|
||||
func (h *Handler) VerifyTransaction(c *fiber.Ctx) error {
|
||||
txRef := c.Params("tx_ref")
|
||||
if txRef == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Missing transaction reference",
|
||||
})
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/transaction/verify/%s", h.Cfg.CHAPA_BASE_URL, txRef)
|
||||
|
||||
httpReq, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{
|
||||
"error": "Failed to create request",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{
|
||||
"error": "Failed to verify transaction",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{
|
||||
"error": "Failed to read response",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(resp.StatusCode).Type("json").Send(body)
|
||||
}
|
||||
|
||||
// ReceiveWebhook godoc
|
||||
// @Summary Receive Chapa webhook
|
||||
// @Description Endpoint to receive webhook payloads from Chapa
|
||||
// @Tags Chapa
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body object true "Webhook Payload (dynamic)"
|
||||
// @Success 200 {string} string "ok"
|
||||
// @Router /api/v1/chapa/payments/callback [post]
|
||||
func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error {
|
||||
var payload map[string]interface{}
|
||||
if err := c.BodyParser(&payload); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid webhook data",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
h.logger.Info("Chapa webhook received", "payload", payload)
|
||||
|
||||
// Optional: you can verify tx_ref here again if needed
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
// CreateTransfer godoc
|
||||
// @Summary Create a money transfer
|
||||
// @Description Initiate a transfer request via Chapa
|
||||
// @Tags Chapa
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body domain.TransferRequest true "Transfer request body"
|
||||
// @Success 200 {object} domain.CreateTransferResponse
|
||||
// @Router /api/v1/chapa/transfers [post]
|
||||
func (h *Handler) CreateTransfer(c *fiber.Ctx) error {
|
||||
var req TransferRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid request",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Inject unique transaction reference
|
||||
req.Reference = uuid.New().String()
|
||||
|
||||
payload, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Failed to serialize request",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transfers", bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Failed to create HTTP request",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Transfer request failed",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Failed to read response",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(resp.StatusCode).Type("json").Send(body)
|
||||
}
|
||||
|
||||
// VerifyTransfer godoc
|
||||
// @Summary Verify a transfer
|
||||
// @Description Check the status of a money transfer via reference
|
||||
// @Tags Chapa
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param transfer_ref path string true "Transfer Reference"
|
||||
// @Success 200 {object} domain.VerifyTransferResponse
|
||||
// @Router /api/v1/chapa/transfers/verify/{transfer_ref} [get]
|
||||
func (h *Handler) VerifyTransfer(c *fiber.Ctx) error {
|
||||
transferRef := c.Params("transfer_ref")
|
||||
if transferRef == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Missing transfer reference in URL",
|
||||
})
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/transfers/verify/%s", h.Cfg.CHAPA_BASE_URL, transferRef)
|
||||
|
||||
httpReq, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Failed to create HTTP request",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Verification request failed",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "Failed to read response body",
|
||||
"details": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(resp.StatusCode).Type("json").Send(body)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package handlers
|
|||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
||||
|
|
@ -15,6 +16,8 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
|
||||
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
|
|
@ -34,9 +37,12 @@ type Handler struct {
|
|||
prematchSvc *odds.ServiceImpl
|
||||
eventSvc event.Service
|
||||
virtualGameSvc virtualgameservice.VirtualGameService
|
||||
aleaVirtualGameSvc alea.AleaVirtualGameService
|
||||
veliVirtualGameSvc veli.VeliVirtualGameService
|
||||
authSvc *authentication.Service
|
||||
jwtConfig jwtutil.JwtConfig
|
||||
validator *customvalidator.CustomValidator
|
||||
Cfg *config.Config
|
||||
}
|
||||
|
||||
func New(
|
||||
|
|
@ -46,6 +52,8 @@ func New(
|
|||
walletSvc *wallet.Service,
|
||||
referralSvc referralservice.ReferralStore,
|
||||
virtualGameSvc virtualgameservice.VirtualGameService,
|
||||
aleaVirtualGameSvc alea.AleaVirtualGameService,
|
||||
veliVirtualGameSvc veli.VeliVirtualGameService,
|
||||
userSvc *user.Service,
|
||||
transactionSvc *transaction.Service,
|
||||
ticketSvc *ticket.Service,
|
||||
|
|
@ -56,6 +64,7 @@ func New(
|
|||
companySvc *company.Service,
|
||||
prematchSvc *odds.ServiceImpl,
|
||||
eventSvc event.Service,
|
||||
cfg *config.Config,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
logger: logger,
|
||||
|
|
@ -72,7 +81,10 @@ func New(
|
|||
prematchSvc: prematchSvc,
|
||||
eventSvc: eventSvc,
|
||||
virtualGameSvc: virtualGameSvc,
|
||||
aleaVirtualGameSvc: aleaVirtualGameSvc,
|
||||
veliVirtualGameSvc: veliVirtualGameSvc,
|
||||
authSvc: authSvc,
|
||||
jwtConfig: jwtConfig,
|
||||
Cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ func (h *Handler) GetAllManagers(c *fiber.Ctx) error {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "User ID"
|
||||
// @Success 200 {object} ManagerRes
|
||||
// @Success 200 {object} ManagersRes
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
|
|
|
|||
107
internal/web_server/handlers/models.chapa.go
Normal file
107
internal/web_server/handlers/models.chapa.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package handlers
|
||||
|
||||
import "time"
|
||||
|
||||
var (
|
||||
ChapaSecret string
|
||||
ChapaBaseURL string
|
||||
)
|
||||
|
||||
type InitPaymentRequest struct {
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
TxRef string `json:"tx_ref"`
|
||||
CallbackURL string `json:"callback_url"`
|
||||
ReturnURL string `json:"return_url"`
|
||||
}
|
||||
|
||||
type TransferRequest struct {
|
||||
AccountNumber string `json:"account_number"`
|
||||
BankCode string `json:"bank_code"`
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Reference string `json:"reference"`
|
||||
Reason string `json:"reason"`
|
||||
RecipientName string `json:"recipient_name"`
|
||||
}
|
||||
|
||||
type ChapaSupportedBank struct {
|
||||
Id int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Swift string `json:"swift"`
|
||||
Name string `json:"name"`
|
||||
AcctLength int `json:"acct_length"`
|
||||
AcctNumberRegex string `json:"acct_number_regex"`
|
||||
ExampleValue string `json:"example_value"`
|
||||
CountryId int `json:"country_id"`
|
||||
IsMobilemoney *int `json:"is_mobilemoney"`
|
||||
|
||||
IsActive int `json:"is_active"`
|
||||
IsRtgs *int `json:"is_rtgs"`
|
||||
Active int `json:"active"`
|
||||
Is24Hrs *int `json:"is_24hrs"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type ChapaSupportedBanksResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data []ChapaSupportedBank `json:"data"`
|
||||
}
|
||||
|
||||
type InitPaymentData struct {
|
||||
TxRef string `json:"tx_ref"`
|
||||
CheckoutURL string `json:"checkout_url"`
|
||||
}
|
||||
|
||||
type InitPaymentResponse struct {
|
||||
Status string `json:"status"` // "success"
|
||||
Message string `json:"message"` // e.g., "Payment initialized"
|
||||
Data InitPaymentData `json:"data"`
|
||||
}
|
||||
|
||||
type WebhookPayload map[string]interface{}
|
||||
|
||||
type TransactionData struct {
|
||||
TxRef string `json:"tx_ref"`
|
||||
Status string `json:"status"`
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
CustomerEmail string `json:"email"`
|
||||
}
|
||||
|
||||
type VerifyTransactionResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data TransactionData `json:"data"`
|
||||
}
|
||||
|
||||
type TransferData struct {
|
||||
Reference string `json:"reference"`
|
||||
Status string `json:"status"`
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type CreateTransferResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data TransferData `json:"data"`
|
||||
}
|
||||
|
||||
type TransferVerificationData struct {
|
||||
Reference string `json:"reference"`
|
||||
Status string `json:"status"`
|
||||
BankCode string `json:"bank_code"`
|
||||
AccountName string `json:"account_name"`
|
||||
}
|
||||
|
||||
type VerifyTransferResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data TransferVerificationData `json:"data"`
|
||||
}
|
||||
|
|
@ -268,7 +268,7 @@ func (h *Handler) GetNotifications(c *fiber.Ctx) error {
|
|||
|
||||
}
|
||||
|
||||
func (h *Handler) getAllRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) {
|
||||
func (h *Handler) GetAllRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) {
|
||||
return h.notificationSvc.ListRecipientIDs(ctx, receiver)
|
||||
}
|
||||
|
||||
|
|
|
|||
75
internal/web_server/handlers/veli_games.go
Normal file
75
internal/web_server/handlers/veli_games.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// LaunchVeliGame godoc
|
||||
// @Summary Launch a Veli game
|
||||
// @Description Generates authenticated launch URL for Veli games
|
||||
// @Tags Veli Games
|
||||
// @Security BearerAuth
|
||||
// @Param game_id path string true "Game ID (e.g., veli_aviator_v1)"
|
||||
// @Param currency query string false "Currency code" default(USD)
|
||||
// @Param mode query string false "Game mode" Enums(real, demo) default(real)
|
||||
// @Success 200 {object} map[string]string "Returns launch URL"
|
||||
// @Failure 400 {object} map[string]string "Invalid request"
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/veli/launch/{game_id} [get]
|
||||
func (h *Handler) LaunchVeliGame(c *fiber.Ctx) error {
|
||||
userID := c.Locals("userID").(int64)
|
||||
gameID := c.Params("game_id")
|
||||
currency := c.Query("currency", "USD")
|
||||
mode := c.Query("mode", "real")
|
||||
|
||||
launchURL, err := h.veliVirtualGameSvc.GenerateGameLaunchURL(c.Context(), userID, gameID, currency, mode)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to generate Veli launch URL",
|
||||
"error", err,
|
||||
"userID", userID,
|
||||
"gameID", gameID)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "failed to launch game",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"launch_url": launchURL,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleVeliCallback godoc
|
||||
// @Summary Veli Games webhook handler
|
||||
// @Description Processes game round settlements from Veli
|
||||
// @Tags Virtual Games
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param payload body domain.VeliCallback true "Callback payload"
|
||||
// @Success 200 {object} map[string]string "Callback processed"
|
||||
// @Failure 400 {object} map[string]string "Invalid payload"
|
||||
// @Failure 403 {object} map[string]string "Invalid signature"
|
||||
// @Failure 500 {object} map[string]string "Processing error"
|
||||
// @Router /webhooks/veli [post]
|
||||
func (h *Handler) HandleVeliCallback(c *fiber.Ctx) error {
|
||||
var cb domain.VeliCallback
|
||||
if err := c.BodyParser(&cb); err != nil {
|
||||
h.logger.Error("invalid Veli callback format", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "invalid payload format",
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.veliVirtualGameSvc.HandleCallback(c.Context(), &cb); err != nil {
|
||||
h.logger.Error("failed to process Veli callback",
|
||||
"roundID", cb.RoundID,
|
||||
"error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "failed to process callback",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "processed",
|
||||
})
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ type CustomerWalletRes struct {
|
|||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func convertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes {
|
||||
func ConvertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes {
|
||||
return CustomerWalletRes{
|
||||
ID: wallet.ID,
|
||||
RegularID: wallet.RegularID,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
_ "github.com/SamuelTariku/FortuneBet-Backend/docs"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
|
@ -20,6 +21,8 @@ func (a *App) initAppRoutes() {
|
|||
a.walletSvc,
|
||||
a.referralSvc,
|
||||
a.virtualGameSvc,
|
||||
a.aleaVirtualGameService,
|
||||
a.veliVirtualGameService,
|
||||
a.userSvc,
|
||||
a.transactionSvc,
|
||||
a.ticketSvc,
|
||||
|
|
@ -30,6 +33,7 @@ func (a *App) initAppRoutes() {
|
|||
a.companySvc,
|
||||
a.prematchSvc,
|
||||
a.eventSvc,
|
||||
a.cfg,
|
||||
)
|
||||
|
||||
a.fiber.Get("/", func(c *fiber.Ctx) error {
|
||||
|
|
@ -173,6 +177,24 @@ func (a *App) initAppRoutes() {
|
|||
a.fiber.Get("/transfer/wallet/:id", a.authMiddleware, h.GetTransfersByWallet)
|
||||
a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet)
|
||||
|
||||
//Chapa Routes
|
||||
group := a.fiber.Group("/api/v1")
|
||||
|
||||
group.Post("/chapa/payments/initialize", h.InitializePayment)
|
||||
group.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction)
|
||||
group.Post("/chapa/payments/callback", h.ReceiveWebhook)
|
||||
group.Get("/chapa/banks", h.GetBanks)
|
||||
group.Post("/chapa/transfers", h.CreateTransfer)
|
||||
group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer)
|
||||
|
||||
//Alea Play Virtual Game Routes
|
||||
group.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame)
|
||||
group.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback)
|
||||
|
||||
//Veli Virtual Game Routes
|
||||
group.Get("/veli-games/launch", a.authMiddleware, h.LaunchVeliGame)
|
||||
group.Post("/webhooks/veli-games", a.authMiddleware, h.HandleVeliCallback)
|
||||
|
||||
// Transactions /transactions
|
||||
a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction)
|
||||
a.fiber.Get("/transaction", a.authMiddleware, h.GetAllTransactions)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user