172 lines
6.7 KiB
PHP
172 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\GameBet;
|
|
use App\Models\OperatorSession;
|
|
use App\Models\User;
|
|
use App\Services\BetiXClient;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class BetiXWebhookController extends Controller
|
|
{
|
|
public function sessionEnded(Request $request, BetiXClient $client): \Illuminate\Http\Response
|
|
{
|
|
$rawBody = $request->getContent();
|
|
$sig = $request->header('X-Signature', '');
|
|
$secret = config('services.betix.webhook_secret', '');
|
|
|
|
// If a webhook secret is configured, verify the signature
|
|
if ($secret !== '' && !$client->verifyWebhook($rawBody, $sig)) {
|
|
Log::warning('[BetiX] Webhook signature mismatch', ['ip' => $request->ip()]);
|
|
return response('Unauthorized', 401);
|
|
}
|
|
|
|
$payload = json_decode($rawBody, true);
|
|
if (!$payload || !isset($payload['session_token'])) {
|
|
return response('Bad Request', 400);
|
|
}
|
|
|
|
$token = $payload['session_token'];
|
|
$playerId = $payload['player_id'] ?? null;
|
|
$endBalance = isset($payload['end_balance']) ? (float) $payload['end_balance'] : null;
|
|
$startBalance = isset($payload['start_balance']) ? (float) $payload['start_balance'] : null;
|
|
$game = $payload['game'] ?? null;
|
|
$rounds = $payload['rounds'] ?? 0;
|
|
|
|
$session = OperatorSession::where('session_token', $token)->first();
|
|
|
|
if (!$session) {
|
|
// Unknown session — could be a replay or a stale token, acknowledge anyway
|
|
Log::info('[BetiX] Webhook for unknown session', ['token' => $token]);
|
|
return response('OK', 200);
|
|
}
|
|
|
|
if ($session->status !== 'active') {
|
|
// Already processed (idempotency)
|
|
return response('OK', 200);
|
|
}
|
|
|
|
DB::transaction(function () use ($session, $playerId, $endBalance, $startBalance, $rounds, $game) {
|
|
// Mark session as expired
|
|
$session->update([
|
|
'status' => 'expired',
|
|
'current_balance' => $endBalance ?? $session->current_balance,
|
|
]);
|
|
|
|
if ($endBalance === null || $playerId === null) {
|
|
return;
|
|
}
|
|
|
|
// Apply balance delta to user (safe even if other transactions happened mid-session)
|
|
$user = User::lockForUpdate()->find((int) $playerId);
|
|
if (!$user) {
|
|
return;
|
|
}
|
|
|
|
$sessionStart = $startBalance ?? (float) $session->start_balance;
|
|
$delta = $endBalance - $sessionStart; // positive = player won, negative = player lost
|
|
|
|
$newBalance = max(0, (float) $user->balance + $delta);
|
|
$user->balance = $newBalance;
|
|
$user->save();
|
|
|
|
Log::info(sprintf(
|
|
'[BetiX] Session ended | player=%s | game=%s | rounds=%d | delta=%+.4f BTX | new_balance=%.4f',
|
|
$playerId, $game ?? '?', $rounds, $delta, $newBalance
|
|
));
|
|
});
|
|
|
|
return response('OK', 200);
|
|
}
|
|
|
|
/**
|
|
* POST /api/betix/round
|
|
*
|
|
* Called by the BetiX game server after each completed round.
|
|
* Updates the user's balance incrementally and logs to game_bets.
|
|
*
|
|
* Payload: { session_token, player_id, new_balance, currency, server_seed_hash, round }
|
|
*/
|
|
public function roundUpdate(Request $request, BetiXClient $client): \Illuminate\Http\Response
|
|
{
|
|
$rawBody = $request->getContent();
|
|
$sig = $request->header('X-Signature', '');
|
|
$secret = config('services.betix.webhook_secret', '');
|
|
|
|
if ($secret !== '' && !$client->verifyWebhook($rawBody, $sig)) {
|
|
Log::warning('[BetiX] Round webhook signature mismatch', ['ip' => $request->ip()]);
|
|
return response('Unauthorized', 401);
|
|
}
|
|
|
|
$payload = json_decode($rawBody, true);
|
|
if (!$payload || !isset($payload['session_token'], $payload['player_id'], $payload['new_balance'])) {
|
|
return response('Bad Request', 400);
|
|
}
|
|
|
|
$token = $payload['session_token'];
|
|
$playerId = (int) $payload['player_id'];
|
|
$newBalance = (float) $payload['new_balance'];
|
|
$seedHash = $payload['server_seed_hash'] ?? null;
|
|
$roundNumber = $payload['round'] ?? null;
|
|
$currency = strtoupper($payload['currency'] ?? 'EUR');
|
|
|
|
$session = OperatorSession::where('session_token', $token)->first();
|
|
|
|
if (!$session || $session->status !== 'active') {
|
|
// Unknown or already-ended session — acknowledge without processing
|
|
return response('OK', 200);
|
|
}
|
|
|
|
DB::transaction(function () use ($session, $playerId, $newBalance, $seedHash, $roundNumber, $currency) {
|
|
$oldSessionBalance = (float) $session->current_balance;
|
|
$delta = $newBalance - $oldSessionBalance; // positive = win, negative = loss
|
|
|
|
// Update session's running balance + optionally rotate the seed hash
|
|
$sessionUpdate = ['current_balance' => $newBalance];
|
|
if ($seedHash) {
|
|
$sessionUpdate['server_seed_hash'] = $seedHash;
|
|
}
|
|
$session->update($sessionUpdate);
|
|
|
|
// Apply balance change to user (row-locked for safety)
|
|
$user = User::lockForUpdate()->find($playerId);
|
|
if (!$user) {
|
|
return;
|
|
}
|
|
|
|
$userBalanceBefore = (float) $user->balance;
|
|
$userBalanceAfter = max(0.0, $userBalanceBefore + $delta);
|
|
$user->balance = $userBalanceAfter;
|
|
$user->save();
|
|
|
|
// Log the round to game_bets (wager = loss side, payout = win side)
|
|
GameBet::create([
|
|
'user_id' => $playerId,
|
|
'game_name' => $session->game_slug,
|
|
'wager_amount' => $delta < 0 ? abs($delta) : 0,
|
|
'payout_amount' => $delta > 0 ? $delta : 0,
|
|
'payout_multiplier'=> 0,
|
|
'currency' => $currency,
|
|
'session_token' => $session->session_token,
|
|
'round_number' => $roundNumber,
|
|
'server_seed_hash' => $seedHash,
|
|
]);
|
|
|
|
Log::info(sprintf(
|
|
'[BetiX] Round update | player=%d | game=%s | round=%s | delta=%+.4f | balance=%.4f→%.4f',
|
|
$playerId,
|
|
$session->game_slug,
|
|
$roundNumber ?? '?',
|
|
$delta,
|
|
$userBalanceBefore,
|
|
$userBalanceAfter
|
|
));
|
|
});
|
|
|
|
return response('OK', 200);
|
|
}
|
|
}
|