togethere.cloud/public_html/api/matches/ping-pong/1v1/index.php

326 lines
15 KiB
PHP

<?php
// Rewards endpoint for ping-pong 1v1.
// Called by the Node match server after every match ends.
// Saves the result to match_results, updates user_stats and transactions inline.
require_once __DIR__ . '/internal/respond.php';
require_once __DIR__ . '/internal/env.php';
require_once __DIR__ . '/internal/hmac.php';
require_once __DIR__ . '/../../../administration/includes/config.php';
if (!isset($pdo) || !($pdo instanceof PDO)) {
og_respond(['success' => 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,
]);