Merge pull request #132 from GameServerPanel/copilot/fix-paypal-admin-issues
This commit is contained in:
commit
bf5918da66
7 changed files with 535 additions and 81 deletions
|
|
@ -626,15 +626,15 @@ rsort($bakFiles); // newest first
|
||||||
// Gather diagnostics data
|
// Gather diagnostics data
|
||||||
$diag_mode = $cfgVals['paypal_mode'] ?? 'sandbox';
|
$diag_mode = $cfgVals['paypal_mode'] ?? 'sandbox';
|
||||||
$diag_is_sandbox = $diag_mode !== 'live';
|
$diag_is_sandbox = $diag_mode !== 'live';
|
||||||
$diag_sb_id_set = $cfgVals['paypal_sandbox_client_id'] !== '';
|
$diag_sb_id_set = ($cfgVals['paypal_sandbox_client_id'] ?? '') !== '';
|
||||||
$diag_sb_sec_set = $cfgVals['paypal_sandbox_client_secret'] !== '';
|
$diag_sb_sec_set = ($cfgVals['paypal_sandbox_client_secret'] ?? '') !== '';
|
||||||
$diag_sb_wh_set = $cfgVals['paypal_sandbox_webhook_id'] !== '';
|
$diag_sb_wh_set = ($cfgVals['paypal_sandbox_webhook_id'] ?? '') !== '';
|
||||||
$diag_lv_id_set = $cfgVals['paypal_live_client_id'] !== '';
|
$diag_lv_id_set = ($cfgVals['paypal_live_client_id'] ?? '') !== '';
|
||||||
$diag_lv_sec_set = $cfgVals['paypal_live_client_secret'] !== '';
|
$diag_lv_sec_set = ($cfgVals['paypal_live_client_secret'] ?? '') !== '';
|
||||||
$diag_lv_wh_set = $cfgVals['paypal_live_webhook_id'] !== '';
|
$diag_lv_wh_set = ($cfgVals['paypal_live_webhook_id'] ?? '') !== '';
|
||||||
$diag_wh_path = $cfgVals['paypal_webhook_path'] ?? '/paypal/webhook.php';
|
$diag_wh_path = '/' . ltrim((string)($cfgVals['paypal_webhook_path'] ?? '/paypal/webhook.php'), '/');
|
||||||
$diag_wh_full_url = $computedWebhookUrl;
|
$diag_wh_full_url = $computedWebhookUrl;
|
||||||
$diag_wh_file = __DIR__ . ltrim($diag_wh_path, '/');
|
$diag_wh_file = rtrim(__DIR__, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($diag_wh_path, '/');
|
||||||
$diag_wh_exists = file_exists($diag_wh_file);
|
$diag_wh_exists = file_exists($diag_wh_file);
|
||||||
|
|
||||||
// Active mode credential check
|
// Active mode credential check
|
||||||
|
|
@ -645,65 +645,214 @@ rsort($bakFiles); // newest first
|
||||||
function diag_badge(bool $ok, string $yes = 'Yes', string $no = 'No'): string {
|
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;';
|
$cls = $ok ? 'background:#d4edda;color:#155724;border:1px solid #c3e6cb;' : 'background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;';
|
||||||
$label = $ok ? $yes : $no;
|
$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>';
|
return '<span style="' . $cls . 'padding:2px 8px;border-radius:3px;font-size:0.85em;font-weight:600;display:inline-block;word-break:break-word;">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last webhook events
|
// Last webhook events + recent PayPal errors
|
||||||
$diag_recent_events = [];
|
$diag_recent_events = [];
|
||||||
|
$diag_recent_errors = [];
|
||||||
|
$diag_errors_warning = '';
|
||||||
try {
|
try {
|
||||||
$port_int = intval($db_port ?? 3306) ?: 3306;
|
$port_int = intval($db_port ?? 3306) ?: 3306;
|
||||||
$diag_db = @mysqli_connect($db_host ?? 'localhost', $db_user ?? '', $db_pass ?? '', $db_name ?? '', $port_int);
|
$diag_db = @mysqli_connect($db_host ?? 'localhost', $db_user ?? '', $db_pass ?? '', $db_name ?? '', $port_int);
|
||||||
if ($diag_db) {
|
if ($diag_db) {
|
||||||
$pfx_diag = $table_prefix ?? 'gsp_';
|
$pfx_diag = $table_prefix ?? 'gsp_';
|
||||||
|
mysqli_set_charset($diag_db, 'utf8mb4');
|
||||||
|
|
||||||
$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");
|
$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) {
|
if ($res) {
|
||||||
while ($row = mysqli_fetch_assoc($res)) {
|
while ($row = mysqli_fetch_assoc($res)) {
|
||||||
$diag_recent_events[] = $row;
|
$diag_recent_events[] = $row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recent PayPal errors — use BillingRepository for safe table creation
|
||||||
|
require_once __DIR__ . '/classes/BillingRepository.php';
|
||||||
|
$diag_repo = new BillingRepository($diag_db, $pfx_diag);
|
||||||
|
if ($diag_repo->ensureBillingPaypalErrorsTable()) {
|
||||||
|
$diag_recent_errors = $diag_repo->getRecentPaypalErrors(10);
|
||||||
|
} else {
|
||||||
|
$diag_errors_warning = 'Could not create billing_paypal_errors table. Check DB permissions.';
|
||||||
|
}
|
||||||
|
|
||||||
mysqli_close($diag_db);
|
mysqli_close($diag_db);
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
// non-fatal
|
$diag_errors_warning = 'Diagnostics DB query failed: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
<style>
|
||||||
|
.diag-grid { display:grid; grid-template-columns:1fr; gap:8px; font-size:0.9em; }
|
||||||
|
@media (min-width:600px) { .diag-grid { grid-template-columns:220px 1fr; } }
|
||||||
|
.diag-row { display:contents; }
|
||||||
|
.diag-label { color:#555; font-weight:600; padding:6px 0; border-bottom:1px solid #f0f0f0; word-break:break-word; }
|
||||||
|
.diag-value { padding:6px 0; border-bottom:1px solid #f0f0f0; word-break:break-all; }
|
||||||
|
.diag-sub { font-size:0.85em; color:#888; margin-top:4px; }
|
||||||
|
.diag-sep { grid-column:1/-1; border-top:2px solid #e9ecef; margin:6px 0 2px; }
|
||||||
|
.recent-errors-table { width:100%; border-collapse:collapse; font-size:0.85em; overflow-x:auto; display:block; }
|
||||||
|
.recent-errors-table th { background:#f8f9fa; padding:6px 8px; text-align:left; border-bottom:2px solid #dee2e6; white-space:nowrap; }
|
||||||
|
.recent-errors-table td { padding:5px 8px; border-bottom:1px solid #eee; word-break:break-word; }
|
||||||
|
</style>
|
||||||
<div class="cfg-section">
|
<div class="cfg-section">
|
||||||
<h2>PayPal Diagnostics</h2>
|
<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>
|
<!-- Self-check button -->
|
||||||
<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>
|
<form method="post" style="margin-bottom:16px;">
|
||||||
<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>
|
<input type="hidden" name="csrf" value="<?php echo h($csrf); ?>">
|
||||||
<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>
|
<input type="hidden" name="action" value="self_check">
|
||||||
<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>
|
<button type="submit" class="btn-show" style="padding:9px 18px;font-size:0.95em;">🔍 Run Billing Self-Check</button>
|
||||||
<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>
|
</form>
|
||||||
<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>
|
<?php
|
||||||
<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>
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'self_check') {
|
||||||
<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>
|
$token = $_POST['csrf'] ?? '';
|
||||||
</table>
|
if (hash_equals($csrf, (string)$token)):
|
||||||
|
?>
|
||||||
|
<div class="status-box status-info" style="font-size:0.9em;">
|
||||||
|
<strong>Self-Check Results:</strong><br>
|
||||||
|
• Mode: <strong><?php echo h($diag_mode ?: '(unknown)'); ?></strong><br>
|
||||||
|
• Active Client ID: <?php echo $diag_active_id_set ? '✅ configured' : '❌ missing'; ?><br>
|
||||||
|
• Active Client Secret: <?php echo $diag_active_sec_set ? '✅ configured' : '❌ missing'; ?><br>
|
||||||
|
• Active Webhook ID: <?php echo $diag_active_wh_set ? '✅ configured' : '⚠️ missing (signature verification skipped)'; ?><br>
|
||||||
|
• Webhook file: <?php echo $diag_wh_exists ? '✅ exists' : '❌ not found'; ?> — <code style="word-break:break-all"><?php echo h($diag_wh_file); ?></code><br>
|
||||||
|
• Logs directory: <?php $logDir = __DIR__ . '/logs'; echo (is_dir($logDir) && is_writable($logDir)) ? '✅ writable' : '⚠️ ' . (is_dir($logDir) ? 'not writable' : 'missing'); ?><br>
|
||||||
|
• Data directory: <?php echo (is_dir($SITE_DATA_DIR ?? '') && is_writable($SITE_DATA_DIR ?? '')) ? '✅ writable' : '⚠️ check path'; ?><br>
|
||||||
|
• Config file: <?php echo is_writable($cfgPath) ? '✅ writable' : '⚠️ read-only'; ?><br>
|
||||||
|
</div>
|
||||||
|
<?php endif; } ?>
|
||||||
|
|
||||||
|
<div class="diag-grid">
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Current mode</div>
|
||||||
|
<div class="diag-value">
|
||||||
|
<strong><?php echo h($diag_mode !== '' ? $diag_mode : '(not set)'); ?></strong>
|
||||||
|
<?php if ($diag_mode === 'sandbox'): ?>
|
||||||
|
<span style="background:#fff3cd;color:#856404;border:1px solid #ffc107;padding:1px 7px;border-radius:3px;font-size:0.8em;margin-left:6px;">test</span>
|
||||||
|
<?php elseif ($diag_mode === 'live'): ?>
|
||||||
|
<span style="background:#d4edda;color:#155724;border:1px solid #c3e6cb;padding:1px 7px;border-radius:3px;font-size:0.8em;margin-left:6px;">live</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="diag-sep"></div>
|
||||||
|
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Active Client ID</div>
|
||||||
|
<div class="diag-value"><?php echo diag_badge($diag_active_id_set); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Active Client Secret</div>
|
||||||
|
<div class="diag-value"><?php echo diag_badge($diag_active_sec_set); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Active Webhook ID</div>
|
||||||
|
<div class="diag-value"><?php echo diag_badge($diag_active_wh_set, 'Yes', 'No — signature verification skipped'); ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="diag-sep"></div>
|
||||||
|
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Sandbox Client ID</div>
|
||||||
|
<div class="diag-value"><?php echo diag_badge($diag_sb_id_set); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Sandbox Client Secret</div>
|
||||||
|
<div class="diag-value"><?php echo diag_badge($diag_sb_sec_set); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Sandbox Webhook ID</div>
|
||||||
|
<div class="diag-value"><?php echo diag_badge($diag_sb_wh_set); ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="diag-sep"></div>
|
||||||
|
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Live Client ID</div>
|
||||||
|
<div class="diag-value"><?php echo diag_badge($diag_lv_id_set); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Live Client Secret</div>
|
||||||
|
<div class="diag-value"><?php echo diag_badge($diag_lv_sec_set); ?></div>
|
||||||
|
</div>
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Live Webhook ID</div>
|
||||||
|
<div class="diag-value"><?php echo diag_badge($diag_lv_wh_set); ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="diag-sep"></div>
|
||||||
|
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Webhook path</div>
|
||||||
|
<div class="diag-value"><code><?php echo h($diag_wh_path); ?></code></div>
|
||||||
|
</div>
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Full public webhook URL</div>
|
||||||
|
<div class="diag-value">
|
||||||
|
<code><?php echo h($diag_wh_full_url !== '' ? $diag_wh_full_url : '(Site Base URL not configured)'); ?></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="diag-row">
|
||||||
|
<div class="diag-label">Webhook file on disk</div>
|
||||||
|
<div class="diag-value">
|
||||||
|
<?php echo diag_badge($diag_wh_exists, 'Found', 'Not found'); ?>
|
||||||
|
<div class="diag-sub"><code><?php echo h($diag_wh_file); ?></code></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php if (!empty($diag_recent_events)): ?>
|
<?php if (!empty($diag_recent_events)): ?>
|
||||||
<h4 style="margin-top:18px;color:#555;">Recent Webhook Events</h4>
|
<h4 style="margin-top:22px;color:#555;">Recent Webhook Events</h4>
|
||||||
<table style="border-collapse:collapse;width:100%;font-size:0.85em;">
|
<div style="overflow-x:auto;">
|
||||||
<thead><tr style="background:#f8f9fa;">
|
<table class="recent-errors-table">
|
||||||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">PayPal Event ID</th>
|
<thead><tr>
|
||||||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">Type</th>
|
<th>PayPal Event ID</th>
|
||||||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">Status</th>
|
<th>Type</th>
|
||||||
<th style="padding:6px 8px;text-align:left;border-bottom:2px solid #dee2e6;">Received</th>
|
<th>Status</th>
|
||||||
|
<th>Received</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($diag_recent_events as $ev): ?>
|
<?php foreach ($diag_recent_events as $ev): ?>
|
||||||
<tr style="border-bottom:1px solid #eee;">
|
<tr>
|
||||||
<td style="padding:5px 8px;font-family:monospace;font-size:0.85em;"><?php echo h($ev['paypal_event_id'] ?: '—'); ?></td>
|
<td><code><?php echo h($ev['paypal_event_id'] ?: '—'); ?></code></td>
|
||||||
<td style="padding:5px 8px;"><?php echo h($ev['event_type']); ?></td>
|
<td><?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><?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>
|
<td><?php echo h($ev['created_at']); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<?php elseif (empty($diag_recent_events)): ?>
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
<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>
|
<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; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<h4 style="margin-top:22px;color:#555;">Recent PayPal Errors</h4>
|
||||||
|
<?php if ($diag_errors_warning): ?>
|
||||||
|
<div class="warn-box"><?php echo h($diag_errors_warning); ?></div>
|
||||||
|
<?php elseif (empty($diag_recent_errors)): ?>
|
||||||
|
<p style="color:#888;font-size:0.9em;">No PayPal errors logged yet.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table class="recent-errors-table">
|
||||||
|
<thead><tr>
|
||||||
|
<th>Time</th><th>Context</th><th>Error Code</th><th>Message</th>
|
||||||
|
<th>Debug ID</th><th>Order ID</th><th>User</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($diag_recent_errors as $er): ?>
|
||||||
|
<tr>
|
||||||
|
<td style="white-space:nowrap"><?php echo h($er['created_at']); ?></td>
|
||||||
|
<td><?php echo h($er['context']); ?></td>
|
||||||
|
<td><code><?php echo h($er['error_code']); ?></code></td>
|
||||||
|
<td><?php echo h($er['message']); ?></td>
|
||||||
|
<td><code><?php echo h($er['paypal_debug_id'] ?? '—'); ?></code></td>
|
||||||
|
<td><code><?php echo h($er['order_id'] ?? '—'); ?></code></td>
|
||||||
|
<td><?php echo h($er['user_id'] ?? '—'); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===================================================================
|
<!-- ===================================================================
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ if (!$db) {
|
||||||
if (!empty($_GET['home_id'])) $filter['home_id'] = intval($_GET['home_id']);
|
if (!empty($_GET['home_id'])) $filter['home_id'] = intval($_GET['home_id']);
|
||||||
if (!empty($_GET['payment_method'])) $filter['payment_method'] = trim($_GET['payment_method']);
|
if (!empty($_GET['payment_method'])) $filter['payment_method'] = trim($_GET['payment_method']);
|
||||||
|
|
||||||
$transactions = $repo->getTransactions($filter, 200, 0);
|
try {
|
||||||
|
$transactions = $repo->getTransactions($filter, 200, 0);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$errorMsg = 'Could not load transactions: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
mysqli_close($db);
|
mysqli_close($db);
|
||||||
$db = null;
|
$db = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,13 @@ $userId = intval($_SESSION['website_user_id'] ?? $_SESSION['user_id'] ?? 0);
|
||||||
if ($userId <= 0) {
|
if ($userId <= 0) {
|
||||||
cap_log('NO_USER_SESSION', ['session_keys' => array_keys($_SESSION)]);
|
cap_log('NO_USER_SESSION', ['session_keys' => array_keys($_SESSION)]);
|
||||||
ob_clean();
|
ob_clean();
|
||||||
echo json_encode(['error' => 'no_user_session', 'request_id' => $requestId]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error_code' => 'no_user_session',
|
||||||
|
'message' => 'You must be logged in to complete payment.',
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'request_id' => $requestId,
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,14 +57,26 @@ $rawInput = file_get_contents('php://input');
|
||||||
$input = json_decode($rawInput, true);
|
$input = json_decode($rawInput, true);
|
||||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
ob_clean();
|
ob_clean();
|
||||||
echo json_encode(['error' => 'invalid_json', 'request_id' => $requestId]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error_code' => 'invalid_json',
|
||||||
|
'message' => 'Invalid JSON in request body.',
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'request_id' => $requestId,
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$paypalOrderId = $input['order_id'] ?? null;
|
$paypalOrderId = $input['order_id'] ?? null;
|
||||||
if (!$paypalOrderId) {
|
if (!$paypalOrderId) {
|
||||||
ob_clean();
|
ob_clean();
|
||||||
echo json_encode(['error' => 'missing_order_id', 'request_id' => $requestId]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error_code' => 'missing_order_id',
|
||||||
|
'message' => 'Missing PayPal order ID.',
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'request_id' => $requestId,
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +88,13 @@ $mysqli = @mysqli_connect($db_host, $db_user, $db_pass, $db_name, $port);
|
||||||
if (!$mysqli) {
|
if (!$mysqli) {
|
||||||
cap_log('DB_FAILED', mysqli_connect_error());
|
cap_log('DB_FAILED', mysqli_connect_error());
|
||||||
ob_clean();
|
ob_clean();
|
||||||
echo json_encode(['error' => 'db_connection_failed', 'request_id' => $requestId]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error_code' => 'db_connection_failed',
|
||||||
|
'message' => 'Database connection failed.',
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'request_id' => $requestId,
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
mysqli_set_charset($mysqli, 'utf8mb4');
|
mysqli_set_charset($mysqli, 'utf8mb4');
|
||||||
|
|
@ -84,8 +108,21 @@ try {
|
||||||
$gateway = GatewayFactory::make('paypal');
|
$gateway = GatewayFactory::make('paypal');
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
cap_log('GATEWAY_ERROR', $e->getMessage());
|
cap_log('GATEWAY_ERROR', $e->getMessage());
|
||||||
|
$repo->logPaypalError([
|
||||||
|
'context' => 'gateway_init',
|
||||||
|
'error_code' => 'gateway_init_failed',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'order_id' => $paypalOrderId,
|
||||||
|
'user_id' => $userId,
|
||||||
|
]);
|
||||||
ob_clean();
|
ob_clean();
|
||||||
echo json_encode(['error' => 'gateway_init_failed', 'request_id' => $requestId]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error_code' => 'gateway_init_failed',
|
||||||
|
'message' => 'Payment gateway initialisation failed.',
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'request_id' => $requestId,
|
||||||
|
]);
|
||||||
mysqli_close($mysqli);
|
mysqli_close($mysqli);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
@ -95,8 +132,29 @@ cap_log('CAPTURE_RESULT', ['success' => $capture['success'], 'txid' => $capture[
|
||||||
|
|
||||||
if (!$capture['success']) {
|
if (!$capture['success']) {
|
||||||
cap_log('CAPTURE_FAILED', $capture);
|
cap_log('CAPTURE_FAILED', $capture);
|
||||||
|
// Sanitize raw capture data before logging — never store secrets
|
||||||
|
$captureForLog = $capture;
|
||||||
|
foreach (['client_secret', 'access_token', 'refresh_token'] as $_sk) {
|
||||||
|
unset($captureForLog[$_sk]);
|
||||||
|
}
|
||||||
|
$repo->logPaypalError([
|
||||||
|
'context' => 'capture_order',
|
||||||
|
'error_code' => $capture['error'] ?? 'capture_failed',
|
||||||
|
'message' => $capture['message'] ?? 'PayPal order capture failed.',
|
||||||
|
'paypal_debug_id' => $capture['debug_id'] ?? null,
|
||||||
|
'order_id' => $paypalOrderId,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'raw_json' => $captureForLog,
|
||||||
|
]);
|
||||||
ob_clean();
|
ob_clean();
|
||||||
echo json_encode(['error' => $capture['error'] ?? 'capture_failed', 'request_id' => $requestId]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error_code' => $capture['error'] ?? 'capture_failed',
|
||||||
|
'message' => $capture['message'] ?? 'PayPal order capture failed. Please try again.',
|
||||||
|
'debug_id' => $capture['debug_id'] ?? null,
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'request_id' => $requestId,
|
||||||
|
]);
|
||||||
mysqli_close($mysqli);
|
mysqli_close($mysqli);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
@ -128,6 +186,23 @@ foreach ($invoices as $inv) {
|
||||||
$invoicesPaid++;
|
$invoicesPaid++;
|
||||||
cap_log('INVOICE_PAID', ['invoice_id' => $invoiceId, 'txid' => $txid]);
|
cap_log('INVOICE_PAID', ['invoice_id' => $invoiceId, 'txid' => $txid]);
|
||||||
|
|
||||||
|
// Record transaction in billing_transactions (idempotent — skip on duplicate external ID)
|
||||||
|
$rawCapture = $capture['raw_response'] ?? [];
|
||||||
|
if (is_array($rawCapture)) {
|
||||||
|
unset($rawCapture['client_secret'], $rawCapture['access_token']); // never log secrets
|
||||||
|
}
|
||||||
|
$repo->logTransaction([
|
||||||
|
'invoice_id' => $invoiceId,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'home_id' => $homeId,
|
||||||
|
'payment_method' => 'paypal',
|
||||||
|
'transaction_external_id' => $txid,
|
||||||
|
'amount' => (float)($inv['amount'] ?? $inv['total_due'] ?? 0),
|
||||||
|
'currency' => (string)($inv['currency'] ?? 'USD'),
|
||||||
|
'status' => 'completed',
|
||||||
|
'raw_response' => $rawCapture,
|
||||||
|
]);
|
||||||
|
|
||||||
// Resolve (or create) the billing_orders row for this invoice so the provisioner can run.
|
// Resolve (or create) the billing_orders row for this invoice so the provisioner can run.
|
||||||
// billing_orders.status='Active' is what create_servers.php queries.
|
// billing_orders.status='Active' is what create_servers.php queries.
|
||||||
$orderId = intval($inv['order_id'] ?? 0);
|
$orderId = intval($inv['order_id'] ?? 0);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,13 @@ $rawInput = file_get_contents('php://input');
|
||||||
$in = json_decode($rawInput, true);
|
$in = json_decode($rawInput, true);
|
||||||
if (json_last_error() !== JSON_ERROR_NONE || !$in) {
|
if (json_last_error() !== JSON_ERROR_NONE || !$in) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['error' => 'invalid_json', 'request_id' => $requestId]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error_code' => 'invalid_json',
|
||||||
|
'message' => 'Invalid JSON in request body.',
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'request_id' => $requestId,
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,14 +75,28 @@ try {
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
co_log('EXCEPTION', $e->getMessage());
|
co_log('EXCEPTION', $e->getMessage());
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['error' => 'gateway_error', 'message' => $e->getMessage(), 'request_id' => $requestId]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error_code' => 'gateway_error',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'debug_id' => null,
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'request_id' => $requestId,
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$result['success']) {
|
if (!$result['success']) {
|
||||||
co_log('CREATE_FAILED', $result);
|
co_log('CREATE_FAILED', $result);
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['error' => $result['error'] ?? 'create_failed', 'request_id' => $requestId]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error_code' => $result['error'] ?? 'create_failed',
|
||||||
|
'message' => $result['message'] ?? 'Failed to create PayPal order.',
|
||||||
|
'debug_id' => $result['debug_id'] ?? null,
|
||||||
|
'timestamp' => date('c'),
|
||||||
|
'request_id' => $requestId,
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -686,6 +686,36 @@ $siteBase = $protocol . $host;
|
||||||
|
|
||||||
<?php if ($final_amount > 0.00 && !empty($client_id)): ?>
|
<?php if ($final_amount > 0.00 && !empty($client_id)): ?>
|
||||||
<script>
|
<script>
|
||||||
|
function showPaymentError(msg) {
|
||||||
|
var statusDiv = document.getElementById('status-message');
|
||||||
|
if (statusDiv) {
|
||||||
|
statusDiv.textContent = msg;
|
||||||
|
statusDiv.style.display = 'block';
|
||||||
|
statusDiv.style.color = '#721c24';
|
||||||
|
statusDiv.style.background = '#f8d7da';
|
||||||
|
statusDiv.style.border = '1px solid #f5c6cb';
|
||||||
|
statusDiv.style.padding = '12px 16px';
|
||||||
|
statusDiv.style.borderRadius = '4px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logErrorToServer(context, errorCode, message, debugId, orderId) {
|
||||||
|
try {
|
||||||
|
fetch('/api/log_error.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
context: context,
|
||||||
|
error_code: errorCode,
|
||||||
|
message: message,
|
||||||
|
paypal_debug_id: debugId || null,
|
||||||
|
order_id: orderId || null,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
}).catch(function() {}); // silently ignore logging failures
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
paypal.Buttons({
|
paypal.Buttons({
|
||||||
createOrder: function(data, actions) {
|
createOrder: function(data, actions) {
|
||||||
setStatus('Creating order...');
|
setStatus('Creating order...');
|
||||||
|
|
@ -712,48 +742,56 @@ $siteBase = $protocol . $host;
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onApprove: function(data, actions) {
|
onApprove: function(data, actions) {
|
||||||
setStatus('Processing payment...');
|
setStatus('Processing payment...');
|
||||||
|
|
||||||
// Capture the order via our backend
|
|
||||||
return fetch('/api/capture_order.php', {
|
return fetch('/api/capture_order.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ order_id: data.orderID })
|
body: JSON.stringify({ order_id: data.orderID })
|
||||||
})
|
})
|
||||||
.then(function(res) {
|
.then(function(res) {
|
||||||
if (!res.ok) {
|
return res.json().then(function(body) {
|
||||||
return res.text().then(function(text) {
|
return { ok: res.ok, body: body };
|
||||||
throw new Error('Payment capture failed: ' + text);
|
}).catch(function() {
|
||||||
});
|
return { ok: false, body: { error_code: 'invalid_response', message: 'Server returned non-JSON response (HTTP ' + res.status + ').' } };
|
||||||
}
|
});
|
||||||
return res.json();
|
|
||||||
})
|
})
|
||||||
.then(function(orderData) {
|
.then(function(result) {
|
||||||
console.log('Capture result:', orderData);
|
if (!result.ok || result.body.success === false) {
|
||||||
if (orderData.status === 'COMPLETED') {
|
var errCode = result.body.error_code || result.body.error || 'capture_failed';
|
||||||
|
var errMsg = result.body.message || 'Payment capture failed. Please try again or contact support.';
|
||||||
|
var debugId = result.body.debug_id || null;
|
||||||
|
logErrorToServer('cart_capture', errCode, errMsg, debugId, data.orderID);
|
||||||
|
showPaymentError('Payment failed: ' + errMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// status=COMPLETED is the success indicator
|
||||||
|
if (result.body.status === 'COMPLETED') {
|
||||||
setStatus('Payment successful! Redirecting...');
|
setStatus('Payment successful! Redirecting...');
|
||||||
window.location.href = '/payment_success.php?order_id=' + data.orderID;
|
window.location.href = '/payment_success.php?order_id=' + encodeURIComponent(data.orderID);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unexpected payment status: ' + orderData.status);
|
var unexpectedMsg = 'Unexpected payment status: ' + (result.body.status || 'unknown');
|
||||||
|
logErrorToServer('cart_capture', 'unexpected_status', unexpectedMsg, null, data.orderID);
|
||||||
|
showPaymentError(unexpectedMsg + '. Please contact support.');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error('Payment error:', err);
|
var errMsg = err && err.message ? err.message : 'Network error during payment capture.';
|
||||||
setStatus('Error: ' + err.message);
|
logErrorToServer('cart_capture', 'fetch_error', errMsg, null, data.orderID);
|
||||||
alert('Payment processing failed. Please try again or contact support.');
|
showPaymentError('Payment error: ' + errMsg);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: function(err) {
|
onError: function(err) {
|
||||||
console.error('PayPal error:', err);
|
var errMsg = err && err.message ? err.message : String(err);
|
||||||
setStatus('Payment error occurred');
|
logErrorToServer('cart_paypal_sdk', 'sdk_error', errMsg, null, null);
|
||||||
alert('An error occurred during payment. Please try again.');
|
showPaymentError('A PayPal error occurred. Please try again or contact support.');
|
||||||
},
|
},
|
||||||
|
|
||||||
onCancel: function(data) {
|
onCancel: function(data) {
|
||||||
setStatus('Payment cancelled');
|
setStatus('Payment cancelled.');
|
||||||
window.location.href = '/payment_cancel.php';
|
window.location.href = '/payment_cancel.php';
|
||||||
}
|
}
|
||||||
}).render('#paypal-button-container');
|
}).render('#paypal-button-container');
|
||||||
|
|
|
||||||
|
|
@ -156,13 +156,90 @@ class BillingRepository
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Safe table-creation helpers (idempotent, check INFORMATION_SCHEMA first)
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure billing_transactions table exists.
|
||||||
|
* Safe to call on every request; uses INFORMATION_SCHEMA to skip if already present.
|
||||||
|
*/
|
||||||
|
public function ensureBillingTransactionsTable(): bool
|
||||||
|
{
|
||||||
|
$res = $this->db->query(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = '{$this->prefix}billing_transactions'"
|
||||||
|
);
|
||||||
|
if ($res && (int)$res->fetch_assoc()['cnt'] > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (bool)$this->db->query(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `{$this->prefix}billing_transactions` (
|
||||||
|
`transaction_id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`invoice_id` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`user_id` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`home_id` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`payment_method` VARCHAR(50) NOT NULL DEFAULT 'paypal',
|
||||||
|
`transaction_external_id` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
`amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
|
||||||
|
`currency` VARCHAR(3) NOT NULL DEFAULT 'USD',
|
||||||
|
`status` ENUM('pending','completed','failed','refunded') NOT NULL DEFAULT 'pending',
|
||||||
|
`raw_response` MEDIUMTEXT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`transaction_id`),
|
||||||
|
KEY `invoice_id` (`invoice_id`),
|
||||||
|
KEY `user_id` (`user_id`),
|
||||||
|
KEY `home_id` (`home_id`),
|
||||||
|
KEY `status` (`status`),
|
||||||
|
KEY `payment_method` (`payment_method`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure billing_paypal_errors table exists.
|
||||||
|
* Safe to call on every request; uses INFORMATION_SCHEMA to skip if already present.
|
||||||
|
*/
|
||||||
|
public function ensureBillingPaypalErrorsTable(): bool
|
||||||
|
{
|
||||||
|
$res = $this->db->query(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = '{$this->prefix}billing_paypal_errors'"
|
||||||
|
);
|
||||||
|
if ($res && (int)$res->fetch_assoc()['cnt'] > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (bool)$this->db->query(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `{$this->prefix}billing_paypal_errors` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`context` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
`error_code` VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
`message` TEXT NULL,
|
||||||
|
`paypal_debug_id` VARCHAR(128) NULL,
|
||||||
|
`order_id` VARCHAR(128) NULL,
|
||||||
|
`capture_id` VARCHAR(128) NULL,
|
||||||
|
`billing_order_id` INT NULL,
|
||||||
|
`user_id` INT NULL,
|
||||||
|
`raw_json` LONGTEXT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_context` (`context`),
|
||||||
|
KEY `idx_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// Transaction (payment log) helpers
|
// Transaction (payment log) helpers
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Insert a row into gsp_billing_transactions. Returns new transaction_id. */
|
/** Insert a row into billing_transactions. Returns new transaction_id. */
|
||||||
public function logTransaction(array $data): int
|
public function logTransaction(array $data): int
|
||||||
{
|
{
|
||||||
|
$this->ensureBillingTransactionsTable();
|
||||||
$stmt = $this->db->prepare(
|
$stmt = $this->db->prepare(
|
||||||
"INSERT INTO `{$this->prefix}billing_transactions`
|
"INSERT INTO `{$this->prefix}billing_transactions`
|
||||||
(invoice_id, user_id, home_id, payment_method, transaction_external_id,
|
(invoice_id, user_id, home_id, payment_method, transaction_external_id,
|
||||||
|
|
@ -171,17 +248,17 @@ class BillingRepository
|
||||||
);
|
);
|
||||||
if (!$stmt) return 0;
|
if (!$stmt) return 0;
|
||||||
$rawJson = is_array($data['raw_response']) ? json_encode($data['raw_response']) : (string)($data['raw_response'] ?? '');
|
$rawJson = is_array($data['raw_response']) ? json_encode($data['raw_response']) : (string)($data['raw_response'] ?? '');
|
||||||
|
$invoiceId = intval($data['invoice_id'] ?? 0);
|
||||||
|
$userId = intval($data['user_id'] ?? 0);
|
||||||
|
$homeId = intval($data['home_id'] ?? 0);
|
||||||
|
$method = (string)($data['payment_method'] ?? 'paypal');
|
||||||
|
$extId = (string)($data['transaction_external_id'] ?? '');
|
||||||
|
$amount = (float)($data['amount'] ?? 0);
|
||||||
|
$currency = (string)($data['currency'] ?? 'USD');
|
||||||
|
$status = (string)($data['status'] ?? 'completed');
|
||||||
$stmt->bind_param(
|
$stmt->bind_param(
|
||||||
'iiissdsss',
|
'iiissdsss',
|
||||||
$data['invoice_id'],
|
$invoiceId, $userId, $homeId, $method, $extId, $amount, $currency, $status, $rawJson
|
||||||
$data['user_id'],
|
|
||||||
$data['home_id'],
|
|
||||||
$data['payment_method'],
|
|
||||||
$data['transaction_external_id'],
|
|
||||||
$data['amount'],
|
|
||||||
$data['currency'],
|
|
||||||
$data['status'],
|
|
||||||
$rawJson
|
|
||||||
);
|
);
|
||||||
if (!$stmt->execute()) { $stmt->close(); return 0; }
|
if (!$stmt->execute()) { $stmt->close(); return 0; }
|
||||||
$id = (int)$stmt->insert_id;
|
$id = (int)$stmt->insert_id;
|
||||||
|
|
@ -189,9 +266,12 @@ class BillingRepository
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get all transactions, optionally filtered. */
|
/** Get all transactions, optionally filtered. Creates the table if missing. */
|
||||||
public function getTransactions(array $filter = [], int $limit = 100, int $offset = 0): array
|
public function getTransactions(array $filter = [], int $limit = 100, int $offset = 0): array
|
||||||
{
|
{
|
||||||
|
if (!$this->ensureBillingTransactionsTable()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
$where = '1=1';
|
$where = '1=1';
|
||||||
$params = [];
|
$params = [];
|
||||||
$types = '';
|
$types = '';
|
||||||
|
|
@ -226,6 +306,72 @@ class BillingRepository
|
||||||
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// PayPal error log helpers
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a row into billing_paypal_errors. Never logs client secrets.
|
||||||
|
* Returns new error log id (0 on failure).
|
||||||
|
*/
|
||||||
|
public function logPaypalError(array $data): int
|
||||||
|
{
|
||||||
|
$this->ensureBillingPaypalErrorsTable();
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"INSERT INTO `{$this->prefix}billing_paypal_errors`
|
||||||
|
(context, error_code, message, paypal_debug_id, order_id, capture_id,
|
||||||
|
billing_order_id, user_id, raw_json)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
if (!$stmt) return 0;
|
||||||
|
$context = substr((string)($data['context'] ?? ''), 0, 64);
|
||||||
|
$errorCode = substr((string)($data['error_code'] ?? ''), 0, 128);
|
||||||
|
$message = (string)($data['message'] ?? '');
|
||||||
|
$debugId = isset($data['paypal_debug_id']) ? substr((string)$data['paypal_debug_id'], 0, 128) : null;
|
||||||
|
$orderId = isset($data['order_id']) ? substr((string)$data['order_id'], 0, 128) : null;
|
||||||
|
$captureId = isset($data['capture_id']) ? substr((string)$data['capture_id'], 0, 128) : null;
|
||||||
|
$billingOrderId = isset($data['billing_order_id']) ? intval($data['billing_order_id']) : null;
|
||||||
|
$userId = isset($data['user_id']) ? intval($data['user_id']) : null;
|
||||||
|
$rawJson = isset($data['raw_json'])
|
||||||
|
? (is_array($data['raw_json']) ? json_encode($data['raw_json']) : (string)$data['raw_json'])
|
||||||
|
: null;
|
||||||
|
// Truncate large payloads to avoid LONGTEXT bloat
|
||||||
|
if ($rawJson !== null && strlen($rawJson) > 65536) {
|
||||||
|
$rawJson = substr($rawJson, 0, 65536) . '…[truncated]';
|
||||||
|
}
|
||||||
|
$stmt->bind_param(
|
||||||
|
'ssssssiis',
|
||||||
|
$context, $errorCode, $message, $debugId, $orderId, $captureId,
|
||||||
|
$billingOrderId, $userId, $rawJson
|
||||||
|
);
|
||||||
|
if (!$stmt->execute()) { $stmt->close(); return 0; }
|
||||||
|
$id = (int)$stmt->insert_id;
|
||||||
|
$stmt->close();
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the $limit most recent rows from billing_paypal_errors.
|
||||||
|
* Returns empty array if the table does not exist.
|
||||||
|
*/
|
||||||
|
public function getRecentPaypalErrors(int $limit = 10): array
|
||||||
|
{
|
||||||
|
if (!$this->ensureBillingPaypalErrorsTable()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$stmt = $this->db->prepare(
|
||||||
|
"SELECT id, created_at, context, error_code, message,
|
||||||
|
paypal_debug_id, order_id, capture_id, billing_order_id, user_id
|
||||||
|
FROM `{$this->prefix}billing_paypal_errors`
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?"
|
||||||
|
);
|
||||||
|
if (!$stmt) return [];
|
||||||
|
$stmt->bind_param('i', $limit);
|
||||||
|
$stmt->execute();
|
||||||
|
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// Server home (billing state) helpers
|
// Server home (billing state) helpers
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@
|
||||||
|
|
||||||
// Module general information
|
// Module general information
|
||||||
$module_title = "billing";
|
$module_title = "billing";
|
||||||
$module_version = "3.3";
|
$module_version = "3.4";
|
||||||
$db_version = 3;
|
$db_version = 4;
|
||||||
$module_required = FALSE;
|
$module_required = FALSE;
|
||||||
// Module description
|
// Module description
|
||||||
$module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management.";
|
$module_description = "Billing storefront / provisioning integration. Public ordering runs as a standalone site; panel pages provide provisioning and admin order management.";
|
||||||
|
|
@ -346,4 +346,26 @@ $install_queries[3] = array(
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// db_version 4 — Add billing_paypal_errors table for checkout error logging.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
$install_queries[4] = array(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `OGP_DB_PREFIXbilling_paypal_errors` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`context` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
`error_code` VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
`message` TEXT NULL,
|
||||||
|
`paypal_debug_id` VARCHAR(128) NULL,
|
||||||
|
`order_id` VARCHAR(128) NULL,
|
||||||
|
`capture_id` VARCHAR(128) NULL,
|
||||||
|
`billing_order_id` INT NULL,
|
||||||
|
`user_id` INT NULL,
|
||||||
|
`raw_json` LONGTEXT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_context` (`context`),
|
||||||
|
KEY `idx_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
|
||||||
|
);
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue