Initialer Laravel Commit für BetiX
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
2026-04-04 18:01:50 +02:00
commit 0280278978
374 changed files with 65210 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
<?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);
}
}