- Implemented PayPal webhook for handling payment notifications, including signature verification and transaction updates. - Created invoice generation and license management for software upgrades upon successful payment. - Added comprehensive logging for debugging purposes. - Introduced new CSS styles for the marketing file management system, including layout, toolbar, breadcrumb navigation, search filters, and file management UI components.
428 lines
18 KiB
PHP
428 lines
18 KiB
PHP
<?php
|
|
// PayPal Webhook for software upgrade payments
|
|
// Based on webhook_mollie.php structure
|
|
// Handles PayPal IPN/webhook notifications for payment status updates
|
|
|
|
require_once 'settings/config_redirector.php';
|
|
require_once 'assets/functions.php';
|
|
include dirname(__FILE__).'/settings/settings_redirector.php';
|
|
|
|
// DEBUG: Log webhook call
|
|
debuglog("PAYPAL WEBHOOK CALLED - POST data: " . print_r($_POST, true));
|
|
debuglog("PAYPAL WEBHOOK CALLED - RAW input: " . file_get_contents('php://input'));
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
//LOGIN TO API (same as webhook_mollie.php)
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
debuglog("WEBHOOK: Attempting API authorization...");
|
|
debuglog("WEBHOOK: Interface user: " . interface_user);
|
|
$data = json_encode(array("clientID" => interface_user, "clientsecret" => interface_pw), JSON_UNESCAPED_UNICODE);
|
|
$responses = ioAPIv2('/v2/authorization', $data,'');
|
|
debuglog("WEBHOOK: Authorization response: " . $responses);
|
|
if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';}
|
|
$clientsecret = $responses['token'];
|
|
debuglog("WEBHOOK: Token obtained: " . ($clientsecret ? 'YES' : 'NO'));
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// BASEURL is required for invoice template
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
$base_url = 'https://'.$_SERVER['SERVER_NAME'].'/';
|
|
define('base_url', $base_url);
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Initialize DomPDF for invoice generation
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
use Dompdf\Dompdf;
|
|
use Dompdf\Options;
|
|
$options = new Options();
|
|
$options->set('isRemoteEnabled', true);
|
|
$dompdf = new Dompdf($options);
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Language mapping for invoices
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
$available_languages = [
|
|
'NL' => 'NL',
|
|
'BE' => 'NL',
|
|
'US' => 'US',
|
|
'GB' => 'US',
|
|
'DE' => 'DE',
|
|
'FR' => 'FR',
|
|
'ES' => 'ES'
|
|
];
|
|
|
|
try {
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Get the webhook event data from PayPal
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
$raw_input = file_get_contents('php://input');
|
|
$webhook_event = json_decode($raw_input, true);
|
|
|
|
debuglog("PAYPAL WEBHOOK: Decoded event: " . print_r($webhook_event, true));
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Verify webhook signature (recommended for production)
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
if (!debug && PAYPAL_WEBHOOK_ID) {
|
|
$verified = verifyPayPalWebhookSignature($raw_input, $_SERVER);
|
|
if (!$verified) {
|
|
debuglog("PAYPAL WEBHOOK ERROR: Signature verification failed");
|
|
http_response_code(401);
|
|
exit;
|
|
}
|
|
debuglog("PAYPAL WEBHOOK: Signature verified successfully");
|
|
}
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Extract event type and resource
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
$event_type = $webhook_event['event_type'] ?? '';
|
|
$resource = $webhook_event['resource'] ?? [];
|
|
|
|
debuglog("PAYPAL WEBHOOK: Event type: {$event_type}");
|
|
|
|
// Get order ID from custom_id in the payment resource
|
|
$orderId = $resource['custom_id'] ?? $resource['purchase_units'][0]['custom_id'] ?? null;
|
|
|
|
if (!$orderId) {
|
|
debuglog("PAYPAL WEBHOOK ERROR: No custom_id (order_id) in webhook event");
|
|
http_response_code(400);
|
|
exit;
|
|
}
|
|
|
|
debuglog("PAYPAL WEBHOOK: Order ID: {$orderId}");
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Map PayPal event types to payment status
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
$payment_status = null;
|
|
|
|
switch ($event_type) {
|
|
case 'PAYMENT.CAPTURE.COMPLETED':
|
|
case 'CHECKOUT.ORDER.APPROVED':
|
|
$payment_status = 1; // Paid
|
|
debuglog("PAYPAL WEBHOOK: Payment completed/approved");
|
|
break;
|
|
|
|
case 'PAYMENT.CAPTURE.PENDING':
|
|
case 'CHECKOUT.ORDER.PROCESSED':
|
|
$payment_status = 101; // Pending
|
|
debuglog("PAYPAL WEBHOOK: Payment pending");
|
|
break;
|
|
|
|
case 'PAYMENT.CAPTURE.DECLINED':
|
|
case 'PAYMENT.CAPTURE.FAILED':
|
|
$payment_status = 102; // Failed
|
|
debuglog("PAYPAL WEBHOOK: Payment failed/declined");
|
|
break;
|
|
|
|
case 'PAYMENT.CAPTURE.REFUNDED':
|
|
$payment_status = 104; // Refunded
|
|
debuglog("PAYPAL WEBHOOK: Payment refunded");
|
|
break;
|
|
|
|
case 'CHECKOUT.ORDER.VOIDED':
|
|
$payment_status = 999; // Canceled
|
|
debuglog("PAYPAL WEBHOOK: Payment voided/canceled");
|
|
break;
|
|
|
|
default:
|
|
debuglog("PAYPAL WEBHOOK: Unhandled event type: {$event_type}");
|
|
http_response_code(200);
|
|
echo "OK - Event type not handled";
|
|
exit;
|
|
}
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Update transaction status directly in database
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
if ($payment_status !== null) {
|
|
debuglog("PAYPAL WEBHOOK: Order ID: $orderId, Payment Status: $payment_status");
|
|
|
|
$pdo = dbConnect($dbname);
|
|
|
|
// Update transaction status
|
|
$sql = 'UPDATE transactions SET payment_status = ? WHERE txn_id = ?';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([$payment_status, $orderId]);
|
|
|
|
debuglog("PAYPAL WEBHOOK: Transaction status updated in database");
|
|
|
|
// Fetch transaction data for license creation
|
|
$sql = 'SELECT * FROM transactions WHERE txn_id = ?';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([$orderId]);
|
|
$transaction = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
debuglog("PAYPAL WEBHOOK: Transaction data: " . print_r($transaction, true));
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Only create license and invoice if payment is PAID
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
if ($payment_status == 1 && $transaction !== null && !empty($transaction)) {
|
|
debuglog("PAYPAL WEBHOOK: Payment is PAID, processing license...");
|
|
debuglog("PAYPAL WEBHOOK: Transaction ID (auto-increment): " . $transaction['id']);
|
|
debuglog("PAYPAL WEBHOOK: Transaction txn_id: " . $transaction['txn_id']);
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// CREATE LICENSE for software upgrade
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
$pdo = dbConnect($dbname);
|
|
|
|
// Fetch transaction items to find software upgrade
|
|
$sql = 'SELECT * FROM transactions_items WHERE txn_id = ?';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([$transaction['id']]);
|
|
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
debuglog("PAYPAL WEBHOOK: Found " . count($items) . " transaction items");
|
|
debuglog("PAYPAL WEBHOOK: Items data: " . print_r($items, true));
|
|
|
|
foreach ($items as $item) {
|
|
debuglog("PAYPAL WEBHOOK: Processing item: " . print_r($item, true));
|
|
|
|
if (!empty($item['item_options'])) {
|
|
$options = json_decode($item['item_options'], true);
|
|
debuglog("PAYPAL WEBHOOK: Item options: " . print_r($options, true));
|
|
|
|
// Check if this is a software upgrade (has serial_number and equipment_id)
|
|
if (isset($options['serial_number']) && isset($options['equipment_id'])) {
|
|
debuglog("PAYPAL WEBHOOK: This is a software upgrade item");
|
|
|
|
// Check if license already exists for this transaction
|
|
$sql = 'SELECT rowID FROM products_software_licenses WHERE transaction_id = ?';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([$orderId]);
|
|
$existing_license = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$existing_license) {
|
|
debuglog("PAYPAL WEBHOOK: No existing license, creating new one...");
|
|
|
|
// Generate unique license key
|
|
$license_key = generateUniqueLicenseKey();
|
|
|
|
// Create license
|
|
$sql = 'INSERT INTO products_software_licenses
|
|
(version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, created, createdby)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([
|
|
$item['item_id'], // version_id
|
|
1, // license_type (1 = upgrade)
|
|
$license_key,
|
|
1, // status = active
|
|
date('Y-m-d H:i:s'),
|
|
'2099-12-31 23:59:59', // effectively permanent
|
|
$orderId,
|
|
date('Y-m-d H:i:s'),
|
|
'webhook_paypal' // created by PayPal webhook
|
|
]);
|
|
|
|
debuglog("PAYPAL WEBHOOK: License created: $license_key");
|
|
|
|
// Update equipment.sw_version_license
|
|
$sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([$license_key, $options['equipment_id']]);
|
|
|
|
debuglog("PAYPAL WEBHOOK: Equipment updated with license: {$options['equipment_id']}");
|
|
} else {
|
|
debuglog("PAYPAL WEBHOOK: License already exists for order: $orderId");
|
|
}
|
|
} else {
|
|
debuglog("PAYPAL WEBHOOK: Not a software upgrade item (no serial_number/equipment_id)");
|
|
}
|
|
} else {
|
|
debuglog("PAYPAL WEBHOOK: No item_options found");
|
|
}
|
|
}
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Generate INVOICE directly in database
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
|
|
// Check if invoice already exists for this transaction
|
|
$sql = 'SELECT id FROM invoice WHERE txn_id = ?';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([$transaction['txn_id']]);
|
|
$existing_invoice = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$existing_invoice) {
|
|
// Create invoice
|
|
$sql = 'INSERT INTO invoice (txn_id, payment_status, payment_amount, shipping_amount, discount_amount, tax_amount, created)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([
|
|
$transaction['txn_id'],
|
|
$transaction['payment_status'],
|
|
$transaction['payment_amount'],
|
|
$transaction['shipping_amount'] ?? 0.00,
|
|
$transaction['discount_amount'] ?? 0.00,
|
|
$transaction['tax_amount'] ?? 0.00,
|
|
date('Y-m-d H:i:s')
|
|
]);
|
|
$invoice_id = $pdo->lastInsertId();
|
|
} else {
|
|
$invoice_id = $existing_invoice['id'];
|
|
}
|
|
|
|
// Fetch full invoice data with customer details for email
|
|
$sql = 'SELECT tx.*, txi.item_id, txi.item_price, txi.item_quantity, txi.item_options,
|
|
p.productcode, p.productname, inv.id as invoice, inv.created as invoice_created,
|
|
i.language as user_language
|
|
FROM invoice inv
|
|
LEFT JOIN transactions tx ON tx.txn_id = inv.txn_id
|
|
LEFT JOIN transactions_items txi ON txi.txn_id = tx.id
|
|
LEFT JOIN products p ON p.rowID = txi.item_id
|
|
LEFT JOIN identity i ON i.userkey = tx.account_id
|
|
WHERE inv.id = ?';
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute([$invoice_id]);
|
|
$invoice_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
if (!empty($invoice_data)) {
|
|
// Transform the data (group items like the API does)
|
|
$invoice_cust = transformOrderData($invoice_data);
|
|
|
|
// Determine invoice language
|
|
if (!empty($invoice_cust['customer']['language'])) {
|
|
$invoice_language = strtoupper($invoice_cust['customer']['language']);
|
|
} elseif (!empty($invoice_cust['customer']['country']) && isset($available_languages[strtoupper($invoice_cust['customer']['country'])])) {
|
|
$invoice_language = $available_languages[strtoupper($invoice_cust['customer']['country'])];
|
|
} else {
|
|
$invoice_language = 'US'; // Default fallback
|
|
}
|
|
|
|
// Generate invoice HTML (using custom template for software upgrades)
|
|
list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
//CREATE PDF using DomPDF
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
$dompdf->loadHtml($data);
|
|
$dompdf->setPaper('A4', 'portrait');
|
|
$dompdf->render();
|
|
$subject = 'Software Upgrade - Invoice: '.$order_id;
|
|
$attachment = $dompdf->output();
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
//Send email via PHPMailer
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
$mail_result = send_mail($customer_email, $subject, $data, $attachment, $subject);
|
|
|
|
// Send to bookkeeping if configured
|
|
if(invoice_bookkeeping){
|
|
debuglog("PAYPAL WEBHOOK: Sending to bookkeeping: " . email_bookkeeping);
|
|
send_mail(email_bookkeeping, $subject, $data, $attachment, $subject);
|
|
}
|
|
} else {
|
|
debuglog("PAYPAL WEBHOOK: No invoice data found for invoice_id: $invoice_id");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return 200 OK to PayPal
|
|
http_response_code(200);
|
|
echo "OK";
|
|
|
|
} catch (Exception $e) {
|
|
debuglog("PAYPAL WEBHOOK ERROR: " . $e->getMessage());
|
|
debuglog("PAYPAL WEBHOOK ERROR TRACE: " . $e->getTraceAsString());
|
|
error_log("PayPal webhook error: " . htmlspecialchars($e->getMessage()));
|
|
http_response_code(500);
|
|
}
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Helper function to verify PayPal webhook signature
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
function verifyPayPalWebhookSignature($raw_body, $headers) {
|
|
if (!PAYPAL_WEBHOOK_ID || !PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
|
|
debuglog("PAYPAL WEBHOOK: Signature verification skipped - credentials not configured");
|
|
return true; // Skip verification if not configured
|
|
}
|
|
|
|
// Get required headers
|
|
$transmission_id = $headers['HTTP_PAYPAL_TRANSMISSION_ID'] ?? '';
|
|
$transmission_time = $headers['HTTP_PAYPAL_TRANSMISSION_TIME'] ?? '';
|
|
$cert_url = $headers['HTTP_PAYPAL_CERT_URL'] ?? '';
|
|
$auth_algo = $headers['HTTP_PAYPAL_AUTH_ALGO'] ?? '';
|
|
$transmission_sig = $headers['HTTP_PAYPAL_TRANSMISSION_SIG'] ?? '';
|
|
|
|
debuglog("PAYPAL WEBHOOK: Headers - transmission_id: $transmission_id, cert_url: $cert_url");
|
|
|
|
if (!$transmission_id || !$transmission_sig) {
|
|
debuglog("PAYPAL WEBHOOK: Missing signature headers");
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Get PayPal access token
|
|
$access_token = getPayPalAccessToken();
|
|
|
|
// Prepare verification request
|
|
$verify_url = PAYPAL_URL . '/v1/notifications/verify-webhook-signature';
|
|
$verify_data = [
|
|
'transmission_id' => $transmission_id,
|
|
'transmission_time' => $transmission_time,
|
|
'cert_url' => $cert_url,
|
|
'auth_algo' => $auth_algo,
|
|
'transmission_sig' => $transmission_sig,
|
|
'webhook_id' => PAYPAL_WEBHOOK_ID,
|
|
'webhook_event' => json_decode($raw_body, true)
|
|
];
|
|
|
|
debuglog("PAYPAL WEBHOOK: Verification request - webhook_id: " . PAYPAL_WEBHOOK_ID);
|
|
debuglog("PAYPAL WEBHOOK: Verification URL: " . $verify_url);
|
|
|
|
$ch = curl_init($verify_url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($verify_data));
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $access_token
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
debuglog("PAYPAL WEBHOOK: Verification response (HTTP $http_code): " . $response);
|
|
|
|
$result = json_decode($response, true);
|
|
$verified = ($http_code == 200 && isset($result['verification_status']) && $result['verification_status'] === 'SUCCESS');
|
|
|
|
debuglog("PAYPAL WEBHOOK: Signature verification result: " . ($verified ? 'SUCCESS' : 'FAILED'));
|
|
return $verified;
|
|
|
|
} catch (Exception $e) {
|
|
debuglog("PAYPAL WEBHOOK: Signature verification error: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// Helper function to get PayPal access token
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
function getPayPalAccessToken() {
|
|
$ch = curl_init(PAYPAL_URL . '/v1/oauth2/token');
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
|
|
curl_setopt($ch, CURLOPT_USERPWD, PAYPAL_CLIENT_ID . ':' . PAYPAL_CLIENT_SECRET);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
|
|
|
|
$response = curl_exec($ch);
|
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($http_code != 200) {
|
|
throw new Exception("Failed to get PayPal access token: HTTP $http_code");
|
|
}
|
|
|
|
$result = json_decode($response, true);
|
|
return $result['access_token'] ?? '';
|
|
}
|
|
|
|
?>
|