403Webshell
Server IP : 202.61.199.114  /  Your IP : 216.73.217.139
Web Server : nginx/1.22.1
System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64
User : web20 ( 1018)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /var/www/clients/client2/web31/web/slots/includes/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/clients/client2/web31/web/slots/includes/betting.php
<?php
/**
 * Reel Royale · Wettmodul – Service-Schicht (BettingService)
 * ---------------------------------------------------------------------------
 *  - Virtuelle Coins, KEIN Echtgeld.
 *  - Quoten werden AUSSCHLIESSLICH serverseitig verwaltet (bet_options.odds).
 *  - Jede Coin-Bewegung wird in coin_transactions protokolliert (Audit).
 *  - Einsätze laufen atomar in einer SQL-Transaktion mit Row-Lock.
 *  - Schutz gegen Doppelabrechnung über bet_markets.status = 'settled'.
 *  - Modular: jede Logik ist turnierunabhängig (bet_tournaments).
 * ---------------------------------------------------------------------------
 *  PHP 7.3 kompatibel.
 */

class BettingException extends RuntimeException {}

class BettingService
{
    /** @var PDO */
    private $pdo;

    /** Markttypen, die automatisch aus einem Ergebnis abgerechnet werden können. */
    const AUTO_TYPES = ['1x2', 'double_chance', 'correct_score', 'over_under', 'btts'];

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    // =======================================================================
    //  Lesen – Turniere / Teams / Spiele / Märkte
    // =======================================================================

    public function tournaments(): array
    {
        return $this->pdo->query(
            'SELECT * FROM bet_tournaments ORDER BY FIELD(status,"active","upcoming","finished","archived"), starts_at'
        )->fetchAll();
    }

    public function activeTournament(): ?array
    {
        $row = $this->pdo->query(
            'SELECT * FROM bet_tournaments WHERE status IN ("active","upcoming")
             ORDER BY FIELD(status,"active","upcoming"), starts_at LIMIT 1'
        )->fetch();
        return $row ?: null;
    }

    public function tournament(int $id): ?array
    {
        $st = $this->pdo->prepare('SELECT * FROM bet_tournaments WHERE id = ? LIMIT 1');
        $st->execute([$id]);
        return $st->fetch() ?: null;
    }

    public function teams(int $tournamentId): array
    {
        $st = $this->pdo->prepare('SELECT * FROM bet_teams WHERE tournament_id = ? ORDER BY group_label, name');
        $st->execute([$tournamentId]);
        return $st->fetchAll();
    }

    public function teamMap(int $tournamentId): array
    {
        $map = [];
        foreach ($this->teams($tournamentId) as $t) {
            $map[(int)$t['id']] = $t;
        }
        return $map;
    }

    /**
     * Spiele eines Turniers nach Filter.
     * @param string $filter  upcoming | live | finished | all
     */
    public function matches(int $tournamentId, string $filter = 'all'): array
    {
        $sql = 'SELECT * FROM bet_matches WHERE tournament_id = ?';
        if ($filter === 'upcoming')      { $sql .= ' AND status = "scheduled"'; }
        elseif ($filter === 'live')      { $sql .= ' AND status = "live"'; }
        elseif ($filter === 'finished')  { $sql .= ' AND status IN ("finished","cancelled")'; }
        $sql .= ' ORDER BY kickoff_at';
        $st = $this->pdo->prepare($sql);
        $st->execute([$tournamentId]);
        return $st->fetchAll();
    }

    public function match(int $id): ?array
    {
        $st = $this->pdo->prepare('SELECT * FROM bet_matches WHERE id = ? LIMIT 1');
        $st->execute([$id]);
        return $st->fetch() ?: null;
    }

    public function market(int $id): ?array
    {
        $st = $this->pdo->prepare('SELECT * FROM bet_markets WHERE id = ? LIMIT 1');
        $st->execute([$id]);
        return $st->fetch() ?: null;
    }

    /** Optionen eines Marktes. */
    public function options(int $marketId): array
    {
        $st = $this->pdo->prepare('SELECT * FROM bet_options WHERE market_id = ? ORDER BY sort, id');
        $st->execute([$marketId]);
        return $st->fetchAll();
    }

    /** Alle Märkte (mit Optionen) zu einem Spiel. */
    public function marketsForMatch(int $matchId): array
    {
        $st = $this->pdo->prepare('SELECT * FROM bet_markets WHERE match_id = ? ORDER BY sort, id');
        $st->execute([$matchId]);
        $markets = $st->fetchAll();
        foreach ($markets as &$m) {
            $m['options'] = $this->options((int)$m['id']);
        }
        return $markets;
    }

    /** Spezialwetten (match_id = NULL) eines Turniers, inkl. Optionen. */
    public function specialMarkets(int $tournamentId, bool $onlyOpen = false): array
    {
        $sql = 'SELECT * FROM bet_markets WHERE tournament_id = ? AND match_id IS NULL';
        if ($onlyOpen) { $sql .= ' AND status = "open"'; }
        $sql .= ' ORDER BY sort, id';
        $st = $this->pdo->prepare($sql);
        $st->execute([$tournamentId]);
        $markets = $st->fetchAll();
        foreach ($markets as &$m) {
            $m['options'] = $this->options((int)$m['id']);
        }
        return $markets;
    }

    /** Spiele eines Turniers mit eingebetteten Märkten/Optionen + Team-Daten. */
    public function matchesWithMarkets(int $tournamentId, string $filter = 'all'): array
    {
        $teams   = $this->teamMap($tournamentId);
        $matches = $this->matches($tournamentId, $filter);
        foreach ($matches as &$mt) {
            $hid = (int)$mt['home_team_id'];
            $aid = (int)$mt['away_team_id'];
            $mt['home']    = $teams[$hid] ?? null;
            $mt['away']    = $teams[$aid] ?? null;
            $mt['markets'] = $this->marketsForMatch((int)$mt['id']);
        }
        return $matches;
    }

    // =======================================================================
    //  Wettschluss / Bettbarkeit
    // =======================================================================

    /** Effektiver Wettschluss eines Marktes (close_at, sonst Anstoß). */
    public function effectiveClose(array $market, ?array $match): ?string
    {
        if (!empty($market['close_at'])) {
            return $market['close_at'];
        }
        if ($match && !empty($match['kickoff_at'])) {
            return $match['kickoff_at'];
        }
        return null;
    }

    /** Kann auf diesen Markt aktuell gewettet werden? */
    public function isBettable(array $market, ?array $match = null): bool
    {
        if ($market['status'] !== 'open') {
            return false;
        }
        if ($match && in_array($match['status'], ['live', 'finished', 'cancelled', 'postponed'], true)) {
            return false;
        }
        $close = $this->effectiveClose($market, $match);
        if ($close !== null && strtotime($close) <= time()) {
            return false;
        }
        return true;
    }

    // =======================================================================
    //  Coin-Buchhaltung (immer innerhalb einer Transaktion + Row-Lock!)
    // =======================================================================

    /**
     * Bucht einen vorzeichenbehafteten Betrag auf das Konto und protokolliert ihn.
     * Erwartet eine offene Transaktion; die users-Zeile sollte zuvor mit
     * FOR UPDATE gesperrt sein. Gibt den neuen Kontostand zurück.
     */
    private function postCoins(int $userId, string $type, float $amount, ?string $refType = null, $refId = null, ?string $note = null): float
    {
        $amount = round($amount, 2);
        $this->pdo->prepare('UPDATE users SET balance = balance + ? WHERE id = ?')
                  ->execute([$amount, $userId]);
        $st = $this->pdo->prepare('SELECT balance FROM users WHERE id = ? LIMIT 1');
        $st->execute([$userId]);
        $balance = round((float)$st->fetchColumn(), 2);
        $this->pdo->prepare(
            'INSERT INTO coin_transactions (user_id, type, amount, balance_after, ref_type, ref_id, note, created_at)
             VALUES (?,?,?,?,?,?,?,NOW())'
        )->execute([$userId, $type, $amount, $balance, $refType, $refId !== null ? (int)$refId : null, $note]);
        return $balance;
    }

