Merge pull request #131 from GameServerPanel/copilot/update-paypal-configuration
This commit is contained in:
commit
19b32af973
9 changed files with 1354 additions and 109 deletions
|
|
@ -108,11 +108,22 @@ function billing_admin_build_config(string $existingContent, array $vals): strin
|
|||
return '"' . addslashes($v) . '"';
|
||||
};
|
||||
|
||||
$sandbox = (bool)$vals['paypal_sandbox'];
|
||||
$retention = max(1, min(10, (int)($vals['backup_retention'] ?? 5)));
|
||||
$baseUrl = trim($vals['SITE_BASE_URL'] ?? '');
|
||||
$bg = trim($vals['SITE_BACKGROUND'] ?? 'images/dark.jpg');
|
||||
$dataDir = trim($vals['SITE_DATA_DIR'] ?? '');
|
||||
$mode = (strtolower($vals['paypal_mode'] ?? 'sandbox') === 'live') ? 'live' : 'sandbox';
|
||||
$retention = max(1, min(10, (int)($vals['backup_retention'] ?? 5)));
|
||||
$baseUrl = rtrim(trim($vals['SITE_BASE_URL'] ?? ''), '/');
|
||||
$bg = trim($vals['SITE_BACKGROUND'] ?? 'images/dark.jpg');
|
||||
$dataDir = trim($vals['SITE_DATA_DIR'] ?? '');
|
||||
$wh_path = '/' . ltrim(trim($vals['paypal_webhook_path'] ?? '/paypal/webhook.php'), '/');
|
||||
|
||||
// Sandbox credentials — never erase existing secret if field was left blank
|
||||
$sb_id = trim($vals['paypal_sandbox_client_id'] ?? '');
|
||||
$sb_sec = trim($vals['paypal_sandbox_client_secret'] ?? '');
|
||||
$sb_wh = trim($vals['paypal_sandbox_webhook_id'] ?? '');
|
||||
|
||||
// Live credentials — never erase existing secret if field was left blank
|
||||
$lv_id = trim($vals['paypal_live_client_id'] ?? '');
|
||||
$lv_sec = trim($vals['paypal_live_client_secret'] ?? '');
|
||||
$lv_wh = trim($vals['paypal_live_webhook_id'] ?? '');
|
||||
|
||||
$dbBlock = '';
|
||||
foreach (['db_host', 'db_port', 'db_user', 'db_pass', 'db_name', 'table_prefix', 'db_type'] as $var) {
|
||||
|
|
@ -135,26 +146,36 @@ function billing_admin_build_config(string $existingContent, array $vals): strin
|
|||
. '###############################################' . "\n"
|
||||
. $dbBlock
|
||||
. "\n"
|
||||
. '// Optional: base URL used by admin pages to build absolute image previews.' . "\n"
|
||||
. '// Leave empty to prefer relative paths (local folder).' . "\n"
|
||||
. '// Optional: base URL without trailing slash (e.g. https://gameservers.world).' . "\n"
|
||||
. '// Leave empty to use relative paths.' . "\n"
|
||||
. '$SITE_BASE_URL = ' . $q($baseUrl) . ';' . "\n"
|
||||
. "\n"
|
||||
. '// Normalize: ensure either empty or ends without trailing slash' . "\n"
|
||||
. '$SITE_BASE_URL = trim((string)$SITE_BASE_URL);' . "\n"
|
||||
. '$SITE_BASE_URL = rtrim(trim((string)$SITE_BASE_URL), \'/\');' . "\n"
|
||||
. "\n"
|
||||
. '// Site-wide background image (relative to site root).' . "\n"
|
||||
. '$SITE_BACKGROUND = ' . $q($bg) . ';' . "\n"
|
||||
. '// Normalize' . "\n"
|
||||
. '$SITE_BACKGROUND = trim((string)$SITE_BACKGROUND);' . "\n"
|
||||
. "\n"
|
||||
. '// Data directory for persisted payment webhook JSON files (relative to repo root)' . "\n"
|
||||
. '// Data directory for persisted payment webhook JSON files.' . "\n"
|
||||
. $dataDirLine . "\n"
|
||||
. "\n"
|
||||
. '// PayPal configuration — set credentials here, never in API files' . "\n"
|
||||
. '$paypal_sandbox = ' . ($sandbox ? 'true' : 'false') . '; // Set to false for live payments' . "\n"
|
||||
. '$paypal_client_id = ' . $q($vals['paypal_client_id'] ?? '') . '; // Your PayPal Client ID' . "\n"
|
||||
. '$paypal_client_secret = ' . $q($vals['paypal_client_secret'] ?? '') . '; // Your PayPal Client Secret' . "\n"
|
||||
. '$paypal_webhook_id = ' . $q($vals['paypal_webhook_id'] ?? '') . '; // Your PayPal Webhook ID' . "\n"
|
||||
. '// ---------------------------------------------------------------------------' . "\n"
|
||||
. '// PayPal configuration' . "\n"
|
||||
. '// ---------------------------------------------------------------------------' . "\n"
|
||||
. '$paypal_mode = ' . $q($mode) . '; // \'sandbox\' or \'live\'' . "\n"
|
||||
. "\n"
|
||||
. '// Sandbox credentials (PayPal Developer Dashboard → sandbox app)' . "\n"
|
||||
. '$paypal_sandbox_client_id = ' . $q($sb_id) . ';' . "\n"
|
||||
. '$paypal_sandbox_client_secret = ' . $q($sb_sec) . ';' . "\n"
|
||||
. '$paypal_sandbox_webhook_id = ' . $q($sb_wh) . ';' . "\n"
|
||||
. "\n"
|
||||
. '// Live credentials (leave blank until ready for production)' . "\n"
|
||||
. '$paypal_live_client_id = ' . $q($lv_id) . ';' . "\n"
|
||||
. '$paypal_live_client_secret = ' . $q($lv_sec) . ';' . "\n"
|
||||
. '$paypal_live_webhook_id = ' . $q($lv_wh) . ';' . "\n"
|
||||
. "\n"
|
||||
. '// Webhook path (relative to billing site root, must start with /)' . "\n"
|
||||
. '// Full public URL = $SITE_BASE_URL + $paypal_webhook_path' . "\n"
|
||||
. '$paypal_webhook_path = ' . $q($wh_path) . ';' . "\n"
|
||||
. "\n"
|
||||
. '// Admin config backup retention: how many backups to keep (1–10). Default 5.' . "\n"
|
||||
. '$SITE_CONFIG_BACKUP_RETENTION = ' . $retention . ';' . "\n"
|
||||
|
|
@ -165,16 +186,25 @@ function billing_admin_build_config(string $existingContent, array $vals): strin
|
|||
// Read current values from config (already loaded by config_loader above).
|
||||
// ---------------------------------------------------------------------------
|
||||
$cfgVals = [
|
||||
'SITE_BASE_URL' => $SITE_BASE_URL ?? '',
|
||||
'SITE_BACKGROUND' => $SITE_BACKGROUND ?? 'images/dark.jpg',
|
||||
'SITE_DATA_DIR' => isset($SITE_DATA_DIR) ? $SITE_DATA_DIR : '',
|
||||
'paypal_sandbox' => $paypal_sandbox ?? true,
|
||||
'paypal_client_id' => $paypal_client_id ?? '',
|
||||
'paypal_client_secret' => $paypal_client_secret ?? '',
|
||||
'paypal_webhook_id' => $paypal_webhook_id ?? '',
|
||||
'backup_retention' => $SITE_CONFIG_BACKUP_RETENTION ?? 5,
|
||||
'SITE_BASE_URL' => $SITE_BASE_URL ?? '',
|
||||
'SITE_BACKGROUND' => $SITE_BACKGROUND ?? 'images/dark.jpg',
|
||||
'SITE_DATA_DIR' => $SITE_DATA_DIR ?? '',
|
||||
'paypal_mode' => $paypal_mode ?? 'sandbox',
|
||||
'paypal_sandbox_client_id' => $paypal_sandbox_client_id ?? '',
|
||||
'paypal_sandbox_client_secret' => $paypal_sandbox_client_secret ?? '',
|
||||
'paypal_sandbox_webhook_id' => $paypal_sandbox_webhook_id ?? '',
|
||||
'paypal_live_client_id' => $paypal_live_client_id ?? '',
|
||||
'paypal_live_client_secret' => $paypal_live_client_secret ?? '',
|
||||
'paypal_live_webhook_id' => $paypal_live_webhook_id ?? '',
|
||||
'paypal_webhook_path' => $paypal_webhook_path ?? '/paypal/webhook.php',
|
||||
'backup_retention' => $SITE_CONFIG_BACKUP_RETENTION ?? 5,
|
||||
];
|
||||
|
||||
// Computed full webhook URL for display
|
||||
$computedWebhookUrl = function_exists('gsp_paypal_get_full_webhook_url')
|
||||
? gsp_paypal_get_full_webhook_url()
|
||||
: rtrim($cfgVals['SITE_BASE_URL'], '/') . $cfgVals['paypal_webhook_path'];
|
||||
|
||||
// Detect panel-mode (DB settings are managed by the panel)
|
||||
$panelMode = defined('BILLING_PANEL_CONFIG_PATH');
|
||||
$panelCfgPath = $panelMode ? BILLING_PANEL_CONFIG_PATH : null;
|
||||
|
|
@ -196,16 +226,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_
|
|||
} else {
|
||||
// Collect and validate form values
|
||||
$formVals = [
|
||||
'SITE_BASE_URL' => trim($_POST['SITE_BASE_URL'] ?? ''),
|
||||
'SITE_BACKGROUND' => trim($_POST['SITE_BACKGROUND'] ?? 'images/dark.jpg'),
|
||||
'SITE_DATA_DIR' => trim($_POST['SITE_DATA_DIR'] ?? ''),
|
||||
'paypal_sandbox' => (($_POST['paypal_sandbox'] ?? 'true') === 'true'),
|
||||
'paypal_client_id' => trim($_POST['paypal_client_id'] ?? ''),
|
||||
'paypal_client_secret' => trim($_POST['paypal_client_secret'] ?? ''),
|
||||
'paypal_webhook_id' => trim($_POST['paypal_webhook_id'] ?? ''),
|
||||
'backup_retention' => (int)($_POST['backup_retention'] ?? 5),
|
||||
'SITE_BASE_URL' => trim($_POST['SITE_BASE_URL'] ?? ''),
|
||||
'SITE_BACKGROUND' => trim($_POST['SITE_BACKGROUND'] ?? 'images/dark.jpg'),
|
||||
'SITE_DATA_DIR' => trim($_POST['SITE_DATA_DIR'] ?? ''),
|
||||
'paypal_mode' => (strtolower(trim($_POST['paypal_mode'] ?? 'sandbox')) === 'live') ? 'live' : 'sandbox',
|
||||
'paypal_sandbox_client_id' => trim($_POST['paypal_sandbox_client_id'] ?? ''),
|
||||
'paypal_live_client_id' => trim($_POST['paypal_live_client_id'] ?? ''),
|
||||
'paypal_sandbox_webhook_id' => trim($_POST['paypal_sandbox_webhook_id'] ?? ''),
|
||||
'paypal_live_webhook_id' => trim($_POST['paypal_live_webhook_id'] ?? ''),
|
||||
'paypal_webhook_path' => trim($_POST['paypal_webhook_path'] ?? '/paypal/webhook.php'),
|
||||
'backup_retention' => (int)($_POST['backup_retention'] ?? 5),
|
||||
];
|
||||
|
||||
// Client secrets: only update if a non-blank value was submitted (never erase existing).
|
||||
$sbSecPost = trim($_POST['paypal_sandbox_client_secret'] ?? '');
|
||||
$formVals['paypal_sandbox_client_secret'] = ($sbSecPost !== '') ? $sbSecPost : ($cfgVals['paypal_sandbox_client_secret'] ?? '');
|
||||
|
||||
$lvSecPost = trim($_POST['paypal_live_client_secret'] ?? '');
|
||||
$formVals['paypal_live_client_secret'] = ($lvSecPost !== '') ? $lvSecPost : ($cfgVals['paypal_live_client_secret'] ?? '');
|
||||
|
||||
// Validate
|
||||
$validationError = '';
|
||||
if ($formVals['backup_retention'] < 1 || $formVals['backup_retention'] > 10) {
|
||||
|
|
@ -243,7 +282,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_
|
|||
$retention = max(1, min(10, $formVals['backup_retention']));
|
||||
billing_admin_apply_retention($bakDir, $retention);
|
||||
|
||||
$cfgVals = $formVals; // update displayed values
|
||||
$cfgVals = $formVals; // update displayed values
|
||||
$computedWebhookUrl = rtrim($formVals['SITE_BASE_URL'], '/') . ('/' . ltrim($formVals['paypal_webhook_path'] ?? '/paypal/webhook.php', '/'));
|
||||
$status = 'Config saved successfully. Backup: ' . basename($bakName);
|
||||
$statusType = 'success';
|
||||
}
|
||||
|
|
@ -398,8 +438,8 @@ rsort($bakFiles); // newest first
|
|||
<div class="field-group">
|
||||
<label for="cfg_base_url">Site Base URL</label>
|
||||
<div class="field-help">
|
||||
Full base URL without trailing slash (e.g. <code>https://gameservers.world</code>).
|
||||
Leave empty to use relative paths.
|
||||
Full base URL <strong>without trailing slash</strong> (e.g. <code>https://gameservers.world</code>).
|
||||
Leave empty to use relative paths. Used to compute the full public PayPal webhook URL.
|
||||
</div>
|
||||
<input type="text" id="cfg_base_url" name="SITE_BASE_URL"
|
||||
value="<?php echo h((string)$cfgVals['SITE_BASE_URL']); ?>"
|
||||
|
|
@ -432,59 +472,130 @@ rsort($bakFiles); // newest first
|
|||
<hr style="border:none;border-top:1px solid #eee;margin:24px 0;">
|
||||
<h3 style="margin-top:0;color:#333;">PayPal Configuration</h3>
|
||||
|
||||
<!-- PayPal Sandbox -->
|
||||
<?php
|
||||
$isSandboxMode = ($cfgVals['paypal_mode'] ?? 'sandbox') !== 'live';
|
||||
$modeLabel = $isSandboxMode ? '🟡 Sandbox (test mode)' : '🟢 Live (real payments)';
|
||||
$modeBadgeClass = $isSandboxMode ? 'status-info' : 'status-success';
|
||||
?>
|
||||
<div class="status-box <?php echo h($modeBadgeClass); ?>" style="margin-bottom:14px;font-size:0.95em;">
|
||||
Currently active PayPal mode: <strong><?php echo h($modeLabel); ?></strong>
|
||||
</div>
|
||||
|
||||
<!-- PayPal Mode -->
|
||||
<div class="field-group">
|
||||
<label for="cfg_sandbox">PayPal Mode</label>
|
||||
<label for="cfg_mode">PayPal Mode</label>
|
||||
<div class="field-help">
|
||||
Use <strong>Sandbox</strong> for testing, <strong>Live</strong> for real payments.
|
||||
Make sure you use the matching Client ID and Secret for the selected mode.
|
||||
<strong>Sandbox</strong> uses test credentials and the PayPal sandbox API — safe for development.
|
||||
<strong>Live</strong> processes real payments. Switch only after configuring live credentials.
|
||||
</div>
|
||||
<select id="cfg_sandbox" name="paypal_sandbox">
|
||||
<option value="true" <?php echo $cfgVals['paypal_sandbox'] ? 'selected' : ''; ?>>Sandbox (test mode)</option>
|
||||
<option value="false" <?php echo !$cfgVals['paypal_sandbox'] ? 'selected' : ''; ?>>Live (real payments)</option>
|
||||
<select id="cfg_mode" name="paypal_mode">
|
||||
<option value="sandbox" <?php echo $isSandboxMode ? 'selected' : ''; ?>>Sandbox (test mode)</option>
|
||||
<option value="live" <?php echo !$isSandboxMode ? 'selected' : ''; ?>>Live (real payments)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- PayPal Client ID -->
|
||||
<!-- Sandbox credentials -->
|
||||
<h4 style="color:#555;margin:20px 0 8px;">Sandbox Credentials</h4>
|
||||
<div class="field-group">
|
||||
<label for="cfg_cid">PayPal Client ID</label>
|
||||
<div class="field-help">
|
||||
Your PayPal app Client ID. Safe to expose in browser JS.
|
||||
Found in your PayPal Developer Dashboard under your app credentials.
|
||||
</div>
|
||||
<input type="text" id="cfg_cid" name="paypal_client_id"
|
||||
value="<?php echo h((string)$cfgVals['paypal_client_id']); ?>"
|
||||
placeholder="AY... or AZ...">
|
||||
<label for="cfg_sb_id">Sandbox Client ID</label>
|
||||
<div class="field-help">Found in PayPal Developer Dashboard → sandbox app. Safe to expose in browser JS.</div>
|
||||
<input type="text" id="cfg_sb_id" name="paypal_sandbox_client_id"
|
||||
value="<?php echo h((string)$cfgVals['paypal_sandbox_client_id']); ?>"
|
||||
placeholder="AfvY_... or sandbox client ID">
|
||||
</div>
|
||||
|
||||
<!-- PayPal Client Secret -->
|
||||
<div class="field-group">
|
||||
<label for="cfg_csecret">PayPal Client Secret</label>
|
||||
<div class="field-help">
|
||||
Your PayPal app Client Secret. <strong>Server-side only</strong> — never sent to the browser.
|
||||
</div>
|
||||
<label for="cfg_sb_sec">Sandbox Client Secret</label>
|
||||
<div class="field-help"><strong>Server-side only</strong> — never sent to the browser. Leave blank to keep existing value.</div>
|
||||
<div class="pw-wrap">
|
||||
<input type="password" id="cfg_csecret" name="paypal_client_secret"
|
||||
value="<?php echo h((string)$cfgVals['paypal_client_secret']); ?>"
|
||||
<input type="password" id="cfg_sb_sec" name="paypal_sandbox_client_secret"
|
||||
placeholder="<?php echo $cfgVals['paypal_sandbox_client_secret'] !== '' ? '(set — leave blank to keep)' : '(not set)'; ?>"
|
||||
autocomplete="new-password">
|
||||
<button type="button" class="btn-show"
|
||||
onclick="var f=document.getElementById('cfg_csecret');f.type=f.type==='password'?'text':'password';this.textContent=f.type==='password'?'Show':'Hide';">
|
||||
Show
|
||||
</button>
|
||||
onclick="var f=document.getElementById('cfg_sb_sec');f.type=f.type==='password'?'text':'password';this.textContent=f.type==='password'?'Show':'Hide';">Show</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="cfg_sb_wh">Sandbox Webhook ID</label>
|
||||
<div class="field-help">
|
||||
Webhook ID from your PayPal sandbox app (for signature verification).
|
||||
Leave empty to skip verification in sandbox mode (OK for initial setup).
|
||||
</div>
|
||||
<input type="text" id="cfg_sb_wh" name="paypal_sandbox_webhook_id"
|
||||
value="<?php echo h((string)$cfgVals['paypal_sandbox_webhook_id']); ?>"
|
||||
placeholder="Sandbox Webhook ID">
|
||||
</div>
|
||||
|
||||
<!-- PayPal Webhook ID -->
|
||||
<!-- Live credentials -->
|
||||
<h4 style="color:#555;margin:20px 0 8px;">Live Credentials</h4>
|
||||
<div class="field-group">
|
||||
<label for="cfg_wh">PayPal Webhook ID</label>
|
||||
<div class="field-help">
|
||||
Webhook ID from your PayPal app (used for webhook signature verification).
|
||||
Leave empty to skip signature verification (not recommended for production).
|
||||
</div>
|
||||
<input type="text" id="cfg_wh" name="paypal_webhook_id"
|
||||
value="<?php echo h((string)$cfgVals['paypal_webhook_id']); ?>"
|
||||
placeholder="Webhook ID">
|
||||
<label for="cfg_lv_id">Live Client ID</label>
|
||||
<div class="field-help">From your PayPal live app. Leave blank until ready for production.</div>
|
||||
<input type="text" id="cfg_lv_id" name="paypal_live_client_id"
|
||||
value="<?php echo h((string)$cfgVals['paypal_live_client_id']); ?>"
|
||||
placeholder="Live Client ID">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="cfg_lv_sec">Live Client Secret</label>
|
||||
<div class="field-help"><strong>Server-side only.</strong> Leave blank to keep existing value.</div>
|
||||
<div class="pw-wrap">
|
||||
<input type="password" id="cfg_lv_sec" name="paypal_live_client_secret"
|
||||
placeholder="<?php echo $cfgVals['paypal_live_client_secret'] !== '' ? '(set — leave blank to keep)' : '(not set)'; ?>"
|
||||
autocomplete="new-password">
|
||||
<button type="button" class="btn-show"
|
||||
onclick="var f=document.getElementById('cfg_lv_sec');f.type=f.type==='password'?'text':'password';this.textContent=f.type==='password'?'Show':'Hide';">Show</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="cfg_lv_wh">Live Webhook ID</label>
|
||||
<div class="field-help">Webhook ID from your PayPal live app (for signature verification).</div>
|
||||
<input type="text" id="cfg_lv_wh" name="paypal_live_webhook_id"
|
||||
value="<?php echo h((string)$cfgVals['paypal_live_webhook_id']); ?>"
|
||||
placeholder="Live Webhook ID">
|
||||
</div>
|
||||
|
||||
<!-- Webhook path + computed URL -->
|
||||
<h4 style="color:#555;margin:20px 0 8px;">Webhook Endpoint</h4>
|
||||
<div class="field-help" style="margin-bottom:10px;">
|
||||
PayPal requires a <strong>full public HTTPS URL</strong> to deliver webhook events.
|
||||
Set your Site Base URL above, then copy the computed URL below into your PayPal app's webhook configuration.
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label for="cfg_wh_path">Webhook Path</label>
|
||||
<div class="field-help">Path relative to the billing site root (must start with <code>/</code>). Default: <code>/paypal/webhook.php</code></div>
|
||||
<input type="text" id="cfg_wh_path" name="paypal_webhook_path"
|
||||
value="<?php echo h((string)$cfgVals['paypal_webhook_path']); ?>"
|
||||
placeholder="/paypal/webhook.php"
|
||||
oninput="updateWebhookUrl()">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label>Computed Full Webhook URL <small style="font-weight:normal;color:#888;">(read-only — paste this into PayPal)</small></label>
|
||||
<div class="field-help">
|
||||
This is the URL PayPal will POST webhook events to.
|
||||
It must be publicly accessible over HTTPS before enabling live mode.
|
||||
</div>
|
||||
<input type="text" id="computed_webhook_url"
|
||||
class="readonly-field"
|
||||
value="<?php echo h($computedWebhookUrl); ?>"
|
||||
readonly
|
||||
style="font-family:monospace;color:#333;background:#f0f4ff;">
|
||||
<button type="button" id="copy_webhook_url_btn" class="btn-show" style="margin-top:4px;"
|
||||
onclick="var u=document.getElementById('computed_webhook_url');if(u){navigator.clipboard.writeText(u.value).then(function(){var b=document.getElementById('copy_webhook_url_btn');b.textContent='Copied!';setTimeout(function(){b.textContent='Copy';},2000);});}">Copy</button>
|
||||
</div>
|
||||
<script>
|
||||
function updateWebhookUrl() {
|
||||
var base = document.getElementById('cfg_base_url');
|
||||
var path = document.getElementById('cfg_wh_path');
|
||||
var out = document.getElementById('computed_webhook_url');
|
||||
if (!base || !path || !out) return;
|
||||
var b = base.value.replace(/\/+$/, '');
|
||||
var p = path.value.replace(/^([^\/])/, '/$1');
|
||||
out.value = b + p;
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var base = document.getElementById('cfg_base_url');
|
||||
if (base) base.addEventListener('input', updateWebhookUrl);
|
||||
});
|
||||
</script>
|
||||
|
||||
<hr style="border:none;border-top:1px solid #eee;margin:24px 0;">
|
||||
<h3 style="margin-top:0;color:#333;">Backup Settings</h3>
|
||||
|
|
@ -509,7 +620,94 @@ rsort($bakFiles); // newest first
|
|||
</div>
|
||||
|
||||
<!-- ===================================================================
|
||||
SECTION B: Raw PHP editor
|
||||
SECTION B: PayPal Diagnostics
|
||||
==================================================================== -->
|
||||
<?php
|
||||
// Gather diagnostics data
|
||||
$diag_mode = $cfgVals['paypal_mode'] ?? 'sandbox';
|
||||
$diag_is_sandbox = $diag_mode !== 'live';
|
||||
$diag_sb_id_set = $cfgVals['paypal_sandbox_client_id'] !== '';
|
||||
$diag_sb_sec_set = $cfgVals['paypal_sandbox_client_secret'] !== '';
|
||||
$diag_sb_wh_set = $cfgVals['paypal_sandbox_webhook_id'] !== '';
|
||||
$diag_lv_id_set = $cfgVals['paypal_live_client_id'] !== '';
|
||||
$diag_lv_sec_set = $cfgVals['paypal_live_client_secret'] !== '';
|
||||
$diag_lv_wh_set = $cfgVals['paypal_live_webhook_id'] !== '';
|
||||
$diag_wh_path = $cfgVals['paypal_webhook_path'] ?? '/paypal/webhook.php';
|
||||
$diag_wh_full_url = $computedWebhookUrl;
|
||||
$diag_wh_file = __DIR__ . ltrim($diag_wh_path, '/');
|
||||
$diag_wh_exists = file_exists($diag_wh_file);
|
||||
|
||||
// Active mode credential check
|
||||
$diag_active_id_set = $diag_is_sandbox ? $diag_sb_id_set : $diag_lv_id_set;
|
||||
$diag_active_sec_set = $diag_is_sandbox ? $diag_sb_sec_set : $diag_lv_sec_set;
|
||||
$diag_active_wh_set = $diag_is_sandbox ? $diag_sb_wh_set : $diag_lv_wh_set;
|
||||
|
||||
function diag_badge(bool $ok, string $yes = 'Yes', string $no = 'No'): string {
|
||||
$cls = $ok ? 'background:#d4edda;color:#155724;border:1px solid #c3e6cb;' : 'background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;';
|
||||
$label = $ok ? $yes : $no;
|
||||
return '<span style="' . $cls . 'padding:2px 8px;border-radius:3px;font-size:0.85em;font-weight:600;">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span>';
|
||||
}
|
||||
|
||||
// Last webhook events
|
||||
$diag_recent_events = [];
|
||||
try {
|
||||
$port_int = intval($db_port ?? 3306) ?: 3306;
|
||||
$diag_db = @mysqli_connect($db_host ?? 'localhost', $db_user ?? '', $db_pass ?? '', $db_name ?? '', $port_int);
|
||||
if ($diag_db) {
|
||||
$pfx_diag = $table_prefix ?? 'gsp_';
|
||||
$res = @mysqli_query($diag_db, "SELECT paypal_event_id, event_type, processing_status, created_at FROM `{$pfx_diag}billing_paypal_webhook_events` ORDER BY id DESC LIMIT 5");
|
||||
if ($res) {
|
||||
while ($row = mysqli_fetch_assoc($res)) {
|
||||
$diag_recent_events[] = $row;
|
||||
}
|
||||
}
|
||||
mysqli_close($diag_db);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// non-fatal
|
||||
}
|
||||
?>
|
||||
<div class="cfg-section">
|
||||
<h2>PayPal Diagnostics</h2>
|
||||
<table style="border-collapse:collapse;width:100%;font-size:0.9em;">
|
||||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;width:260px;">Current mode</td><td style="padding:6px 8px;"><strong><?php echo h($diag_mode); ?></strong></td></tr>
|
||||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Active Client ID configured</td><td style="padding:6px 8px;"><?php echo diag_badge($diag_active_id_set); ?></td></tr>
|
||||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Active Client Secret configured</td><td style="padding:6px 8px;"><?php echo diag_badge($diag_active_sec_set); ?></td></tr>
|
||||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Active Webhook ID configured</td><td style="padding:6px 8px;"><?php echo diag_badge($diag_active_wh_set, 'Yes', 'No (signature verification skipped)'); ?></td></tr>
|
||||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Sandbox credentials</td><td style="padding:6px 8px;">ID: <?php echo diag_badge($diag_sb_id_set); ?> Secret: <?php echo diag_badge($diag_sb_sec_set); ?> Webhook ID: <?php echo diag_badge($diag_sb_wh_set); ?></td></tr>
|
||||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Live credentials</td><td style="padding:6px 8px;">ID: <?php echo diag_badge($diag_lv_id_set); ?> Secret: <?php echo diag_badge($diag_lv_sec_set); ?> Webhook ID: <?php echo diag_badge($diag_lv_wh_set); ?></td></tr>
|
||||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Webhook path</td><td style="padding:6px 8px;"><code><?php echo h($diag_wh_path); ?></code></td></tr>
|
||||
<tr style="border-bottom:1px solid #eee;"><td style="padding:6px 8px;color:#555;">Full public webhook URL</td><td style="padding:6px 8px;"><code style="word-break:break-all;"><?php echo h($diag_wh_full_url ?: '(Site Base URL not set)'); ?></code></td></tr>
|
||||
<tr><td style="padding:6px 8px;color:#555;">Webhook file exists on disk</td><td style="padding:6px 8px;"><?php echo diag_badge($diag_wh_exists, 'Yes — ' . h($diag_wh_file), 'No — ' . h($diag_wh_file) . ' not found'); ?></td></tr>
|
||||
</table>
|
||||
|
||||
<?php if (!empty($diag_recent_events)): ?>
|
||||
<h4 style="margin-top:18px;color:#555;">Recent Webhook Events</h4>
|
||||
<table style="border-collapse:collapse;width:100%;font-size:0.85em;">
|
||||
<thead><tr style="background:#f8f9fa;">
|
||||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">PayPal Event ID</th>
|
||||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">Type</th>
|
||||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">Status</th>
|
||||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">Received</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($diag_recent_events as $ev): ?>
|
||||
<tr style="border-bottom:1px solid #eee;">
|
||||
<td style="padding:5px 8px;font-family:monospace;font-size:0.85em;"><?php echo h($ev['paypal_event_id'] ?: '—'); ?></td>
|
||||
<td style="padding:5px 8px;"><?php echo h($ev['event_type']); ?></td>
|
||||
<td style="padding:5px 8px;"><?php echo diag_badge($ev['processing_status'] === 'processed', $ev['processing_status'], $ev['processing_status']); ?></td>
|
||||
<td style="padding:5px 8px;"><?php echo h($ev['created_at']); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php elseif (empty($diag_recent_events)): ?>
|
||||
<p style="color:#888;font-size:0.9em;margin-top:12px;">No webhook events recorded yet. Events will appear here after PayPal delivers the first webhook to <code><?php echo h($diag_wh_full_url ?: $diag_wh_path); ?></code>.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- ===================================================================
|
||||
SECTION C: Raw PHP editor
|
||||
==================================================================== -->
|
||||
<div class="cfg-section">
|
||||
<h2>Advanced: Raw Config Editor</h2>
|
||||
|
|
|
|||
|
|
@ -253,8 +253,8 @@ if ($applied_coupon && $coupon_discount_percent > 0) {
|
|||
$final_amount = $total_amount - $discount_amount;
|
||||
|
||||
// PayPal configuration (from config)
|
||||
$sandbox = $paypal_sandbox ?? true;
|
||||
$client_id = $paypal_client_id ?? '';
|
||||
$client_id = function_exists('gsp_paypal_get_client_id') ? gsp_paypal_get_client_id() : ($paypal_client_id ?? '');
|
||||
$sandbox = function_exists('gsp_paypal_is_sandbox') ? gsp_paypal_is_sandbox() : ($paypal_sandbox ?? true);
|
||||
|
||||
// Prepare PayPal items
|
||||
$paypal_items = [];
|
||||
|
|
@ -510,7 +510,7 @@ $siteBase = $protocol . $host;
|
|||
<link rel="icon" href="images/logo-sm.png" type="image/png">
|
||||
<link rel="apple-touch-icon" href="images/logo-sm.png">
|
||||
<?php if (!$cart_empty && !empty($client_id)): ?>
|
||||
<script src="https://www.paypal.com/sdk/js?client-id=<?php echo htmlspecialchars($client_id); ?>¤cy=USD&intent=capture"></script>
|
||||
<script src="https://www.paypal.com/sdk/js?client-id=<?php echo htmlspecialchars($client_id, ENT_QUOTES, 'UTF-8'); ?>¤cy=USD&intent=capture<?php echo $sandbox ? '&debug=false' : ''; ?>"></script>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -659,7 +659,7 @@ $siteBase = $protocol . $host;
|
|||
}
|
||||
if ($cart_is_admin):
|
||||
?>
|
||||
<br><small><em>Admin: set <code>$paypal_client_id</code> in <a href="/admin_config.php" style="color:inherit;text-decoration:underline;">Site Config</a>.</em></small>
|
||||
<br><small><em>Admin: configure PayPal credentials in <a href="/admin_config.php" style="color:inherit;text-decoration:underline;">Site Config</a>.</em></small>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
|
|
|||
|
|
@ -20,13 +20,19 @@ class PayPalGateway implements PaymentGatewayInterface
|
|||
|
||||
/**
|
||||
* Build a PayPalGateway instance from global config variables.
|
||||
* Expects $paypal_client_id, $paypal_client_secret, $paypal_sandbox in scope.
|
||||
* Prefers the new gsp_paypal_* helper functions; falls back to legacy globals.
|
||||
*/
|
||||
public static function fromConfig(): self
|
||||
{
|
||||
$clientId = $GLOBALS['paypal_client_id'] ?? '';
|
||||
$clientSecret = $GLOBALS['paypal_client_secret'] ?? '';
|
||||
$sandbox = (bool)($GLOBALS['paypal_sandbox'] ?? true);
|
||||
if (function_exists('gsp_paypal_get_client_id')) {
|
||||
$clientId = gsp_paypal_get_client_id();
|
||||
$clientSecret = gsp_paypal_get_client_secret();
|
||||
$sandbox = gsp_paypal_is_sandbox();
|
||||
} else {
|
||||
$clientId = $GLOBALS['paypal_client_id'] ?? '';
|
||||
$clientSecret = $GLOBALS['paypal_client_secret'] ?? '';
|
||||
$sandbox = (bool)($GLOBALS['paypal_sandbox'] ?? true);
|
||||
}
|
||||
return new self($clientId, $clientSecret, $sandbox);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,8 @@ $table_prefix = "gsp_"; // Table prefix used in the panel database
|
|||
$db_type = "mysql";
|
||||
|
||||
# --- Site base URL ---
|
||||
# Leave empty to use relative paths (works for any install path).
|
||||
# Set to your full base URL (without trailing slash) if you need absolute URLs:
|
||||
# e.g. "https://gameservers.world" or "http://173.208.136.11/testing/modules/billing"
|
||||
# Full base URL WITHOUT trailing slash. Leave empty to use relative paths.
|
||||
# Example: "https://gameservers.world" or "https://your-domain.com"
|
||||
$SITE_BASE_URL = '';
|
||||
|
||||
# --- Background image ---
|
||||
|
|
@ -37,10 +36,23 @@ $SITE_BACKGROUND = 'images/dark.jpg';
|
|||
$SITE_DATA_DIR = realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data';
|
||||
|
||||
# --- PayPal settings ---
|
||||
$paypal_sandbox = true; // Set to false for live payments
|
||||
$paypal_client_id = ''; // Your PayPal Client ID
|
||||
$paypal_client_secret = ''; // Your PayPal Client Secret
|
||||
$paypal_webhook_id = ''; // Your PayPal Webhook ID (for webhook signature verification)
|
||||
# Mode: 'sandbox' for testing, 'live' for real payments.
|
||||
$paypal_mode = 'sandbox';
|
||||
|
||||
# Sandbox credentials (PayPal Developer Dashboard → sandbox app)
|
||||
$paypal_sandbox_client_id = ''; // e.g. AfvY_...
|
||||
$paypal_sandbox_client_secret = ''; // Keep server-side only
|
||||
$paypal_sandbox_webhook_id = ''; // Set after registering webhook in PayPal
|
||||
|
||||
# Live credentials (leave blank until ready for production)
|
||||
$paypal_live_client_id = '';
|
||||
$paypal_live_client_secret = '';
|
||||
$paypal_live_webhook_id = '';
|
||||
|
||||
# Webhook path (relative to billing site root, must start with /)
|
||||
# Full public URL = $SITE_BASE_URL + $paypal_webhook_path
|
||||
# Example full URL: https://gameservers.world/paypal/webhook.php
|
||||
$paypal_webhook_path = '/paypal/webhook.php';
|
||||
|
||||
# --- Admin config backup retention ---
|
||||
# Number of config backups to keep (1–10). Oldest backups beyond this limit are deleted.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# Website Database Configuration
|
||||
# This file contains the database connection
|
||||
# settings for the _website standalone site.
|
||||
#
|
||||
#
|
||||
# These settings should match the panel's
|
||||
# database configuration in includes/config.inc.php
|
||||
###############################################
|
||||
|
|
@ -14,14 +14,15 @@ $db_pass="Pkloyn7yvpht!";
|
|||
$db_name="panel";
|
||||
$table_prefix="gsp_";
|
||||
$db_type="mysql";
|
||||
|
||||
// Optional: base URL used by admin pages to build absolute image previews.
|
||||
// Leave empty to prefer relative paths (local folder).
|
||||
// To enable production base URL, uncomment and set it to your site, e.g.:
|
||||
// $SITE_BASE_URL = 'https://gameservers.world/';
|
||||
// $SITE_BASE_URL = 'https://gameservers.world';
|
||||
$SITE_BASE_URL = '';
|
||||
|
||||
// Normalize: ensure either empty or ends without trailing slash (we use join_base to handle joining)
|
||||
$SITE_BASE_URL = trim((string)$SITE_BASE_URL);
|
||||
// Normalize: ensure either empty or ends without trailing slash
|
||||
$SITE_BASE_URL = rtrim(trim((string)$SITE_BASE_URL), '/');
|
||||
|
||||
// Site-wide background image (relative to site root). Change to your preferred background.
|
||||
$SITE_BACKGROUND = 'images/dark.jpg';
|
||||
|
|
@ -31,11 +32,28 @@ $SITE_BACKGROUND = trim((string)$SITE_BACKGROUND);
|
|||
// Data directory for persisted payment webhook JSON files (relative to repo root)
|
||||
$SITE_DATA_DIR = realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data';
|
||||
|
||||
// PayPal configuration — set credentials here, never in API files
|
||||
$paypal_sandbox = true; // Set to false for live payments
|
||||
$paypal_client_id = ''; // Your PayPal Client ID
|
||||
$paypal_client_secret = ''; // Your PayPal Client Secret
|
||||
$paypal_webhook_id = ''; // Your PayPal Webhook ID (for webhook signature verification)
|
||||
// ---------------------------------------------------------------------------
|
||||
// PayPal configuration
|
||||
// Set credentials here — never in API files or public pages.
|
||||
//
|
||||
// Mode: 'sandbox' for testing, 'live' for real payments.
|
||||
// ---------------------------------------------------------------------------
|
||||
$paypal_mode = 'sandbox'; // 'sandbox' or 'live'
|
||||
|
||||
// Sandbox credentials (use for testing — safe to share with the dev team)
|
||||
$paypal_sandbox_client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
|
||||
$paypal_sandbox_client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
|
||||
$paypal_sandbox_webhook_id = ''; // Set after registering the webhook in PayPal sandbox dashboard
|
||||
|
||||
// Live credentials (leave blank until ready for production)
|
||||
$paypal_live_client_id = '';
|
||||
$paypal_live_client_secret = '';
|
||||
$paypal_live_webhook_id = '';
|
||||
|
||||
// Webhook path (relative to billing site root, must start with /)
|
||||
// Full public URL = $SITE_BASE_URL + $paypal_webhook_path
|
||||
// e.g. https://gameservers.world/paypal/webhook.php
|
||||
$paypal_webhook_path = '/paypal/webhook.php';
|
||||
|
||||
// Admin config backup retention: how many backups to keep (1–10). Default 5.
|
||||
$SITE_CONFIG_BACKUP_RETENTION = 5;
|
||||
|
|
|
|||
|
|
@ -207,21 +207,71 @@ unset($_billing_panel_config, $panelDbVars, $diskVars, $needsSync, $k, $v);
|
|||
// Step 4: Apply safe defaults for billing-specific variables that may be absent
|
||||
// in older config files (never overwrite values already set by the config).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// --- PayPal mode (new-style) ---
|
||||
// Backward compat: if $paypal_sandbox was set (old config) and $paypal_mode is absent,
|
||||
// derive $paypal_mode from $paypal_sandbox.
|
||||
if (!isset($paypal_mode)) {
|
||||
if (isset($paypal_sandbox)) {
|
||||
$paypal_mode = $paypal_sandbox ? 'sandbox' : 'live';
|
||||
} else {
|
||||
$paypal_mode = 'sandbox';
|
||||
}
|
||||
}
|
||||
$paypal_mode = (strtolower((string)$paypal_mode) === 'live') ? 'live' : 'sandbox';
|
||||
|
||||
// --- Sandbox credentials ---
|
||||
if (!isset($paypal_sandbox_client_id)) {
|
||||
// Backward compat: if old $paypal_client_id was set while in sandbox mode, use it
|
||||
$paypal_sandbox_client_id = (isset($paypal_client_id) && $paypal_mode === 'sandbox') ? $paypal_client_id : '';
|
||||
}
|
||||
if (!isset($paypal_sandbox_client_secret)) {
|
||||
$paypal_sandbox_client_secret = (isset($paypal_client_secret) && $paypal_mode === 'sandbox') ? $paypal_client_secret : '';
|
||||
}
|
||||
if (!isset($paypal_sandbox_webhook_id)) {
|
||||
$paypal_sandbox_webhook_id = (isset($paypal_webhook_id) && $paypal_mode === 'sandbox') ? $paypal_webhook_id : '';
|
||||
}
|
||||
|
||||
// --- Live credentials ---
|
||||
if (!isset($paypal_live_client_id)) {
|
||||
$paypal_live_client_id = (isset($paypal_client_id) && $paypal_mode === 'live') ? $paypal_client_id : '';
|
||||
}
|
||||
if (!isset($paypal_live_client_secret)) {
|
||||
$paypal_live_client_secret = (isset($paypal_client_secret) && $paypal_mode === 'live') ? $paypal_client_secret : '';
|
||||
}
|
||||
if (!isset($paypal_live_webhook_id)) {
|
||||
$paypal_live_webhook_id = (isset($paypal_webhook_id) && $paypal_mode === 'live') ? $paypal_webhook_id : '';
|
||||
}
|
||||
|
||||
// --- Legacy compatibility shims (read-only derived values) ---
|
||||
// Keep old variable names populated so any code that still reads $paypal_sandbox,
|
||||
// $paypal_client_id, $paypal_client_secret, $paypal_webhook_id keeps working.
|
||||
if (!isset($paypal_sandbox)) {
|
||||
$paypal_sandbox = true;
|
||||
$paypal_sandbox = ($paypal_mode !== 'live');
|
||||
}
|
||||
if (!isset($paypal_client_id)) {
|
||||
$paypal_client_id = '';
|
||||
$paypal_client_id = ($paypal_mode === 'live') ? $paypal_live_client_id : $paypal_sandbox_client_id;
|
||||
}
|
||||
if (!isset($paypal_client_secret)) {
|
||||
$paypal_client_secret = '';
|
||||
$paypal_client_secret = ($paypal_mode === 'live') ? $paypal_live_client_secret : $paypal_sandbox_client_secret;
|
||||
}
|
||||
if (!isset($paypal_webhook_id)) {
|
||||
$paypal_webhook_id = '';
|
||||
$paypal_webhook_id = ($paypal_mode === 'live') ? $paypal_live_webhook_id : $paypal_sandbox_webhook_id;
|
||||
}
|
||||
|
||||
// --- Webhook path ---
|
||||
if (!isset($paypal_webhook_path) || (string)$paypal_webhook_path === '') {
|
||||
$paypal_webhook_path = '/paypal/webhook.php';
|
||||
}
|
||||
// Ensure webhook path starts with /
|
||||
$paypal_webhook_path = '/' . ltrim((string)$paypal_webhook_path, '/');
|
||||
|
||||
// --- Site settings ---
|
||||
if (!isset($SITE_BASE_URL)) {
|
||||
$SITE_BASE_URL = '';
|
||||
}
|
||||
$SITE_BASE_URL = rtrim(trim((string)$SITE_BASE_URL), '/');
|
||||
|
||||
if (!isset($SITE_BACKGROUND)) {
|
||||
$SITE_BACKGROUND = 'images/dark.jpg';
|
||||
}
|
||||
|
|
@ -234,3 +284,77 @@ if (!isset($SITE_CONFIG_BACKUP_RETENTION) || !is_int($SITE_CONFIG_BACKUP_RETENTI
|
|||
$SITE_CONFIG_BACKUP_RETENTION = max(1, min(10, (int)$SITE_CONFIG_BACKUP_RETENTION));
|
||||
|
||||
define('BILLING_CONFIG_LOADED', true);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PayPal helper functions — use these everywhere instead of reading globals.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (!function_exists('gsp_paypal_get_mode')) {
|
||||
function gsp_paypal_get_mode(): string
|
||||
{
|
||||
return $GLOBALS['paypal_mode'] === 'live' ? 'live' : 'sandbox';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gsp_paypal_is_sandbox')) {
|
||||
function gsp_paypal_is_sandbox(): bool
|
||||
{
|
||||
return gsp_paypal_get_mode() !== 'live';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gsp_paypal_get_client_id')) {
|
||||
function gsp_paypal_get_client_id(): string
|
||||
{
|
||||
if (gsp_paypal_is_sandbox()) {
|
||||
return (string)($GLOBALS['paypal_sandbox_client_id'] ?? '');
|
||||
}
|
||||
return (string)($GLOBALS['paypal_live_client_id'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gsp_paypal_get_client_secret')) {
|
||||
function gsp_paypal_get_client_secret(): string
|
||||
{
|
||||
if (gsp_paypal_is_sandbox()) {
|
||||
return (string)($GLOBALS['paypal_sandbox_client_secret'] ?? '');
|
||||
}
|
||||
return (string)($GLOBALS['paypal_live_client_secret'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gsp_paypal_get_webhook_id')) {
|
||||
function gsp_paypal_get_webhook_id(): string
|
||||
{
|
||||
if (gsp_paypal_is_sandbox()) {
|
||||
return (string)($GLOBALS['paypal_sandbox_webhook_id'] ?? '');
|
||||
}
|
||||
return (string)($GLOBALS['paypal_live_webhook_id'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gsp_paypal_get_api_base')) {
|
||||
function gsp_paypal_get_api_base(): string
|
||||
{
|
||||
return gsp_paypal_is_sandbox()
|
||||
? 'https://api-m.sandbox.paypal.com'
|
||||
: 'https://api-m.paypal.com';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gsp_paypal_get_webhook_path')) {
|
||||
function gsp_paypal_get_webhook_path(): string
|
||||
{
|
||||
$path = (string)($GLOBALS['paypal_webhook_path'] ?? '/paypal/webhook.php');
|
||||
return '/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('gsp_paypal_get_full_webhook_url')) {
|
||||
function gsp_paypal_get_full_webhook_url(): string
|
||||
{
|
||||
$base = rtrim((string)($GLOBALS['SITE_BASE_URL'] ?? ''), '/');
|
||||
$path = gsp_paypal_get_webhook_path();
|
||||
return $base . $path;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@
|
|||
|
||||
// Module general information
|
||||
$module_title = "billing";
|
||||
$module_version = "3.2";
|
||||
$db_version = 2;
|
||||
$module_version = "3.3";
|
||||
$db_version = 3;
|
||||
$module_required = FALSE;
|
||||
// Module description
|
||||
$module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management.";
|
||||
|
|
@ -322,4 +322,28 @@ $install_queries[2] = array(
|
|||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;"
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// db_version 3 — Add billing_paypal_webhook_events table for idempotent
|
||||
// webhook event processing.
|
||||
// -----------------------------------------------------------------------
|
||||
$install_queries[3] = array(
|
||||
"CREATE TABLE IF NOT EXISTS `OGP_DB_PREFIXbilling_paypal_webhook_events` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`paypal_event_id` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`event_type` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`resource_id` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`order_id` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`capture_id` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`billing_order_id` INT(11) NOT NULL DEFAULT 0,
|
||||
`processing_status` VARCHAR(50) NOT NULL DEFAULT 'received',
|
||||
`raw_json` MEDIUMTEXT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`processed_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uidx_paypal_event_id` (`paypal_event_id`),
|
||||
KEY `idx_event_type` (`event_type`),
|
||||
KEY `idx_billing_order_id` (`billing_order_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
|
||||
);
|
||||
|
||||
?>
|
||||
|
|
|
|||
714
modules/billing/paypal/webhook.php
Normal file
714
modules/billing/paypal/webhook.php
Normal file
|
|
@ -0,0 +1,714 @@
|
|||
<?php
|
||||
/**
|
||||
* GSP PayPal Webhook Receiver
|
||||
*
|
||||
* Public URL: $SITE_BASE_URL + $paypal_webhook_path (e.g. https://gameservers.world/paypal/webhook.php)
|
||||
*
|
||||
* This endpoint:
|
||||
* 1. Reads raw POST JSON from PayPal
|
||||
* 2. Verifies the webhook signature using PayPal's verify-webhook-signature API
|
||||
* 3. Checks for duplicate events (idempotency) via billing_paypal_webhook_events table
|
||||
* 4. Processes supported event types and updates billing_orders / triggers provisioning
|
||||
* 5. Returns appropriate HTTP status codes
|
||||
*
|
||||
* HTTP status codes returned:
|
||||
* 200 — success (or duplicate event safely ignored)
|
||||
* 400 — missing / invalid JSON body
|
||||
* 401 — PayPal signature verification failed or OAuth failed
|
||||
* 500 — internal error (DB unavailable, etc.)
|
||||
*/
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bootstrap: load config and DB
|
||||
// ---------------------------------------------------------------------------
|
||||
$_billing_dir = dirname(__DIR__);
|
||||
require_once $_billing_dir . '/includes/config_loader.php';
|
||||
|
||||
// Log helper — writes to logs/paypal_webhook.log; never logs secrets.
|
||||
$_wh_log_file = $_billing_dir . '/logs/paypal_webhook.log';
|
||||
@mkdir(dirname($_wh_log_file), 0755, true);
|
||||
|
||||
function wh_log(string $level, string $message, array $context = []): void
|
||||
{
|
||||
global $_wh_log_file;
|
||||
$ctx = empty($context) ? '' : ' ' . json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
$line = '[' . date('c') . '] [' . strtoupper($level) . '] ' . $message . $ctx . "\n";
|
||||
@file_put_contents($_wh_log_file, $line, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Read raw input
|
||||
// ---------------------------------------------------------------------------
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$headers = array_change_key_case((array)(getallheaders() ?: []), CASE_UPPER);
|
||||
|
||||
wh_log('info', 'webhook_received', ['ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'bytes' => strlen($raw)]);
|
||||
|
||||
if ($raw === '') {
|
||||
wh_log('warn', 'empty_body');
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'empty_body']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$evt = json_decode($raw, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($evt)) {
|
||||
wh_log('warn', 'invalid_json', ['json_error' => json_last_error_msg()]);
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'invalid_json']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. DB connection (needed for idempotency log and order updates)
|
||||
// ---------------------------------------------------------------------------
|
||||
$db_port_int = intval($db_port ?? 3306) ?: 3306;
|
||||
$wh_db = @mysqli_connect($db_host ?? 'localhost', $db_user ?? '', $db_pass ?? '', $db_name ?? '', $db_port_int);
|
||||
if (!$wh_db) {
|
||||
wh_log('error', 'db_connect_failed', ['error' => mysqli_connect_error()]);
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'db_unavailable']);
|
||||
exit;
|
||||
}
|
||||
mysqli_set_charset($wh_db, 'utf8mb4');
|
||||
|
||||
$pfx = $table_prefix ?? 'gsp_';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2a. Ensure the webhook event log table exists (idempotent DDL)
|
||||
// ---------------------------------------------------------------------------
|
||||
wh_ensure_event_table($wh_db, $pfx);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. PayPal OAuth token
|
||||
// ---------------------------------------------------------------------------
|
||||
$api_base = gsp_paypal_get_api_base();
|
||||
$client_id = gsp_paypal_get_client_id();
|
||||
$client_secret = gsp_paypal_get_client_secret();
|
||||
$webhook_id = gsp_paypal_get_webhook_id();
|
||||
|
||||
if (empty($client_id) || empty($client_secret)) {
|
||||
wh_log('error', 'paypal_not_configured');
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'paypal_not_configured']);
|
||||
mysqli_close($wh_db);
|
||||
exit;
|
||||
}
|
||||
|
||||
$access_token = wh_get_access_token($api_base, $client_id, $client_secret);
|
||||
if (!$access_token) {
|
||||
wh_log('warn', 'oauth_failed');
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'oauth_failed']);
|
||||
mysqli_close($wh_db);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. Verify webhook signature (skip only if webhook_id is empty)
|
||||
// ---------------------------------------------------------------------------
|
||||
if (!empty($webhook_id)) {
|
||||
$verified = wh_verify_signature($api_base, $access_token, $webhook_id, $headers, $evt);
|
||||
if (!$verified) {
|
||||
wh_log('warn', 'signature_invalid', [
|
||||
'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? '',
|
||||
'event_type' => $evt['event_type'] ?? '',
|
||||
]);
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'signature_invalid']);
|
||||
mysqli_close($wh_db);
|
||||
exit;
|
||||
}
|
||||
wh_log('info', 'signature_ok');
|
||||
} else {
|
||||
wh_log('warn', 'signature_skipped_no_webhook_id');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5. Idempotency check
|
||||
// ---------------------------------------------------------------------------
|
||||
$paypal_event_id = $evt['id'] ?? '';
|
||||
$event_type = $evt['event_type'] ?? '';
|
||||
$resource = $evt['resource'] ?? [];
|
||||
|
||||
if ($paypal_event_id !== '') {
|
||||
$existing = wh_get_event($wh_db, $pfx, $paypal_event_id);
|
||||
if ($existing && $existing['processing_status'] === 'processed') {
|
||||
wh_log('info', 'duplicate_event_ignored', ['paypal_event_id' => $paypal_event_id, 'event_type' => $event_type]);
|
||||
http_response_code(200);
|
||||
echo json_encode(['status' => 'duplicate_ignored']);
|
||||
mysqli_close($wh_db);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Log the event as received (upsert — so retries update the record)
|
||||
$log_id = wh_log_event($wh_db, $pfx, [
|
||||
'paypal_event_id' => $paypal_event_id,
|
||||
'event_type' => $event_type,
|
||||
'resource_id' => $resource['id'] ?? '',
|
||||
'order_id' => '',
|
||||
'capture_id' => '',
|
||||
'billing_order_id' => 0,
|
||||
'processing_status' => 'received',
|
||||
'raw_json' => $raw,
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 6. Process event
|
||||
// ---------------------------------------------------------------------------
|
||||
$result = wh_process_event($wh_db, $pfx, $event_type, $resource, $evt, $access_token, $api_base, $raw, $_billing_dir);
|
||||
|
||||
// Update log entry with final status
|
||||
if ($log_id > 0) {
|
||||
wh_update_event_status($wh_db, $pfx, $log_id, $result['status'], $result['billing_order_id'] ?? 0);
|
||||
}
|
||||
|
||||
wh_log('info', 'event_processed', [
|
||||
'event_type' => $event_type,
|
||||
'status' => $result['status'],
|
||||
'billing_order_id' => $result['billing_order_id'] ?? 0,
|
||||
]);
|
||||
|
||||
http_response_code(200);
|
||||
echo json_encode(['status' => $result['status']]);
|
||||
mysqli_close($wh_db);
|
||||
exit;
|
||||
|
||||
// ============================================================================
|
||||
// Helper functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get OAuth access token from PayPal.
|
||||
*/
|
||||
function wh_get_access_token(string $api_base, string $client_id, string $client_secret): ?string
|
||||
{
|
||||
$ch = curl_init($api_base . '/v1/oauth2/token');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => 'grant_type=client_credentials',
|
||||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||||
CURLOPT_USERPWD => $client_id . ':' . $client_secret,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($code !== 200 || !$body) {
|
||||
return null;
|
||||
}
|
||||
$data = json_decode($body, true);
|
||||
return $data['access_token'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify PayPal webhook signature.
|
||||
* Returns true only when PayPal confirms verification_status = SUCCESS.
|
||||
*/
|
||||
function wh_verify_signature(
|
||||
string $api_base,
|
||||
string $access_token,
|
||||
string $webhook_id,
|
||||
array $headers,
|
||||
array $evt
|
||||
): bool {
|
||||
$payload = [
|
||||
'auth_algo' => $headers['PAYPAL-AUTH-ALGO'] ?? '',
|
||||
'cert_url' => $headers['PAYPAL-CERT-URL'] ?? '',
|
||||
'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? '',
|
||||
'transmission_sig' => $headers['PAYPAL-TRANSMISSION-SIG'] ?? '',
|
||||
'transmission_time'=> $headers['PAYPAL-TRANSMISSION-TIME'] ?? '',
|
||||
'webhook_id' => $webhook_id,
|
||||
'webhook_event' => $evt,
|
||||
];
|
||||
$ch = curl_init($api_base . '/v1/notifications/verify-webhook-signature');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $access_token,
|
||||
],
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($code !== 200 || !$resp) {
|
||||
return false;
|
||||
}
|
||||
$data = json_decode($resp, true);
|
||||
return ($data['verification_status'] ?? '') === 'SUCCESS';
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single webhook event. Returns ['status' => string, 'billing_order_id' => int].
|
||||
*/
|
||||
function wh_process_event(
|
||||
mysqli $db,
|
||||
string $pfx,
|
||||
string $event_type,
|
||||
array $resource,
|
||||
array $evt,
|
||||
string $access_token,
|
||||
string $api_base,
|
||||
string $raw_json,
|
||||
string $billing_dir = ''
|
||||
): array {
|
||||
switch ($event_type) {
|
||||
case 'CHECKOUT.ORDER.APPROVED':
|
||||
return wh_handle_order_approved($db, $pfx, $resource, $evt);
|
||||
|
||||
case 'PAYMENT.CAPTURE.COMPLETED':
|
||||
case 'PAYMENT.SALE.COMPLETED':
|
||||
return wh_handle_capture_completed($db, $pfx, $resource, $evt, $access_token, $api_base, $billing_dir);
|
||||
|
||||
case 'PAYMENT.CAPTURE.DENIED':
|
||||
case 'PAYMENT.SALE.DENIED':
|
||||
return wh_handle_capture_denied($db, $pfx, $resource, $evt);
|
||||
|
||||
case 'PAYMENT.CAPTURE.REFUNDED':
|
||||
case 'PAYMENT.SALE.REFUNDED':
|
||||
return wh_handle_capture_refunded($db, $pfx, $resource, $evt);
|
||||
|
||||
default:
|
||||
wh_log('info', 'unhandled_event_type', ['event_type' => $event_type]);
|
||||
return ['status' => 'ignored_unhandled', 'billing_order_id' => 0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CHECKOUT.ORDER.APPROVED — buyer approved the order but capture not yet done.
|
||||
* We log this for auditing; the actual fulfillment happens on CAPTURE.COMPLETED.
|
||||
*/
|
||||
function wh_handle_order_approved(mysqli $db, string $pfx, array $resource, array $evt): array
|
||||
{
|
||||
$paypal_order_id = $resource['id'] ?? ($evt['resource']['id'] ?? '');
|
||||
wh_log('info', 'order_approved', ['paypal_order_id' => $paypal_order_id]);
|
||||
return ['status' => 'approved_logged', 'billing_order_id' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* PAYMENT.CAPTURE.COMPLETED — payment fully captured; provision the server.
|
||||
*/
|
||||
function wh_handle_capture_completed(
|
||||
mysqli $db,
|
||||
string $pfx,
|
||||
array $resource,
|
||||
array $evt,
|
||||
string $access_token,
|
||||
string $api_base,
|
||||
string $billing_dir = ''
|
||||
): array {
|
||||
$capture_id = $resource['id'] ?? '';
|
||||
$amount = $resource['amount']['value'] ?? null;
|
||||
$currency = $resource['amount']['currency_code'] ?? 'USD';
|
||||
|
||||
// Extract PayPal order ID from supplementary_data or links
|
||||
$paypal_order_id = $resource['supplementary_data']['related_ids']['order_id'] ?? '';
|
||||
if (empty($paypal_order_id) && isset($resource['links']) && is_array($resource['links'])) {
|
||||
foreach ($resource['links'] as $lnk) {
|
||||
if (!empty($lnk['href']) && stripos($lnk['href'], '/v2/checkout/orders/') !== false) {
|
||||
$paypal_order_id = basename(parse_url($lnk['href'], PHP_URL_PATH));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract invoice/custom from resource or fetch the full order
|
||||
$invoice_ref = $resource['invoice_id'] ?? ($resource['invoice_number'] ?? null);
|
||||
$custom_id = $resource['custom_id'] ?? ($resource['custom'] ?? null);
|
||||
|
||||
// If we have a PayPal order ID, fetch the order to get invoice/custom IDs
|
||||
if (!empty($paypal_order_id) && (empty($invoice_ref) || empty($custom_id))) {
|
||||
$order_data = wh_fetch_paypal_order($api_base, $access_token, $paypal_order_id);
|
||||
if ($order_data) {
|
||||
$pu = $order_data['purchase_units'][0] ?? [];
|
||||
if (empty($invoice_ref)) $invoice_ref = $pu['invoice_id'] ?? null;
|
||||
if (empty($custom_id)) $custom_id = $pu['custom_id'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
wh_log('info', 'capture_completed', [
|
||||
'capture_id' => $capture_id,
|
||||
'paypal_order_id' => $paypal_order_id,
|
||||
'invoice_ref' => $invoice_ref,
|
||||
'custom_id' => $custom_id,
|
||||
'amount' => $amount,
|
||||
]);
|
||||
|
||||
// Find matching billing invoice(s) and process payment
|
||||
$billing_order_id = wh_fulfill_payment($db, $pfx, [
|
||||
'capture_id' => $capture_id,
|
||||
'paypal_order_id' => $paypal_order_id,
|
||||
'invoice_ref' => $invoice_ref,
|
||||
'custom_id' => $custom_id,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
], $billing_dir);
|
||||
|
||||
return ['status' => 'processed', 'billing_order_id' => $billing_order_id];
|
||||
}
|
||||
|
||||
/**
|
||||
* PAYMENT.CAPTURE.DENIED — capture was denied (e.g. failed fraud check).
|
||||
*/
|
||||
function wh_handle_capture_denied(mysqli $db, string $pfx, array $resource, array $evt): array
|
||||
{
|
||||
$capture_id = $resource['id'] ?? '';
|
||||
wh_log('warn', 'capture_denied', ['capture_id' => $capture_id]);
|
||||
|
||||
// Find the billing order for this capture and mark it denied, if still pending
|
||||
if ($capture_id !== '') {
|
||||
$esc = mysqli_real_escape_string($db, $capture_id);
|
||||
$sql = "UPDATE `{$pfx}billing_orders`
|
||||
SET status = 'payment_denied'
|
||||
WHERE payment_txid = '{$esc}'
|
||||
AND status NOT IN ('Active','cancelled')
|
||||
LIMIT 1";
|
||||
mysqli_query($db, $sql);
|
||||
}
|
||||
|
||||
return ['status' => 'denied_logged', 'billing_order_id' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* PAYMENT.CAPTURE.REFUNDED — payment was refunded.
|
||||
*/
|
||||
function wh_handle_capture_refunded(mysqli $db, string $pfx, array $resource, array $evt): array
|
||||
{
|
||||
$refund_id = $resource['id'] ?? '';
|
||||
$capture_id = $resource['links'] ? (function () use ($resource) {
|
||||
foreach ($resource['links'] as $l) {
|
||||
if (($l['rel'] ?? '') === 'up' && stripos($l['href'] ?? '', '/captures/') !== false) {
|
||||
return basename(parse_url($l['href'], PHP_URL_PATH));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
})() : '';
|
||||
|
||||
wh_log('info', 'capture_refunded', ['refund_id' => $refund_id, 'capture_id' => $capture_id]);
|
||||
|
||||
// Log the refund; do not automatically cancel the server unless the billing lifecycle supports it.
|
||||
return ['status' => 'refunded_logged', 'billing_order_id' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a PayPal order by ID. Returns decoded array or null.
|
||||
*/
|
||||
function wh_fetch_paypal_order(string $api_base, string $access_token, string $order_id): ?array
|
||||
{
|
||||
$ch = curl_init($api_base . '/v2/checkout/orders/' . urlencode($order_id));
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $access_token,
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($code !== 200 || !$body) {
|
||||
wh_log('warn', 'order_fetch_failed', ['order_id' => $order_id, 'http' => $code]);
|
||||
return null;
|
||||
}
|
||||
$data = json_decode($body, true);
|
||||
return is_array($data) ? $data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the PayPal capture to a billing invoice, mark it paid, create/extend billing_orders,
|
||||
* and trigger server provisioning. Returns the billing_order_id or 0.
|
||||
*/
|
||||
function wh_fulfill_payment(mysqli $db, string $pfx, array $payment, string $billing_dir = ''): int
|
||||
{
|
||||
$txid = $payment['capture_id'] ?? '';
|
||||
$custom_id = $payment['custom_id'] ?? null;
|
||||
$invoice_ref = $payment['invoice_ref'] ?? null;
|
||||
$amount = isset($payment['amount']) ? floatval($payment['amount']) : null;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$esc_txid = mysqli_real_escape_string($db, (string)$txid);
|
||||
|
||||
// Find matching invoices
|
||||
$invoices = [];
|
||||
|
||||
// 1) Match by numeric custom_id (which we set to invoice_id when creating the PayPal order)
|
||||
if (!empty($custom_id) && ctype_digit((string)$custom_id)) {
|
||||
$inv_id = intval($custom_id);
|
||||
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_invoices` WHERE invoice_id = {$inv_id} AND status = 'due' LIMIT 1");
|
||||
if ($res && $row = mysqli_fetch_assoc($res)) {
|
||||
$invoices[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Match by invoice reference in description
|
||||
if (empty($invoices) && !empty($invoice_ref)) {
|
||||
$esc_ref = mysqli_real_escape_string($db, (string)$invoice_ref);
|
||||
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_invoices` WHERE status = 'due' AND description LIKE '%{$esc_ref}%'");
|
||||
if ($res) {
|
||||
while ($row = mysqli_fetch_assoc($res)) {
|
||||
$invoices[] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Fallback: match by exact amount
|
||||
if (empty($invoices) && $amount !== null) {
|
||||
$esc_amount = number_format($amount, 2, '.', '');
|
||||
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_invoices` WHERE status = 'due' AND amount = {$esc_amount}");
|
||||
if ($res) {
|
||||
while ($row = mysqli_fetch_assoc($res)) {
|
||||
$invoices[] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($invoices)) {
|
||||
wh_log('warn', 'no_matching_invoices', ['custom_id' => $custom_id, 'invoice_ref' => $invoice_ref, 'amount' => $amount]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
$last_order_id = 0;
|
||||
|
||||
foreach ($invoices as $inv) {
|
||||
$invoice_id = intval($inv['invoice_id']);
|
||||
$order_id = intval($inv['order_id'] ?? 0);
|
||||
$user_id = intval($inv['user_id']);
|
||||
$service_id = intval($inv['service_id'] ?? 0);
|
||||
$duration = $inv['invoice_duration'] ?? 'month';
|
||||
$qty = max(1, intval($inv['qty'] ?? 1));
|
||||
|
||||
// Mark invoice paid
|
||||
$stmt = mysqli_prepare($db, "UPDATE `{$pfx}billing_invoices` SET status='paid', payment_status='paid', paid_date=?, payment_txid=?, payment_method='paypal' WHERE invoice_id=? LIMIT 1");
|
||||
if ($stmt) {
|
||||
mysqli_stmt_bind_param($stmt, 'ssi', $now, $esc_txid, $invoice_id);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
}
|
||||
|
||||
// Increment coupon usage if applicable
|
||||
$coupon_id = intval($inv['coupon_id'] ?? 0);
|
||||
if ($coupon_id > 0) {
|
||||
mysqli_query($db, "UPDATE `{$pfx}billing_coupons` SET current_uses = current_uses + 1 WHERE coupon_id = {$coupon_id}");
|
||||
}
|
||||
|
||||
// Duration → months
|
||||
$months = 1;
|
||||
if (stripos($duration, 'year') !== false) {
|
||||
$months = $qty * 12;
|
||||
} else {
|
||||
$months = $qty;
|
||||
}
|
||||
|
||||
if ($order_id > 0) {
|
||||
// Renewal: extend existing order
|
||||
$res = mysqli_query($db, "SELECT end_date, home_id FROM `{$pfx}billing_orders` WHERE order_id = {$order_id} LIMIT 1");
|
||||
if ($res && $row = mysqli_fetch_assoc($res)) {
|
||||
$current_end = $row['end_date'] ?? $now;
|
||||
$extend_from = (strtotime($current_end) > time()) ? $current_end : $now;
|
||||
$dt = new DateTime($extend_from);
|
||||
$dt->modify('+' . $months . ' months');
|
||||
$new_end = $dt->format('Y-m-d H:i:s');
|
||||
|
||||
$stmt = mysqli_prepare($db, "UPDATE `{$pfx}billing_orders` SET end_date=?, status='Active', payment_txid=?, paid_ts=? WHERE order_id=? LIMIT 1");
|
||||
if ($stmt) {
|
||||
mysqli_stmt_bind_param($stmt, 'sssi', $new_end, $esc_txid, $now, $order_id);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
}
|
||||
$last_order_id = $order_id;
|
||||
wh_log('info', 'order_renewed', ['order_id' => $order_id, 'new_end' => $new_end]);
|
||||
}
|
||||
} else {
|
||||
// New order: create billing_orders row
|
||||
$dt = new DateTime($now);
|
||||
$dt->modify('+' . $months . ' months');
|
||||
$end_date = $dt->format('Y-m-d H:i:s');
|
||||
$invoice_amount = floatval($inv['amount'] ?? $inv['total_due'] ?? 0);
|
||||
$price = number_format($invoice_amount, 2, '.', '');
|
||||
$esc_home = mysqli_real_escape_string($db, $inv['home_name'] ?? '');
|
||||
$esc_dur = mysqli_real_escape_string($db, $duration);
|
||||
$esc_rcon = mysqli_real_escape_string($db, $inv['remote_control_password'] ?? '');
|
||||
$esc_ftp = mysqli_real_escape_string($db, $inv['ftp_password'] ?? '');
|
||||
$ip_val = intval($inv['ip'] ?? 0);
|
||||
$max_pl = intval($inv['max_players'] ?? 0);
|
||||
|
||||
$sql = sprintf(
|
||||
"INSERT INTO `%sbilling_orders` (user_id, service_id, home_name, ip, max_players, qty, invoice_duration, price, remote_control_password, ftp_password, status, order_date, end_date, payment_txid, paid_ts)
|
||||
VALUES (%d, %d, '%s', %d, %d, %d, '%s', %s, '%s', '%s', 'Active', '%s', '%s', '%s', '%s')",
|
||||
$pfx,
|
||||
$user_id, $service_id, $esc_home, $ip_val, $max_pl, $qty,
|
||||
$esc_dur, $price, $esc_rcon, $esc_ftp, $now, $end_date, $esc_txid, $now
|
||||
);
|
||||
|
||||
if (mysqli_query($db, $sql)) {
|
||||
$new_order_id = (int)mysqli_insert_id($db);
|
||||
|
||||
// Link invoice → order
|
||||
$stmt = mysqli_prepare($db, "UPDATE `{$pfx}billing_invoices` SET order_id=? WHERE invoice_id=? LIMIT 1");
|
||||
if ($stmt) {
|
||||
mysqli_stmt_bind_param($stmt, 'ii', $new_order_id, $invoice_id);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
}
|
||||
|
||||
$last_order_id = $new_order_id;
|
||||
wh_log('info', 'order_created', ['order_id' => $new_order_id, 'invoice_id' => $invoice_id]);
|
||||
|
||||
// Attempt provisioning via panel bridge
|
||||
$dir = ($billing_dir !== '') ? $billing_dir : dirname(__DIR__);
|
||||
wh_try_provision($dir, $new_order_id, $user_id);
|
||||
} else {
|
||||
wh_log('error', 'order_insert_failed', ['db_error' => mysqli_error($db), 'invoice_id' => $invoice_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $last_order_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to provision a newly created server via the panel bridge.
|
||||
* Non-fatal: logs warnings on failure.
|
||||
*/
|
||||
function wh_try_provision(string $billing_dir, int $order_id, int $user_id): void
|
||||
{
|
||||
$bridge = $billing_dir . '/includes/panel_bridge.php';
|
||||
$create = $billing_dir . '/create_servers.php';
|
||||
if (!is_file($bridge) || !is_file($create)) {
|
||||
wh_log('info', 'provision_skipped_no_bridge', ['order_id' => $order_id]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
require_once $bridge;
|
||||
if (!function_exists('billing_panel_bootstrap')) {
|
||||
wh_log('warn', 'provision_no_bootstrap_fn', ['order_id' => $order_id]);
|
||||
return;
|
||||
}
|
||||
$ctx = billing_panel_bootstrap();
|
||||
if (!$ctx || empty($ctx['db'])) {
|
||||
wh_log('warn', 'provision_panel_bootstrap_failed', ['order_id' => $order_id]);
|
||||
return;
|
||||
}
|
||||
$GLOBALS['db'] = $ctx['db'];
|
||||
$GLOBALS['settings'] = $ctx['settings'] ?? [];
|
||||
require_once $create;
|
||||
if (function_exists('billing_invoke_provision')) {
|
||||
$r = billing_invoke_provision(['order_ids' => [$order_id], 'user_id' => $user_id, 'is_admin' => true]);
|
||||
wh_log('info', 'provision_result', ['order_id' => $order_id, 'result' => $r]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
wh_log('error', 'provision_exception', ['order_id' => $order_id, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Webhook event log table helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Ensure billing_paypal_webhook_events table exists (idempotent, no ALTER on existing tables).
|
||||
*/
|
||||
function wh_ensure_event_table(mysqli $db, string $pfx): void
|
||||
{
|
||||
$table = $pfx . 'billing_paypal_webhook_events';
|
||||
$res = mysqli_query($db, "SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{$table}'");
|
||||
if ($res && $row = mysqli_fetch_assoc($res)) {
|
||||
if (intval($row['cnt']) > 0) {
|
||||
return; // table exists
|
||||
}
|
||||
}
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `{$table}` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`paypal_event_id` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`event_type` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`resource_id` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`order_id` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`capture_id` VARCHAR(100) NOT NULL DEFAULT '',
|
||||
`billing_order_id` INT(11) NOT NULL DEFAULT 0,
|
||||
`processing_status` VARCHAR(50) NOT NULL DEFAULT 'received',
|
||||
`raw_json` MEDIUMTEXT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`processed_at` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uidx_paypal_event_id` (`paypal_event_id`),
|
||||
KEY `idx_event_type` (`event_type`),
|
||||
KEY `idx_billing_order_id` (`billing_order_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
|
||||
mysqli_query($db, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an existing webhook event log row by paypal_event_id.
|
||||
*/
|
||||
function wh_get_event(mysqli $db, string $pfx, string $paypal_event_id): ?array
|
||||
{
|
||||
if ($paypal_event_id === '') return null;
|
||||
$esc = mysqli_real_escape_string($db, $paypal_event_id);
|
||||
$res = mysqli_query($db, "SELECT * FROM `{$pfx}billing_paypal_webhook_events` WHERE paypal_event_id = '{$esc}' LIMIT 1");
|
||||
if (!$res) return null;
|
||||
$row = mysqli_fetch_assoc($res);
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a webhook event log row. Returns the row id.
|
||||
*/
|
||||
function wh_log_event(mysqli $db, string $pfx, array $data): int
|
||||
{
|
||||
$paypal_event_id = mysqli_real_escape_string($db, $data['paypal_event_id'] ?? '');
|
||||
$event_type = mysqli_real_escape_string($db, $data['event_type'] ?? '');
|
||||
$resource_id = mysqli_real_escape_string($db, $data['resource_id'] ?? '');
|
||||
$order_id_str = mysqli_real_escape_string($db, $data['order_id'] ?? '');
|
||||
$capture_id = mysqli_real_escape_string($db, $data['capture_id'] ?? '');
|
||||
$billing_order_id = intval($data['billing_order_id'] ?? 0);
|
||||
$processing_status = mysqli_real_escape_string($db, $data['processing_status'] ?? 'received');
|
||||
$raw_json = mysqli_real_escape_string($db, $data['raw_json'] ?? '');
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
if ($paypal_event_id === '') {
|
||||
// No stable event ID — always insert
|
||||
$sql = "INSERT INTO `{$pfx}billing_paypal_webhook_events`
|
||||
(paypal_event_id, event_type, resource_id, order_id, capture_id, billing_order_id, processing_status, raw_json, created_at)
|
||||
VALUES ('{$paypal_event_id}', '{$event_type}', '{$resource_id}', '{$order_id_str}', '{$capture_id}', {$billing_order_id}, '{$processing_status}', '{$raw_json}', '{$now}')";
|
||||
mysqli_query($db, $sql);
|
||||
return (int)mysqli_insert_id($db);
|
||||
}
|
||||
|
||||
// Upsert: insert or update existing row
|
||||
$sql = "INSERT INTO `{$pfx}billing_paypal_webhook_events`
|
||||
(paypal_event_id, event_type, resource_id, order_id, capture_id, billing_order_id, processing_status, raw_json, created_at)
|
||||
VALUES ('{$paypal_event_id}', '{$event_type}', '{$resource_id}', '{$order_id_str}', '{$capture_id}', {$billing_order_id}, '{$processing_status}', '{$raw_json}', '{$now}')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
processing_status = VALUES(processing_status),
|
||||
billing_order_id = VALUES(billing_order_id)";
|
||||
mysqli_query($db, $sql);
|
||||
$insert_id = (int)mysqli_insert_id($db);
|
||||
if ($insert_id > 0) {
|
||||
return $insert_id;
|
||||
}
|
||||
// Row already existed — fetch its id
|
||||
$existing = wh_get_event($db, $pfx, $data['paypal_event_id']);
|
||||
return $existing ? intval($existing['id']) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update processing_status and processed_at on an event log row.
|
||||
*/
|
||||
function wh_update_event_status(mysqli $db, string $pfx, int $log_id, string $status, int $billing_order_id): void
|
||||
{
|
||||
$esc_status = mysqli_real_escape_string($db, $status);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$bod = intval($billing_order_id);
|
||||
mysqli_query($db, "UPDATE `{$pfx}billing_paypal_webhook_events`
|
||||
SET processing_status = '{$esc_status}', billing_order_id = {$bod}, processed_at = '{$now}'
|
||||
WHERE id = {$log_id} LIMIT 1");
|
||||
}
|
||||
|
|
@ -2,11 +2,18 @@
|
|||
require_once(__DIR__ . '/includes/config_loader.php');
|
||||
if (is_file(__DIR__ . '/includes/log.php')) require_once(__DIR__ . '/includes/log.php');
|
||||
|
||||
// Derive active credentials via helper functions (falls back to old globals gracefully)
|
||||
$_wh_sandbox = function_exists('gsp_paypal_is_sandbox') ? gsp_paypal_is_sandbox() : ($paypal_sandbox ?? true);
|
||||
$_wh_client_id = function_exists('gsp_paypal_get_client_id') ? gsp_paypal_get_client_id() : ($paypal_client_id ?? '');
|
||||
$_wh_client_secret = function_exists('gsp_paypal_get_client_secret') ? gsp_paypal_get_client_secret() : ($paypal_client_secret ?? '');
|
||||
$_wh_webhook_id = function_exists('gsp_paypal_get_webhook_id') ? gsp_paypal_get_webhook_id() : ($paypal_webhook_id ?? '');
|
||||
$_wh_api_base = function_exists('gsp_paypal_get_api_base') ? gsp_paypal_get_api_base() : ($_wh_sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com');
|
||||
|
||||
$config = [
|
||||
'sandbox' => $paypal_sandbox ?? true,
|
||||
'client_id' => $paypal_client_id ?? '',
|
||||
'client_secret' => $paypal_client_secret ?? '',
|
||||
'webhook_id' => $paypal_webhook_id ?? '',
|
||||
'sandbox' => $_wh_sandbox,
|
||||
'client_id' => $_wh_client_id,
|
||||
'client_secret' => $_wh_client_secret,
|
||||
'webhook_id' => $_wh_webhook_id,
|
||||
'data_dir' => rtrim(
|
||||
(defined('SITE_DATA_DIR') ? SITE_DATA_DIR : '') ?: ($SITE_DATA_DIR ?? ''),
|
||||
DIRECTORY_SEPARATOR
|
||||
|
|
@ -18,8 +25,150 @@ if (!$config['data_dir']) {
|
|||
$config['data_dir'] = realpath(__DIR__ . '/') . DIRECTORY_SEPARATOR . 'data';
|
||||
}
|
||||
|
||||
function log_line($m){global $config; @file_put_contents($config['log_file'],'['.date('c')."] $m\n",FILE_APPEND);}
|
||||
function api_base(){global $config; return $config['sandbox'] ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';}
|
||||
function log_line($m){global $config; @file_put_contents($config['log_file'],'['.date('c')."] $m\n",FILE_APPEND);}
|
||||
function api_base(){global $_wh_api_base; return $_wh_api_base;}
|
||||
|
||||
http_response_code(200);
|
||||
@mkdir($config['data_dir'], 0775, true);
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
$headers = array_change_key_case(getallheaders() ?: [], CASE_UPPER);
|
||||
if (function_exists('site_log_info')) site_log_info('webhook_hit', ['ip'=>($_SERVER['REMOTE_ADDR']??''),'bytes'=>strlen($raw)]);
|
||||
else log_line("HIT ip=".($_SERVER['REMOTE_ADDR']??'') ." bytes=".strlen($raw));
|
||||
if (!$raw) { log_line("NO_BODY"); exit; }
|
||||
|
||||
// 1) OAuth2
|
||||
$ch = curl_init(api_base().'/v1/oauth2/token');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER=>true,
|
||||
CURLOPT_POST=>true,
|
||||
CURLOPT_POSTFIELDS=>'grant_type=client_credentials',
|
||||
CURLOPT_HTTPHEADER=>['Accept: application/json'],
|
||||
CURLOPT_USERPWD=>$config['client_id'].':'.$config['client_secret'],
|
||||
]);
|
||||
$tokenResp = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$o = ['http'=>$http,'resp'=>substr($tokenResp,0,400)];
|
||||
if ($http!==200){ if (function_exists('site_log_warn')) site_log_warn('oauth_fail',$o); else log_line("OAUTH_FAIL http=$http resp=$tokenResp"); exit; }
|
||||
$access_token = json_decode($tokenResp, true)['access_token'] ?? null;
|
||||
if (!$access_token){ if (function_exists('site_log_warn')) site_log_warn('oauth_no_token', []); else log_line("OAUTH_NO_TOKEN"); exit; }
|
||||
|
||||
// 2) Verify webhook signature
|
||||
$verifyPayload = [
|
||||
'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? '',
|
||||
'transmission_time' => $headers['PAYPAL-TRANSMISSION-TIME'] ?? '',
|
||||
'cert_url' => $headers['PAYPAL-CERT-URL'] ?? '',
|
||||
'auth_algo' => $headers['PAYPAL-AUTH-ALGO'] ?? '',
|
||||
'transmission_sig' => $headers['PAYPAL-TRANSMISSION-SIG'] ?? '',
|
||||
'webhook_id' => $config['webhook_id'],
|
||||
'webhook_event' => json_decode($raw, true),
|
||||
];
|
||||
$ch = curl_init(api_base().'/v1/notifications/verify-webhook-signature');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER=>true,
|
||||
CURLOPT_POST=>true,
|
||||
CURLOPT_POSTFIELDS=>json_encode($verifyPayload),
|
||||
CURLOPT_HTTPHEADER=>[
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer '.$access_token
|
||||
],
|
||||
]);
|
||||
$verifyResp = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
$verifyJson = json_decode($verifyResp, true);
|
||||
if ($http!==200 || ($verifyJson['verification_status'] ?? '') !== 'SUCCESS'){
|
||||
if (function_exists('site_log_warn')) site_log_warn('verify_fail', ['http'=>$http,'status'=>($verifyJson['verification_status']??'NONE')]);
|
||||
else log_line("VERIFY_FAIL http=$http status=".($verifyJson['verification_status']??'NONE'));
|
||||
exit;
|
||||
}
|
||||
if (function_exists('site_log_info')) site_log_info('verify_ok', ['http'=>$http]);
|
||||
else log_line("VERIFY_OK");
|
||||
|
||||
// 3) Parse and persist
|
||||
$evt = json_decode($raw, true);
|
||||
$type = $evt['event_type'] ?? '';
|
||||
$res = $evt['resource'] ?? [];
|
||||
|
||||
$invoice = $res['invoice_id'] ?? ($res['invoice_number'] ?? null);
|
||||
$custom = $res['custom_id'] ?? ($res['custom'] ?? null);
|
||||
|
||||
$amount = $res['amount']['value'] ?? ($res['amount']['total'] ?? null);
|
||||
$currency = $res['amount']['currency_code'] ?? ($res['amount']['currency'] ?? null);
|
||||
$payer = $res['payer']['email_address'] ?? ($res['payer']['payer_info']['email'] ?? null);
|
||||
|
||||
$items = [];
|
||||
if (isset($res['purchase_units'][0]['items']) && is_array($res['purchase_units'][0]['items'])) {
|
||||
$items = $res['purchase_units'][0]['items'];
|
||||
}
|
||||
|
||||
if (!$items && $type === 'PAYMENT.CAPTURE.COMPLETED') {
|
||||
$orderId = $res['supplementary_data']['related_ids']['order_id'] ?? null;
|
||||
if (!$orderId && isset($res['links']) && is_array($res['links'])) {
|
||||
foreach ((array)$res['links'] as $lnk) {
|
||||
if (!empty($lnk['href']) && !empty($lnk['rel']) && stripos($lnk['href'], '/v2/checkout/orders/') !== false) {
|
||||
$orderId = basename(parse_url($lnk['href'], PHP_URL_PATH));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($orderId) {
|
||||
$ch = curl_init(api_base()."/v2/checkout/orders/".urlencode($orderId));
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [ 'Authorization: Bearer '.$access_token, 'Content-Type: application/json' ],
|
||||
]);
|
||||
$orderJson = curl_exec($ch);
|
||||
$httpOrder = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($httpOrder === 200) {
|
||||
$order = json_decode($orderJson, true);
|
||||
if (isset($order['purchase_units'][0]['items']) && is_array($order['purchase_units'][0]['items'])) {
|
||||
$items = $order['purchase_units'][0]['items'];
|
||||
}
|
||||
if (!$invoice) { $invoice = $order['purchase_units'][0]['invoice_id'] ?? $invoice; }
|
||||
if (!$custom) { $custom = $order['purchase_units'][0]['custom_id'] ?? $custom; }
|
||||
} else {
|
||||
if (function_exists('site_log_warn')) site_log_warn('order_fetch_fail', ['orderId'=>$orderId,'http'=>$httpOrder]);
|
||||
else log_line("ORDER_FETCH_FAIL id=$orderId http=$httpOrder");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$status = 'IGNORED';
|
||||
if (in_array($type, ['PAYMENT.CAPTURE.COMPLETED','PAYMENT.SALE.COMPLETED'], true)) {
|
||||
$record = [
|
||||
'event_type' => $type,
|
||||
'status' => 'PAID',
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'payer' => $payer,
|
||||
'invoice' => $invoice,
|
||||
'custom' => $custom,
|
||||
'resource_id' => $res['id'] ?? null,
|
||||
'items' => $items,
|
||||
'ts' => date('c'),
|
||||
];
|
||||
$name = $invoice ?: 'NO-INVOICE-'.bin2hex(random_bytes(4));
|
||||
@file_put_contents($config['data_dir']."/".$name.".json", json_encode($record, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
||||
$status = 'WROTE_FILE';
|
||||
|
||||
// Attempt to mark order paid in DB
|
||||
require_once(__DIR__ . '/includes/payment_processor.php');
|
||||
try {
|
||||
process_payment_record($record);
|
||||
} catch (Exception $e) {
|
||||
if (function_exists('site_log_error')) site_log_error('process_payment_fail',['err'=>$e->getMessage()]);
|
||||
else log_line('PROC_FAIL '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('site_log_info')) site_log_info('webhook_event',['type'=>$type,'invoice'=>($invoice ?: 'none'),'items_count'=>count((array)$items),'status'=>$status]);
|
||||
else log_line("EVENT $type invoice=".($invoice ?: 'none')." items_count=".count((array)$items)." status=$status");
|
||||
|
||||
?>
|
||||
|
||||
|
||||
http_response_code(200);
|
||||
@mkdir($config['data_dir'], 0775, true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue