false, 'error' => 'Database connection not initialized'], 500); } if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { og_respond(['success' => false, 'error' => 'Method not allowed'], 405); } $secret = og_env('PINGPONG_1V1_SHARED_SECRET'); if (!$secret) { og_respond(['success' => false, 'error' => 'Server not configured (missing PINGPONG_1V1_SHARED_SECRET)'], 500); } $raw = file_get_contents('php://input'); $check = og_require_node_signature($secret, $raw, 60000); if (empty($check['ok'])) { og_respond(['success' => false, 'error' => 'Invalid signature', 'code' => $check['error']], 401); } $payload = json_decode($raw, true); if (!$payload || !is_array($payload)) { og_respond(['success' => false, 'error' => 'Invalid JSON'], 400); } // matchKey is always set by Node (internal ID like "m_abc123") — primary unique key. // matchId may be 0 if MySQL was unavailable when the match was created — that is OK. $matchKey = isset($payload['matchKey']) ? trim((string) $payload['matchKey']) : ''; $matchId = isset($payload['matchId']) ? (int) $payload['matchId'] : 0; $winnerUserId = isset($payload['winnerUserId']) ? (int) $payload['winnerUserId'] : 0; $loserUserId = isset($payload['loserUserId']) ? (int) $payload['loserUserId'] : 0; $winnerUsername = isset($payload['winnerUsername']) ? (string) $payload['winnerUsername'] : ''; $loserUsername = isset($payload['loserUsername']) ? (string) $payload['loserUsername'] : ''; $isDraw = !empty($payload['isDraw']); $leftUserId = isset($payload['players']['left']['userId']) ? (int) $payload['players']['left']['userId'] : 0; $rightUserId = isset($payload['players']['right']['userId']) ? (int) $payload['players']['right']['userId'] : 0; $leftUsername = isset($payload['players']['left']['username']) ? (string) $payload['players']['left']['username'] : ''; $rightUsername = isset($payload['players']['right']['username']) ? (string) $payload['players']['right']['username'] : ''; $score = isset($payload['score']) ? (string) $payload['score'] : ''; $reason = isset($payload['reason']) ? (string) $payload['reason'] : ''; $endedAt = isset($payload['endedAt']) ? (string) $payload['endedAt'] : gmdate('Y-m-d H:i:s'); $setsLeft = (int) ($payload['sets']['left'] ?? 0); $setsRight = (int) ($payload['sets']['right'] ?? 0); if ($matchKey === '') { og_respond(['success' => false, 'error' => 'Missing required field: matchKey'], 400); } if ($isDraw) { if ($leftUserId <= 0 || $rightUserId <= 0) { og_respond(['success' => false, 'error' => 'Missing required draw fields (players.left.userId, players.right.userId)'], 400); } // Keep legacy columns populated in match_results by mapping draw sides to winner/loser fields. if ($winnerUserId <= 0) $winnerUserId = $leftUserId; if ($loserUserId <= 0) $loserUserId = $rightUserId; if ($winnerUsername === '') $winnerUsername = $leftUsername; if ($loserUsername === '') $loserUsername = $rightUsername; } else if ($winnerUserId <= 0 || $loserUserId <= 0) { og_respond(['success' => false, 'error' => 'Missing required fields (matchKey, winnerUserId, loserUserId)'], 400); } // ─── Ensure tables exist ────────────────────────────────────────────────────── $pdo->exec("CREATE TABLE IF NOT EXISTS match_results ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, match_key VARCHAR(100) NOT NULL, match_id BIGINT UNSIGNED NULL, discipline VARCHAR(50) NOT NULL DEFAULT 'ping-pong', mode VARCHAR(50) NOT NULL DEFAULT '1v1', winner_user_id BIGINT UNSIGNED NOT NULL, loser_user_id BIGINT UNSIGNED NOT NULL, winner_username VARCHAR(100) NOT NULL DEFAULT '', loser_username VARCHAR(100) NOT NULL DEFAULT '', score VARCHAR(200) NOT NULL DEFAULT '', sets_winner TINYINT NOT NULL DEFAULT 0, sets_loser TINYINT NOT NULL DEFAULT 0, reason VARCHAR(50) NOT NULL DEFAULT '', ended_at DATETIME NULL, payload_json LONGTEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uniq_match_key (discipline, mode, match_key), INDEX idx_winner (winner_user_id), INDEX idx_loser (loser_user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS rewards_jobs ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, discipline VARCHAR(50) NOT NULL, mode VARCHAR(50) NOT NULL, match_key VARCHAR(100) NOT NULL DEFAULT '', match_id BIGINT UNSIGNED NOT NULL DEFAULT 0, payload_json LONGTEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'queued', attempts INT NOT NULL DEFAULT 0, result_json LONGTEXT NULL, last_error TEXT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uniq_match_key (discipline, mode, match_key), INDEX idx_status_created (status, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); // If rewards_jobs was created before this version, add match_key column if missing $mkExists = (int) $pdo->query( "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'rewards_jobs' AND column_name = 'match_key'" )->fetchColumn(); if (!$mkExists) { $pdo->exec("ALTER TABLE rewards_jobs ADD COLUMN match_key VARCHAR(100) NOT NULL DEFAULT '' AFTER mode"); // Try to add unique index; ignore error if it already exists under another name try { $pdo->exec("ALTER TABLE rewards_jobs ADD UNIQUE KEY uniq_match_key (discipline, mode, match_key)"); } catch (Throwable $ignored) {} } $pdo->exec("CREATE TABLE IF NOT EXISTS transactions ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, type VARCHAR(20) NOT NULL, amount DECIMAL(12,2) NOT NULL, title VARCHAR(255) NOT NULL, description TEXT NULL, category VARCHAR(50) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_created (user_id, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); // ─── Enqueue rewards job (idempotent by match_key) ──────────────────────────── $pdo->prepare( "INSERT IGNORE INTO rewards_jobs (discipline, mode, match_key, match_id, payload_json, status) VALUES ('ping-pong', '1v1', :mk, :mid, :pj, 'queued')" )->execute([ ':mk' => $matchKey, ':mid' => $matchId, ':pj' => json_encode($payload, JSON_UNESCAPED_UNICODE), ]); $jobRow = $pdo->prepare( "SELECT id, status FROM rewards_jobs WHERE discipline = 'ping-pong' AND mode = '1v1' AND match_key = :mk" ); $jobRow->execute([':mk' => $matchKey]); $job = $jobRow->fetch(PDO::FETCH_ASSOC); if (!$job) { og_respond(['success' => false, 'error' => 'Failed to enqueue rewards job'], 500); } $jobId = (int) $job['id']; // ─── Process inline (no cron required) ─────────────────────────────────────── if ($job['status'] === 'queued') { $claim = $pdo->prepare( "UPDATE rewards_jobs SET status = 'processing', attempts = attempts + 1 WHERE id = :id AND status = 'queued'" ); $claim->execute([':id' => $jobId]); if ($claim->rowCount() > 0) { $winnerReward = 1.00; $loserReward = 0.20; $drawRefund = 1.00; // Determine sets per side: winner always has more sets $winnerSets = max($setsLeft, $setsRight); $loserSets = min($setsLeft, $setsRight); $matchLabel = $matchId > 0 ? 'Mecz #' . $matchId : 'Mecz ' . $matchKey; try { // Ensure both players have a user_stats row $insStats = $pdo->prepare( "INSERT IGNORE INTO user_stats (user_id, balance, matches_played, matches_won, matches_lost, matches_draw, tournaments_played, tournaments_won, leagues_participated, total_income, total_expenses, total_transactions, account_status) VALUES (?, 0.00, 0, 0, 0, 0, 0, 0, 0, 0.00, 0.00, 0, 'active')" ); $insStats->execute([$winnerUserId]); $insStats->execute([$loserUserId]); if ($isDraw) { $insStats->execute([$leftUserId]); $insStats->execute([$rightUserId]); } $pdo->beginTransaction(); // 1. Save match result record $pdo->prepare( "INSERT IGNORE INTO match_results (match_key, match_id, discipline, mode, winner_user_id, loser_user_id, winner_username, loser_username, score, sets_winner, sets_loser, reason, ended_at, payload_json) VALUES (?, ?, 'ping-pong', '1v1', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" )->execute([ $matchKey, $matchId > 0 ? $matchId : null, $winnerUserId, $loserUserId, $winnerUsername, $loserUsername, $score, $winnerSets, $loserSets, $reason, $endedAt, json_encode($payload, JSON_UNESCAPED_UNICODE), ]); // 2/3. Update stats if ($isDraw) { $updDraw = $pdo->prepare( "UPDATE user_stats SET matches_played = matches_played + 1, matches_draw = matches_draw + 1, balance = balance + ?, total_income = total_income + ?, total_transactions = total_transactions + 1 WHERE user_id = ?" ); $updDraw->execute([$drawRefund, $drawRefund, $leftUserId]); $updDraw->execute([$drawRefund, $drawRefund, $rightUserId]); } else { $pdo->prepare( "UPDATE user_stats SET matches_played = matches_played + 1, matches_won = matches_won + 1, balance = balance + ?, total_income = total_income + ?, total_transactions = total_transactions + 1 WHERE user_id = ?" )->execute([$winnerReward, $winnerReward, $winnerUserId]); $pdo->prepare( "UPDATE user_stats SET matches_played = matches_played + 1, matches_lost = matches_lost + 1, balance = balance + ?, total_income = total_income + ?, total_transactions = total_transactions + 1 WHERE user_id = ?" )->execute([$loserReward, $loserReward, $loserUserId]); } // 4. Insert transaction records $txStmt = $pdo->prepare( "INSERT INTO transactions (user_id, type, amount, title, description, category) VALUES (?, 'income', ?, ?, ?, 'match')" ); if ($isDraw) { $txStmt->execute([ $leftUserId, $drawRefund, 'Ping-Pong 1v1 - remis (zwrot stawki)', $matchLabel . ' | ' . $score, ]); $txStmt->execute([ $rightUserId, $drawRefund, 'Ping-Pong 1v1 - remis (zwrot stawki)', $matchLabel . ' | ' . $score, ]); } else { $txStmt->execute([ $winnerUserId, $winnerReward, 'Ping-Pong 1v1 - wygrana', $matchLabel . ' | ' . $score, ]); $txStmt->execute([ $loserUserId, $loserReward, 'Ping-Pong 1v1 - udział', $matchLabel . ' | ' . $score, ]); } // 5. Mark job done $result = $isDraw ? [ 'draw' => [ 'left' => ['userId' => $leftUserId, 'username' => $leftUsername, 'reward' => (float) $drawRefund], 'right' => ['userId' => $rightUserId, 'username' => $rightUsername, 'reward' => (float) $drawRefund], ], 'animation' => ['type' => 'coins', 'durationMs' => 2500], 'match' => ['matchKey' => $matchKey, 'matchId' => $matchId, 'score' => $score], ] : [ 'winner' => ['userId' => $winnerUserId, 'username' => $winnerUsername, 'reward' => (float) $winnerReward], 'loser' => ['userId' => $loserUserId, 'username' => $loserUsername, 'reward' => (float) $loserReward], 'animation' => ['type' => 'coins', 'durationMs' => 2500], 'match' => ['matchKey' => $matchKey, 'matchId' => $matchId, 'score' => $score], ]; $pdo->prepare( "UPDATE rewards_jobs SET status = 'done', result_json = :res, last_error = NULL WHERE id = :id" )->execute([':id' => $jobId, ':res' => json_encode($result, JSON_UNESCAPED_UNICODE)]); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); $pdo->prepare( "UPDATE rewards_jobs SET status = 'queued', last_error = :err WHERE id = :id" )->execute([':id' => $jobId, ':err' => $e->getMessage()]); } } } // Return final status $finalStatus = $pdo->prepare("SELECT status FROM rewards_jobs WHERE id = :id"); $finalStatus->execute([':id' => $jobId]); $statusVal = $finalStatus->fetchColumn() ?: 'queued'; og_respond([ 'success' => true, 'jobId' => $jobId, 'status' => $statusVal, ]);