    public function ledger(int $userId, int $limit = 50): array
    {
        $st = $this->pdo->prepare(
            'SELECT * FROM coin_transactions WHERE user_id = ? ORDER BY id DESC LIMIT ' . max(1, (int)$limit)
        );
        $st->execute([$userId]);
        return $st->fetchAll();
    }

    // =======================================================================
    //  Wette platzieren  (atomar, serverseitige Quote, Guthabenprüfung)
    // =======================================================================

    public function placeBet(int $userId, int $optionId, float $stake): array
    {
        $stake = round((float)$stake, 2);
        if ($stake <= 0) {
            throw new BettingException('Bitte einen gültigen Einsatz größer 0 angeben.');
        }
        if ($stake > 1000000) {
            throw new BettingException('Der Einsatz ist zu hoch.');
        }

        $pdo = $this->pdo;
        $pdo->beginTransaction();
        try {
            // 1) Nutzer sperren
            $st = $pdo->prepare('SELECT * FROM users WHERE id = ? FOR UPDATE');
            $st->execute([$userId]);
            $user = $st->fetch();
            if (!$user) {
                throw new BettingException('Spieler nicht gefunden.');
            }
            if ((int)($user['banned'] ?? 0) === 1) {
                throw new BettingException('Dein Konto ist gesperrt.');
            }

            // 2) Option + Markt frisch & gesperrt laden (Quote NUR von hier)
            $st = $pdo->prepare(
                'SELECT o.id AS option_id, o.label AS option_label, o.odds, o.status AS option_status,
                        m.id AS market_id, m.match_id, m.tournament_id, m.status AS market_status,
                        m.title AS market_title, m.close_at
                 FROM bet_options o
                 JOIN bet_markets m ON m.id = o.market_id
                 WHERE o.id = ? FOR UPDATE'
            );
            $st->execute([$optionId]);
            $row = $st->fetch();
            if (!$row) {
                throw new BettingException('Diese Wettoption existiert nicht.');
            }
            if ($row['option_status'] !== 'open' || $row['market_status'] !== 'open') {
                throw new BettingException('Diese Wette ist nicht mehr verfügbar.');
            }

            // 3) Wettschluss prüfen
            $match = null;
            if (!empty($row['match_id'])) {
                $ms = $pdo->prepare('SELECT * FROM bet_matches WHERE id = ? LIMIT 1');
                $ms->execute([(int)$row['match_id']]);
                $match = $ms->fetch() ?: null;
            }
            if (!$this->isBettable([
                    'status'   => $row['market_status'],
                    'close_at' => $row['close_at'],
                ], $match)) {
                throw new BettingException('Der Wettschluss für diese Wette ist bereits erreicht.');
            }

            // 4) Guthaben prüfen
            if ((float)$user['balance'] < $stake) {
                throw new BettingException('Nicht genügend Coins für diesen Einsatz.');
            }

            // 5) Quote serverseitig, Einsatz buchen, Wette anlegen
            $odds      = round((float)$row['odds'], 2);
            $potential = round($stake * $odds, 2);

            $ins = $pdo->prepare(
                'INSERT INTO bet_user_bets
                    (user_id, tournament_id, market_id, option_id, match_id, stake, odds, potential_win, status, placed_at)
                 VALUES (?,?,?,?,?,?,?,?,"open",NOW())'
            );
            $ins->execute([
                $userId,
                (int)$row['tournament_id'],
                (int)$row['market_id'],
                (int)$row['option_id'],
                !empty($row['match_id']) ? (int)$row['match_id'] : null,
                $stake, $odds, $potential,
            ]);
            $betId = (int)$pdo->lastInsertId();

            $balance = $this->postCoins($userId, 'bet_stake', -$stake, 'user_bet', $betId,
                'Einsatz: ' . $row['market_title']);

            $pdo->commit();

            return [
                'bet_id'        => $betId,
                'stake'         => $stake,
                'odds'          => $odds,
                'potential_win' => $potential,
                'balance'       => $balance,
                'option_label'  => $row['option_label'],
                'market_title'  => $row['market_title'],
            ];
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            throw ($e instanceof BettingException) ? $e : new BettingException($e->getMessage());
        }
    }

    // =======================================================================
    //  Wetten eines Nutzers
    // =======================================================================

    public function userBets(int $userId, int $tournamentId = 0, int $limit = 200): array
    {
        $sql = 'SELECT b.*, m.title AS market_title, m.type AS market_type, o.label AS option_label,
                       mt.kickoff_at, mt.status AS match_status,
                       th.name AS home_name, th.flag AS home_flag,
                       ta.name AS away_name, ta.flag AS away_flag,
                       mt.home_score, mt.away_score
                FROM bet_user_bets b
                JOIN bet_markets m  ON m.id = b.market_id
                JOIN bet_options o  ON o.id = b.option_id
                LEFT JOIN bet_matches mt ON mt.id = b.match_id
                LEFT JOIN bet_teams th ON th.id = mt.home_team_id
                LEFT JOIN bet_teams ta ON ta.id = mt.away_team_id
                WHERE b.user_id = ?';
        $params = [$userId];
        if ($tournamentId > 0) {
            $sql .= ' AND b.tournament_id = ?';
            $params[] = $tournamentId;
        }
        $sql .= ' ORDER BY b.id DESC LIMIT ' . max(1, (int)$limit);
        $st = $this->pdo->prepare($sql);
        $st->execute($params);
        return $st->fetchAll();
    }

    public function userBetStats(int $userId): array
    {
        $st = $this->pdo->prepare(
            'SELECT
                COUNT(*) AS total,
                SUM(status = "open")  AS open,
                SUM(status = "won")   AS won,
                SUM(status = "lost")  AS lost,
                COALESCE(SUM(stake),0)  AS staked,
                COALESCE(SUM(payout),0) AS payout
             FROM bet_user_bets WHERE user_id = ?'
        );
        $st->execute([$userId]);
        return $st->fetch() ?: [];
    }

    // =======================================================================
    //  Abrechnung (Admin) – mit Schutz gegen Doppelabrechnung
    // =======================================================================

