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(); //+++++++++++++++++++++++++++++++++++++++++++++++++++++ //Send email via PHPMailer //+++++++++++++++++++++++++++++++++++++++++++++++++++++ $mail_result = send_mail($customer_email, $subject, $message, $attachment, $subject); // Send to bookkeeping if configured if(invoice_bookkeeping){ debuglog("PAYPAL WEBHOOK: Sending to bookkeeping: " . email_bookkeeping); send_mail(email_bookkeeping, $subject, $message, $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'] ?? ''; } ?>