326 lines
15 KiB
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,
|
|
]);
|