    /**
     * Rechnet einen Markt ab: markiert Gewinn-Optionen, schließt verlorene,
     * zahlt Gewinne aus (stake * odds) und protokolliert alles.
     * @param int[] $winnerOptionIds  IDs der Gewinn-Optionen
     */
    public function settleMarket(int $marketId, array $winnerOptionIds, ?int $adminId = null, ?string $adminName = null): array
    {
        $pdo = $this->pdo;
        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare('SELECT * FROM bet_markets WHERE id = ? FOR UPDATE');
            $st->execute([$marketId]);
            $market = $st->fetch();
            if (!$market) {
                throw new BettingException('Markt nicht gefunden.');
            }
            if ($market['status'] === 'settled') {
                throw new BettingException('Dieser Markt wurde bereits abgerechnet.');
            }
            if ($market['status'] === 'cancelled') {
                throw new BettingException('Dieser Markt wurde storniert.');
            }

            $winnerIds = array_map('intval', $winnerOptionIds);

            // Optionen markieren
            $options = $this->options($marketId);
            foreach ($options as $o) {
                $isWin = in_array((int)$o['id'], $winnerIds, true);
                $pdo->prepare('UPDATE bet_options SET is_winner = ?, status = ? WHERE id = ?')
                    ->execute([$isWin ? 1 : 0, $isWin ? 'won' : 'lost', (int)$o['id']]);
            }

            // Offene Wetten abrechnen
            $bets = $pdo->prepare('SELECT * FROM bet_user_bets WHERE market_id = ? AND status = "open" FOR UPDATE');
            $bets->execute([$marketId]);
            $rows = $bets->fetchAll();

            $wonCount = 0; $lostCount = 0; $paidTotal = 0.0;
            foreach ($rows as $b) {
                $betId = (int)$b['id'];
                if (in_array((int)$b['option_id'], $winnerIds, true)) {
                    $payout = round((float)$b['stake'] * (float)$b['odds'], 2);
                    // Spieler sperren & Gewinn buchen
                    $pdo->prepare('SELECT id FROM users WHERE id = ? FOR UPDATE')->execute([(int)$b['user_id']]);
                    $this->postCoins((int)$b['user_id'], 'bet_payout', $payout, 'user_bet', $betId,
                        'Gewinn: ' . $market['title']);
                    $pdo->prepare('UPDATE bet_user_bets SET status = "won", payout = ?, settled_at = NOW() WHERE id = ?')
                        ->execute([$payout, $betId]);
                    $wonCount++; $paidTotal += $payout;
                } else {
                    $pdo->prepare('UPDATE bet_user_bets SET status = "lost", payout = 0, settled_at = NOW() WHERE id = ?')
                        ->execute([$betId]);
                    $lostCount++;
                }
            }

            $pdo->prepare('UPDATE bet_markets SET status = "settled", settled_at = NOW() WHERE id = ?')
                ->execute([$marketId]);

            $this->logAdmin($adminId, $adminName, 'settle_market', 'market', $marketId, [
                'winners' => $winnerIds, 'won' => $wonCount, 'lost' => $lostCount, 'paid' => $paidTotal,
            ]);

            $pdo->commit();
            return ['won' => $wonCount, 'lost' => $lostCount, 'paid' => round($paidTotal, 2)];
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            throw ($e instanceof BettingException) ? $e : new BettingException($e->getMessage());
        }
    }

    /**
     * Storniert einen Markt: alle OFFENEN Wetten werden erstattet (Einsatz zurück)
     * und auf "cancelled" gesetzt, der Markt wird "cancelled".
     */
    public function cancelMarket(int $marketId, ?int $adminId = null, ?string $adminName = null): array
    {
        $pdo = $this->pdo;
        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare('SELECT * FROM bet_markets WHERE id = ? FOR UPDATE');
            $st->execute([$marketId]);
            $market = $st->fetch();
            if (!$market) {
                throw new BettingException('Markt nicht gefunden.');
            }
            if ($market['status'] === 'cancelled') {
                throw new BettingException('Markt ist bereits storniert.');
            }

            $bets = $pdo->prepare('SELECT * FROM bet_user_bets WHERE market_id = ? AND status = "open" FOR UPDATE');
            $bets->execute([$marketId]);
            $rows = $bets->fetchAll();

            $refunded = 0.0; $count = 0;
            foreach ($rows as $b) {
                $betId = (int)$b['id'];
                $pdo->prepare('SELECT id FROM users WHERE id = ? FOR UPDATE')->execute([(int)$b['user_id']]);
                $this->postCoins((int)$b['user_id'], 'bet_refund', (float)$b['stake'], 'user_bet', $betId,
                    'Erstattung (Storno): ' . $market['title']);
                $pdo->prepare('UPDATE bet_user_bets SET status = "cancelled", payout = ?, settled_at = NOW() WHERE id = ?')
                    ->execute([round((float)$b['stake'], 2), $betId]);
                $refunded += (float)$b['stake']; $count++;
            }

            $pdo->prepare('UPDATE bet_markets SET status = "cancelled", settled_at = NOW() WHERE id = ?')->execute([$marketId]);
            $pdo->prepare('UPDATE bet_options SET status = "void" WHERE market_id = ?')->execute([$marketId]);

            $this->logAdmin($adminId, $adminName, 'cancel_market', 'market', $marketId,
                ['refunded' => $refunded, 'bets' => $count]);

            $pdo->commit();
            return ['refunded' => round($refunded, 2), 'bets' => $count];
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            throw ($e instanceof BettingException) ? $e : new BettingException($e->getMessage());
        }
    }

    /** Einzelne offene Wette stornieren + Einsatz erstatten. */
    public function cancelBet(int $betId, ?int $adminId = null, ?string $adminName = null): array
    {
        $pdo = $this->pdo;
        $pdo->beginTransaction();
        try {
            $st = $pdo->prepare('SELECT * FROM bet_user_bets WHERE id = ? FOR UPDATE');
            $st->execute([$betId]);
            $bet = $st->fetch();
            if (!$bet) {
                throw new BettingException('Wette nicht gefunden.');
            }
            if ($bet['status'] !== 'open') {
                throw new BettingException('Nur offene Wetten können storniert werden.');
            }
            $pdo->prepare('SELECT id FROM users WHERE id = ? FOR UPDATE')->execute([(int)$bet['user_id']]);
            $bal = $this->postCoins((int)$bet['user_id'], 'bet_refund', (float)$bet['stake'], 'user_bet', $betId,
                'Erstattung Einzelwette');
            $pdo->prepare('UPDATE bet_user_bets SET status = "cancelled", payout = ?, settled_at = NOW() WHERE id = ?')
                ->execute([round((float)$bet['stake'], 2), $betId]);
            $this->logAdmin($adminId, $adminName, 'cancel_bet', 'user_bet', $betId, ['stake' => $bet['stake']]);
            $pdo->commit();
            return ['refunded' => round((float)$bet['stake'], 2), 'balance' => $bal];
        } catch (Throwable $e) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            throw ($e instanceof BettingException) ? $e : new BettingException($e->getMessage());
        }
    }

    public function setMarketStatus(int $marketId, string $status, ?int $adminId = null, ?string $adminName = null): void
    {
        if (!in_array($status, ['open', 'suspended', 'closed'], true)) {
            throw new BettingException('Ungültiger Status.');
        }
        $m = $this->market($marketId);
        if (!$m) {
            throw new BettingException('Markt nicht gefunden.');
        }
        if (in_array($m['status'], ['settled', 'cancelled'], true)) {
            throw new BettingException('Abgerechnete oder stornierte Märkte können nicht geändert werden.');
        }
        $this->pdo->prepare('UPDATE bet_markets SET status = ? WHERE id = ?')->execute([$status, $marketId]);
        $this->logAdmin($adminId, $adminName, 'market_status', 'market', $marketId, ['status' => $status]);
    }

    // =======================================================================
    //  Ergebnis erfassen + (optional) automatische Abrechnung
    // =======================================================================

    /**
     * Setzt das Endergebnis eines Spiels und rechnet – falls gewünscht – alle
     * automatisch abrechenbaren Märkte dieses Spiels ab.
     */
    public function setMatchResult(int $matchId, int $homeScore, int $awayScore, bool $autoSettle = true, ?int $adminId = null, ?string $adminName = null): array
    {
        $match = $this->match($matchId);
        if (!$match) {
            throw new BettingException('Spiel nicht gefunden.');
        }
        $homeScore = max(0, $homeScore);
        $awayScore = max(0, $awayScore);

        $this->pdo->prepare('UPDATE bet_matches SET home_score = ?, away_score = ?, status = "finished", updated_at = NOW() WHERE id = ?')
                  ->execute([$homeScore, $awayScore, $matchId]);
        $this->logAdmin($adminId, $adminName, 'match_result', 'match', $matchId,
            ['home' => $homeScore, 'away' => $awayScore, 'auto' => $autoSettle]);

        $settled = [];
        if ($autoSettle) {
            foreach ($this->marketsForMatch($matchId) as $m) {
                if ($m['status'] === 'settled' || $m['status'] === 'cancelled') {
                    continue;
                }
                if (!in_array($m['type'], self::AUTO_TYPES, true)) {
                    continue;
                }
                $winners = $this->resolveMatchMarketWinners($m, $homeScore, $awayScore);
                if ($winners === null) {
                    continue; // Typ nicht auflösbar -> manuell
                }
                $res = $this->settleMarket((int)$m['id'], $winners, $adminId, $adminName);
                $settled[] = ['market_id' => (int)$m['id'], 'title' => $m['title']] + $res;
            }
        }
        return ['match_id' => $matchId, 'settled' => $settled];
    }

    /**
     * Ermittelt die Gewinn-Options-IDs eines Spielmarktes aus dem Ergebnis.
     * Liefert null, wenn der Typ nicht automatisch auflösbar ist.
     * @return int[]|null
     */
    public function resolveMatchMarketWinners(array $market, int $home, int $away): ?array
    {
        $options = $market['options'] ?? $this->options((int)$market['id']);
        $total   = $home + $away;
        $byCode  = [];
        foreach ($options as $o) {
            $byCode[strtolower((string)$o['code'])] = (int)$o['id'];
        }
        $pick = function (array $codes) use ($byCode) {
            $ids = [];
            foreach ($codes as $c) {
                if (isset($byCode[strtolower($c)])) {
                    $ids[] = $byCode[strtolower($c)];
                }
            }
            return $ids;
        };

        switch ($market['type']) {
            case '1x2':
                $code = $home > $away ? '1' : ($home < $away ? '2' : 'x');
                return $pick([$code]);

            case 'double_chance':
                if ($home > $away)      { return $pick(['1x', '12']); }
                elseif ($home < $away)  { return $pick(['12', 'x2']); }
                else                    { return $pick(['1x', 'x2']); }

            case 'btts':
                return $pick([($home > 0 && $away > 0) ? 'yes' : 'no']);

            case 'over_under':
                $line = (float)($market['param'] ?? 2.5);
                return $pick([$total > $line ? 'over' : 'under']);

            case 'correct_score':
                $code = $home . '-' . $away;
                $ids  = $pick([$code]);
                if (!$ids && isset($byCode['other'])) {
                    $ids = [$byCode['other']]; // "Anderes Ergebnis"
                }
                return $ids;
        }
        return null;
    }

    // =======================================================================
    //  Admin-Protokoll
    // =======================================================================

    public function logAdmin(?int $adminId, ?string $adminName, string $action, ?string $refType = null, $refId = null, array $detail = []): void
    {
        $ip = $_SERVER['REMOTE_ADDR'] ?? null;
        $this->pdo->prepare(
            'INSERT INTO bet_admin_log (admin_id, admin_name, action, ref_type, ref_id, detail, ip, created_at)
             VALUES (?,?,?,?,?,?,?,NOW())'
        )->execute([
            $adminId, $adminName, $action, $refType,
            $refId !== null ? (int)$refId : null,
            $detail ? json_encode($detail, JSON_UNESCAPED_UNICODE) : null,
            $ip,
        ]);
    }

    public function adminLog(int $limit = 100): array
    {
        return $this->pdo->query('SELECT * FROM bet_admin_log ORDER BY id DESC LIMIT ' . max(1, (int)$limit))->fetchAll();
    }
}

/** Singleton-Zugriff auf den Wett-Service. */
function betting(): BettingService
{
    static $svc = null;
    if ($svc === null) {
        $svc = new BettingService(db());
    }
    return $svc;
}

Youez - 2016 - github.com/yon3zu
LinuXploit