No changes
This commit is contained in:
parent
8680a02b13
commit
b6b398f5bf
17374 changed files with 2475441 additions and 0 deletions
50
ControlPanel/paypal/api/capture_order.php
Normal file
50
ControlPanel/paypal/api/capture_order.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
// === CONFIG (Sandbox) ===
|
||||
$sandbox = true; // flip to false for Live
|
||||
$client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
|
||||
$client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
$in = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||
$order_id = $in['order_id'] ?? null;
|
||||
if (!$order_id) { http_response_code(400); echo json_encode(['error'=>'missing order_id']); exit; }
|
||||
|
||||
$api = $sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
|
||||
// 1) OAuth2
|
||||
$ch = curl_init("$api/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,
|
||||
]);
|
||||
$tok = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($http !== 200) { http_response_code(500); echo json_encode(['error'=>'oauth_fail']); exit; }
|
||||
$access = json_decode($tok, true)['access_token'] ?? null;
|
||||
|
||||
// 2) Capture
|
||||
$ch = curl_init("$api/v2/checkout/orders/$order_id/capture");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $access
|
||||
],
|
||||
]);
|
||||
$res = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($http !== 201 && $http !== 200) { http_response_code($http); echo $res; exit; }
|
||||
|
||||
$payload = json_decode($res, true);
|
||||
$status = $payload['status'] ?? 'UNKNOWN';
|
||||
$txnId = $payload['purchase_units'][0]['payments']['captures'][0]['id'] ?? null;
|
||||
|
||||
echo json_encode(['status'=>$status, 'txn_id'=>$txnId]);
|
||||
|
||||
113
ControlPanel/paypal/api/create_order.php
Normal file
113
ControlPanel/paypal/api/create_order.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
// === CONFIG (Sandbox) ===
|
||||
$sandbox = true; // flip to false for Live
|
||||
$client_id = 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c';
|
||||
$client_secret = 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$in = json_decode(file_get_contents('php://input'), true) ?: [];
|
||||
|
||||
// Incoming fields from your cart/client
|
||||
$amount_in = $in['amount'] ?? '0.00'; // overall intended amount (string)
|
||||
$currency = $in['currency'] ?? 'USD';
|
||||
$invoice_id = $in['invoice_id'] ?? null; // overall invoice id (string)
|
||||
$custom_id = $in['custom_id'] ?? null; // your short reference (<=127 chars)
|
||||
$description = $in['description'] ?? 'Order';
|
||||
$return_url = $in['return_url'] ?? null;
|
||||
$cancel_url = $in['cancel_url'] ?? null;
|
||||
|
||||
// Optional payloads:
|
||||
$items = (isset($in['items']) && is_array($in['items'])) ? $in['items'] : null; // PayPal items
|
||||
$line_invoices= (isset($in['line_invoices']) && is_array($in['line_invoices'])) ? $in['line_invoices'] : null; // your raw detail
|
||||
|
||||
// --- Server-side reconciliation for items ---
|
||||
// If items are provided, ensure the order 'amount' equals the sum of item unit_amount * quantity.
|
||||
// (Simplest policy: set the order amount to the exact sum of items.)
|
||||
$amount_value = number_format((float)$amount_in, 2, '.', '');
|
||||
|
||||
// Compute sum of items if present
|
||||
if ($items) {
|
||||
$sum = 0.00;
|
||||
foreach ($items as $it) {
|
||||
$qty = isset($it['quantity']) ? (int)$it['quantity'] : 1;
|
||||
$val = isset($it['unit_amount']['value']) ? (float)$it['unit_amount']['value'] : 0.00;
|
||||
$sum += $qty * $val;
|
||||
}
|
||||
// Use the item sum as the authoritative amount for PayPal (avoids mismatch errors)
|
||||
$amount_value = number_format($sum, 2, '.', '');
|
||||
}
|
||||
|
||||
$api = $sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
|
||||
// 1) OAuth2
|
||||
$ch = curl_init("$api/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,
|
||||
]);
|
||||
$tok = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($http !== 200) { http_response_code(500); echo json_encode(['error'=>'oauth_fail']); exit; }
|
||||
$access = json_decode($tok, true)['access_token'] ?? null;
|
||||
if (!$access) { http_response_code(500); echo json_encode(['error'=>'oauth_no_token']); exit; }
|
||||
|
||||
// 2) Build purchase unit
|
||||
$purchaseUnit = [
|
||||
'amount' => [
|
||||
'currency_code' => $currency,
|
||||
'value' => $amount_value,
|
||||
],
|
||||
'description' => $description,
|
||||
// Critical for webhook reconciliation:
|
||||
'invoice_id' => $invoice_id,
|
||||
'custom_id' => $custom_id
|
||||
];
|
||||
|
||||
// If items provided, include them and add a breakdown with item_total to match the overall amount
|
||||
if ($items) {
|
||||
$purchaseUnit['items'] = $items;
|
||||
$purchaseUnit['amount']['breakdown'] = [
|
||||
'item_total' => [
|
||||
'currency_code' => $currency,
|
||||
'value' => $amount_value
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// (Optional) Persist your raw line_invoices server-side here if you wish.
|
||||
// For example, write to a DB keyed by $invoice_id so you can join later.
|
||||
|
||||
// 3) Create order (intent = CAPTURE)
|
||||
$body = [
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => [ $purchaseUnit ],
|
||||
// Guides PayPal where to send the buyer if the flow becomes a full-page redirect
|
||||
'application_context' => [
|
||||
'return_url' => $return_url,
|
||||
'cancel_url' => $cancel_url,
|
||||
'user_action' => 'PAY_NOW'
|
||||
]
|
||||
];
|
||||
|
||||
$ch = curl_init("$api/v2/checkout/orders");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($body),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $access
|
||||
],
|
||||
]);
|
||||
$res = curl_exec($ch);
|
||||
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($http !== 201) { http_response_code($http); echo $res; exit; }
|
||||
echo $res;
|
||||
|
||||
6
ControlPanel/paypal/config.php
Normal file
6
ControlPanel/paypal/config.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Webhook https://panel.iaregamer.com/paypal/webhook.php
|
||||
Webhook ID 6N620673281740730
|
||||
App Gameservers World
|
||||
Client ID AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c
|
||||
Secret Key EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0
|
||||
|
||||
11
ControlPanel/paypal/data/INV-20250825-170438-e37518.json
Normal file
11
ControlPanel/paypal/data/INV-20250825-170438-e37518.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"event_type": "PAYMENT.CAPTURE.COMPLETED",
|
||||
"status": "PAID",
|
||||
"amount": "19.99",
|
||||
"currency": "USD",
|
||||
"payer": null,
|
||||
"invoice": "INV-20250825-170438-e37518",
|
||||
"custom": "user_1234_order_5678",
|
||||
"resource_id": "2V315801FX904340P",
|
||||
"ts": "2025-08-25T17:05:27-04:00"
|
||||
}
|
||||
11
ControlPanel/paypal/data/INV-20250825-174311-0a7993.json
Normal file
11
ControlPanel/paypal/data/INV-20250825-174311-0a7993.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"event_type": "PAYMENT.CAPTURE.COMPLETED",
|
||||
"status": "PAID",
|
||||
"amount": "19.99",
|
||||
"currency": "USD",
|
||||
"payer": null,
|
||||
"invoice": "INV-20250825-174311-0a7993",
|
||||
"custom": "user_1234_order_5678",
|
||||
"resource_id": "9E566091YD182143A",
|
||||
"ts": "2025-08-25T17:43:56-04:00"
|
||||
}
|
||||
10
ControlPanel/paypal/data/NO-INVOICE.json
Normal file
10
ControlPanel/paypal/data/NO-INVOICE.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"event_type": "PAYMENT.SALE.COMPLETED",
|
||||
"status": "PAID",
|
||||
"amount": "0.48",
|
||||
"currency": "USD",
|
||||
"payer": null,
|
||||
"invoice": null,
|
||||
"custom": null,
|
||||
"ts": "2025-08-25T16:46:11-04:00"
|
||||
}
|
||||
102
ControlPanel/paypal/pay.php
Normal file
102
ControlPanel/paypal/pay.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
-<?php
|
||||
// ==== YOUR CART DATA (server authoritative) ====
|
||||
// TODO: set these from your cart/session/DB:
|
||||
$amount = number_format(19.99, 2, '.', '');
|
||||
$currency = 'USD';
|
||||
$invoiceId = 'INV-' . date('Ymd-His') . '-' . bin2hex(random_bytes(3));
|
||||
$customId = 'user_1234_order_5678';
|
||||
$description = 'Game server monthly plan';
|
||||
|
||||
// Site base (adjust if different)
|
||||
$siteBase = 'https://panel.iaregamer.com';
|
||||
$returnUrl = $siteBase . '/paypal/return.php?invoice=' . urlencode($invoiceId);
|
||||
$cancelUrl = $siteBase . '/paypal/return.php?invoice=' . urlencode($invoiceId) . '&cancel=1';
|
||||
|
||||
// Where your API endpoints live:
|
||||
$apiBase = '/paypal/api';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Checkout</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- PayPal JS SDK (Sandbox). Use LIVE client-id when you go live. -->
|
||||
<script src="https://www.paypal.com/sdk/js?client-id=AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c¤cy=USD&intent=capture"></script>
|
||||
<style>body{font-family:system-ui,Arial,sans-serif;max-width:700px;margin:40px auto;padding:0 16px}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Complete your purchase</h1>
|
||||
<p><strong>Amount:</strong> <?= htmlspecialchars($currency) ?> <?= htmlspecialchars($amount) ?></p>
|
||||
<p><strong>Invoice:</strong> <?= htmlspecialchars($invoiceId) ?></p>
|
||||
<div id="paypal-button-container"></div>
|
||||
<div id="status" style="margin-top:16px"></div>
|
||||
|
||||
<script>
|
||||
const statusEl = document.getElementById('status');
|
||||
const amount = "<?= $amount ?>";
|
||||
const currency = "<?= $currency ?>";
|
||||
const invoice_id = "<?= $invoiceId ?>";
|
||||
const custom_id = "<?= htmlspecialchars($customId, ENT_QUOTES) ?>";
|
||||
const description = "<?= htmlspecialchars($description, ENT_QUOTES) ?>";
|
||||
const return_url = "<?= $returnUrl ?>";
|
||||
const cancel_url = "<?= $cancelUrl ?>";
|
||||
|
||||
function setStatus(msg){ statusEl.textContent = msg; }
|
||||
|
||||
|
||||
paypal.Buttons({
|
||||
// Show a single, small PayPal button
|
||||
style: {
|
||||
layout: 'vertical', // or 'horizontal'
|
||||
color: 'gold', // gold | blue | silver | black | white
|
||||
shape: 'pill', // pill | rect
|
||||
label: 'paypal', // paypal | pay | checkout | buynow
|
||||
height: 35, // 25–55 (smaller button = lower height)
|
||||
tagline: false
|
||||
},
|
||||
fundingSource: paypal.FUNDING.PAYPAL, // only the PayPal button
|
||||
|
||||
createOrder: function() {
|
||||
// (unchanged) — your fetch to create_order.php
|
||||
return fetch("<?= $apiBase ?>/create_order.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type":"application/json"},
|
||||
body: JSON.stringify({
|
||||
amount, currency, invoice_id, custom_id, description,
|
||||
return_url, cancel_url,
|
||||
items, line_invoices
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (!d.id) throw new Error(d.error || 'No order id');
|
||||
return d.id;
|
||||
});
|
||||
},
|
||||
|
||||
onApprove: function(data) {
|
||||
// (unchanged) — capture then redirect
|
||||
return fetch("<?= $apiBase ?>/capture_order.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type":"application/json"},
|
||||
body: JSON.stringify({ order_id: data.orderID })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(c => {
|
||||
if (c.status === 'COMPLETED') {
|
||||
window.location.href = return_url;
|
||||
} else {
|
||||
document.getElementById('pp-status').textContent = 'Capture status: ' + c.status;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onCancel: function(){ window.location.href = cancel_url; },
|
||||
onError: function(err){ document.getElementById('pp-status').textContent = 'PayPal error: ' + err; }
|
||||
}).render('#paypal-button-container');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
119
ControlPanel/paypal/return.php
Normal file
119
ControlPanel/paypal/return.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
// Reads data/<invoice>.json written by webhook.php and shows a receipt with items
|
||||
|
||||
$dataDir = __DIR__ . '/data';
|
||||
$invoice = $_GET['invoice'] ?? '';
|
||||
$cancel = isset($_GET['cancel']);
|
||||
|
||||
$status = 'PENDING';
|
||||
$details = null;
|
||||
$items = [];
|
||||
|
||||
if ($invoice && is_file("$dataDir/$invoice.json")) {
|
||||
$details = json_decode(file_get_contents("$dataDir/$invoice.json"), true);
|
||||
if (!empty($details['status'])) {
|
||||
$status = $details['status'];
|
||||
}
|
||||
if (!empty($details['items']) && is_array($details['items'])) {
|
||||
$items = $details['items'];
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function money_fmt($value, $currency) {
|
||||
if ($value === null || $value === '') return '';
|
||||
return h($currency) . ' ' . h(number_format((float)$value, 2, '.', ''));
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Payment Status</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body{font-family:system-ui,Arial,sans-serif;max-width:900px;margin:40px auto;padding:0 16px;line-height:1.45}
|
||||
.muted{color:#555}
|
||||
table{width:100%;border-collapse:collapse;margin-top:12px}
|
||||
th,td{border:1px solid #ddd;padding:8px;text-align:left}
|
||||
th{background:#f6f6f6}
|
||||
code{background:#f4f4f4;padding:2px 4px;border-radius:4px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php if ($cancel): ?>
|
||||
<h1>Payment canceled</h1>
|
||||
<p>Invoice: <?= h($invoice) ?></p>
|
||||
<p class="muted">You can return to your cart and try again.</p>
|
||||
<?php else: ?>
|
||||
<h1>Thank you!</h1>
|
||||
|
||||
<p><strong>Invoice:</strong> <?= h($invoice) ?></p>
|
||||
<p><strong>Status:</strong> <span><?= h($status) ?></span></p>
|
||||
|
||||
<?php if ($details): ?>
|
||||
<h3>Summary</h3>
|
||||
<ul>
|
||||
<li>Amount: <?= money_fmt($details['amount'] ?? null, $details['currency'] ?? '') ?></li>
|
||||
<li>Payer: <?= h($details['payer'] ?? '') ?></li>
|
||||
<li>Transaction ID: <code><?= h($details['resource_id'] ?? '') ?></code></li>
|
||||
<li>Event: <?= h($details['event_type'] ?? '') ?></li>
|
||||
<li>Timestamp: <?= h($details['ts'] ?? '') ?></li>
|
||||
<?php if (!empty($details['custom'])): ?>
|
||||
<li>Custom: <code><?= h($details['custom']) ?></code></li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
|
||||
<h3>Items</h3>
|
||||
<?php if ($items): ?>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Server ID</th>
|
||||
<th>Item</th>
|
||||
<th>Qty</th>
|
||||
<th>Unit Price</th>
|
||||
<th>Line Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$currency = $details['currency'] ?? ($items[0]['unit_amount']['currency_code'] ?? '');
|
||||
$grand = 0.00;
|
||||
foreach ($items as $it) {
|
||||
$name = $it['name'] ?? '';
|
||||
$sku = $it['sku'] ?? ''; // we sent serverID here
|
||||
$qty = isset($it['quantity']) ? (int)$it['quantity'] : 1;
|
||||
$unit = isset($it['unit_amount']['value']) ? (float)$it['unit_amount']['value'] : 0.00;
|
||||
$line = $qty * $unit;
|
||||
$grand += $line;
|
||||
echo '<tr>';
|
||||
echo '<td>'.h($sku).'</td>';
|
||||
echo '<td>'.h($name).'</td>';
|
||||
echo '<td>'.h($qty).'</td>';
|
||||
echo '<td>'.money_fmt($unit, $currency).'</td>';
|
||||
echo '<td>'.money_fmt($line, $currency).'</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td colspan="4" style="text-align:right;"><strong>Total</strong></td>
|
||||
<td><strong><?= money_fmt($grand, $currency) ?></strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<p class="muted">No line items were included in this webhook. If you just paid, refresh in a few seconds.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (strtoupper($status) !== 'PAID'): ?>
|
||||
<p class="muted">Waiting for confirmation from PayPal… this can take a few seconds. Refresh to update.</p>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<p class="muted">We’re waiting for PayPal to confirm your payment. This page will show the receipt once we receive the webhook. Try refreshing in a few seconds.</p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
154
ControlPanel/paypal/webhook.php
Normal file
154
ControlPanel/paypal/webhook.php
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
// ========== CONFIG ==========
|
||||
$config = [
|
||||
'sandbox' => true, // flip to false for Live
|
||||
'client_id' => 'AfvY_C2zA_hTHxHq7TIhtOeub4xBdySYrt_Hjj3d_WYQwjWI9NfOAVOTeResx2rgZ_nP5tOoxQSAHw8c',
|
||||
'client_secret' => 'EJ216np9cAj9n7KSddez3fLVxGe-zi4oKKKl1YGqPp88XIikr4Qzbxh0XW2as-V6LgdX-upjtQAg9dC0',
|
||||
'webhook_id' => '6N620673281740730',
|
||||
'data_dir' => __DIR__ . '/data',
|
||||
'log_file' => __DIR__ . '/webhook.log',
|
||||
];
|
||||
|
||||
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';}
|
||||
|
||||
http_response_code(200);
|
||||
@mkdir($config['data_dir'], 0775, true);
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
$headers = array_change_key_case(getallheaders() ?: [], CASE_UPPER);
|
||||
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);
|
||||
if ($http!==200){ log_line("OAUTH_FAIL http=$http resp=$tokenResp"); exit; }
|
||||
$access_token = json_decode($tokenResp, true)['access_token'] ?? null;
|
||||
if (!$access_token){ 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'){
|
||||
log_line("VERIFY_FAIL http=$http status=".($verifyJson['verification_status']??'NONE'));
|
||||
exit;
|
||||
}
|
||||
log_line("VERIFY_OK");
|
||||
|
||||
// 3) Parse and persist (now with items)
|
||||
$evt = json_decode($raw, true);
|
||||
$type = $evt['event_type'] ?? '';
|
||||
$res = $evt['resource'] ?? [];
|
||||
|
||||
// Extract common fields
|
||||
$invoice = $res['invoice_id'] ?? ($res['invoice_number'] ?? null);
|
||||
$custom = $res['custom_id'] ?? ($res['custom'] ?? null);
|
||||
|
||||
// Amounts/payer
|
||||
$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);
|
||||
|
||||
// Try to capture line items if present directly in this event:
|
||||
// (Some events—like ORDER.*—include purchase_units; CAPTURE events often don't.)
|
||||
$items = [];
|
||||
if (isset($res['purchase_units'][0]['items']) && is_array($res['purchase_units'][0]['items'])) {
|
||||
$items = $res['purchase_units'][0]['items'];
|
||||
}
|
||||
|
||||
// If capture event, try to fetch the parent ORDER to get items
|
||||
if (!$items && $type === 'PAYMENT.CAPTURE.COMPLETED') {
|
||||
$orderId =
|
||||
$res['supplementary_data']['related_ids']['order_id'] // preferred
|
||||
?? null;
|
||||
|
||||
if (!$orderId && isset($res['links']) && is_array($res['links'])) {
|
||||
// Fallback: look for a link to the parent order
|
||||
foreach ($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 the order has invoice/custom (sometimes more reliable), prefer those:
|
||||
if (!$invoice) { $invoice = $order['purchase_units'][0]['invoice_id'] ?? $invoice; }
|
||||
if (!$custom) { $custom = $order['purchase_units'][0]['custom_id'] ?? $custom; }
|
||||
} else {
|
||||
log_line("ORDER_FETCH_FAIL id=$orderId http=$httpOrder");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$status = 'IGNORED';
|
||||
|
||||
// We persist on payment completed events
|
||||
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, // <— Persist line items for your return.php/UI
|
||||
'ts' => date('c'),
|
||||
];
|
||||
$name = $invoice ?: 'NO-INVOICE';
|
||||
@file_put_contents($config['data_dir']."/$name.json", json_encode($record, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
||||
$status = 'WROTE_FILE';
|
||||
}
|
||||
|
||||
log_line("EVENT $type invoice=".($invoice ?: 'none')." items_count=".count($items)." status=$status");
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue