- Updated PDF template to display a fixed software code instead of "SOFTWARE". - Changed VAT label to include tax label dynamically and set to 0% for certain conditions. - Enhanced JavaScript for VAT number validation with asynchronous checks against the VIES database. - Implemented debounce for VAT number input to optimize validation calls. - Updated country settings to include country codes for VAT validation. - Modified email sending functions in webhook handlers to use dynamic attachment names for invoices.
431 lines
19 KiB
PHP
431 lines
19 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, accounthierarchy,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,
|
|
'{"salesid":"21-Total Safety Solutions B.V.","soldto":""}',
|
|
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,accounthierarchy)
|
|
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'),
|
|
'{"salesid":"21-Total Safety Solutions B.V.","soldto":""}'
|
|
]);
|
|
$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($message,$pdf,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
//CREATE PDF using DomPDF
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
$dompdf->loadHtml($pdf);
|
|
$dompdf->setPaper('A4', 'portrait');
|
|
$dompdf->render();
|
|
$subject = 'Software Upgrade - Invoice: '.$order_id;
|
|
$attachment = $dompdf->output();
|
|
$attachment_name = $subject.'.pdf';
|
|
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
//Send email via PHPMailer
|
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
$mail_result = send_mail($customer_email, $subject, $message, $attachment, $attachment_name);
|
|
|
|
// Send to bookkeeping if configured
|
|
if(invoice_bookkeeping){
|
|
debuglog("PAYPAL WEBHOOK: Sending to bookkeeping: " . email_bookkeeping);
|
|
send_mail(email_bookkeeping, $subject, $message, $attachment, $attachment_name);
|
|
}
|
|
} 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'] ?? '';
|
|
}
|
|
|
|
?>
|