# easy-invoice/2.2.0/includes/Gateways/PayPalGateway.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.2.0. 561 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.2.0/code/includes/Gateways/PayPalGateway.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.2.0/raw/includes/Gateways/PayPalGateway.php
- Modified: 2025-11-11T04:55:44+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.2.0/code/includes/Gateways/PayPalGateway.php#L10-L20`.

```php
<?php
namespace EasyInvoice\Gateways;

use EasyInvoice\Abstracts\AbstractPaymentGateway;
use EasyInvoice\Models\Payment;
use EasyInvoice\Traits\PaymentCalculationTrait;

class PayPalGateway extends AbstractPaymentGateway {
    use PaymentCalculationTrait;
    /**
     * Constructor
     */
    public function __construct() {
        parent::__construct();
        
        // Hook to render instructions when needed
        add_action('easy_invoice_payment_gateways_after', [$this, 'maybeRenderInstructions'], 10, 2);
    }

    /**
     * Get gateway name
     * 
     * @return string
     */
    public function getName(): string {
        return 'paypal';
    }

    /**
     * Get gateway title
     * 
     * @return string
     */
    public function getTitle(): string {
        return 'PayPal';
    }

    /**
     * Get gateway icon
     * 
     * @return string
     */
    public function getIcon(): string {
        return EASY_INVOICE_ASSETS_URL . 'images/paypal-icon.svg';
    }

    /**
     * Get gateway settings configuration for the settings page
     * 
     * @return array The settings configuration array
     */
    public function getSettingsConfig(): array {
        return [
            'show_header' => true,
            'header_text' => __('PayPal', 'easy-invoice'),
            'header_class' => 'mt-6 pt-6 border-t border-gray-200',
            'fields' => [
                'easy_invoice_paypal_email' => [
                    'label' => __('PayPal Email', 'easy-invoice'), 
                    'type' => 'email', 
                    'default' => '', 
                    'col_span' => 'sm:col-span-6 md:col-span-3', 
                    'description' => __('Enter your PayPal email address to receive payments.', 'easy-invoice')
                ],
                'easy_invoice_paypal_mode' => [
                    'label' => __('PayPal Mode', 'easy-invoice'), 
                    'type' => 'select', 
                    'default' => 'live', 
                    'options' => ['live' => 'Live', 'sandbox' => 'Sandbox'], 
                    'col_span' => 'sm:col-span-6 md:col-span-3', 
                    'description' => __('Select Live for real transactions or Sandbox for testing.', 'easy-invoice')
                ],
            ],
        ];
    }

    /**
     * Get gateway description
     * 
     * @return string
     */
    public function getDescription(): string {
        return 'Pay securely using your PayPal account';
    }

    /**
     * Maybe render instructions for PayPal
     * 
     * @param mixed $invoice
     * @param string $selected_gateway
     */
    public function maybeRenderInstructions($invoice, $selected_gateway = '') {
        // Only render if this is the selected gateway
        if ($selected_gateway !== 'paypal') {
            return;
        }

        // For PayPal, we can show additional information or payment buttons
        // For now, we'll return empty to keep it simple
        // In the future, this could show PayPal Smart Buttons or other PayPal-specific content
        return;
    }

    /**
     * Check if payment gateway is available for the current invoice
     * 
     * @return bool
     */
    public function isAvailable(): bool {
        // Check if PayPal is generally enabled in plugin settings
        if (!parent::isEnabled()) { // Call the corrected parent::isEnabled()
            return false;
        }

        // Check if PayPal email is configured (settings are loaded in AbstractPaymentGateway::init())
        if (empty($this->settings['email'])) {
            return false;
        }

        return true;
    }

    /**
     * Process payment
     * 
     * @param float $amount
     * @param array $data
     * @return array
     */
    public function processPayment(float $amount, array $data = []): array {
        $this->log('Processing PayPal payment for amount: ' . $amount . ' in mode: ' . ($this->settings['mode'] ?? 'live'));
        $this->log('PayPal payment data: ' . json_encode($data));
        
        $paypal_email = $this->settings['email'] ?? '';
        if (empty($paypal_email)) {
            return [
                'success' => false,
                'message' => 'PayPal email not configured'
            ];
        }
        
        // Trim whitespace from email
        $paypal_email = trim($paypal_email);
        
        // Validate PayPal email format
        if (!filter_var($paypal_email, FILTER_VALIDATE_EMAIL)) {
            return [
                'success' => false,
                'message' => 'Invalid PayPal email format'
            ];
        }
        
    
        
        // For sandbox mode, log the email being used
        if (isset($this->settings['mode']) && $this->settings['mode'] === 'sandbox') {
            $this->log('Using PayPal email in sandbox mode: ' . $paypal_email);
        }

        $paypal_url_base = 'https://www.paypal.com/cgi-bin/webscr';
        if (isset($this->settings['mode']) && $this->settings['mode'] === 'sandbox') {
            $paypal_url_base = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
        }
        
        $this->log('PayPal mode: ' . ($this->settings['mode'] ?? 'live'));
        $this->log('PayPal URL base: ' . $paypal_url_base);
        $this->log('PayPal email: ' . $paypal_email);

        // Validate required data
        if (empty($data['invoice_id'])) {
            return [
                'success' => false,
                'message' => 'Invoice ID is required'
            ];
        }

        // Get invoice details for better PayPal integration
        $invoice_post = get_post($data['invoice_id']);
        if (!$invoice_post) {
            return [
                'success' => false,
                'message' => 'Invoice not found'
            ];
        }

        $invoice = new \EasyInvoice\Models\Invoice($invoice_post);
        
        // Get invoice number
        $invoice_number = $invoice->number ?? '';
        if (empty($invoice_number)) {
            $invoice_number = get_post_meta($data['invoice_id'], '_easy_invoice_number', true);
        }
        if (empty($invoice_number)) {
            $invoice_number = 'INV-' . $data['invoice_id'];
        }
        
        // Get currency from invoice or global settings
        $invoice_currency = $invoice->getCurrencyCode();
        if ($invoice_currency === 'global' || empty($invoice_currency)) {
            // Use global settings
            $currency = get_option('easy_invoice_currency_code', 'USD');
            $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency);
        } else {
            // Use invoice-specific currency
            $currency = $invoice_currency;
            $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency);
        }
        
        // Ensure currency is a valid PayPal currency
        $valid_currencies = ['USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY', 'NZD', 'CHF', 'HKD', 'SGD', 'SEK', 'DKK', 'PLN', 'NOK', 'HUF', 'CZK', 'ILS', 'MXN', 'BRL', 'MYR', 'PHP', 'TWD', 'THB', 'TRY'];
        if (!in_array($currency, $valid_currencies)) {
            $this->log('Invalid currency code: ' . $currency . ', defaulting to USD', 'warning');
            $currency = 'USD';
            $currency_symbol = '$';
        }
        
        // Validate amount
        if ($amount <= 0) {
            $this->log('Invalid amount: ' . $amount, 'error');
            return [
                'success' => false,
                'message' => 'Invalid payment amount'
            ];
        }
        
        // Ensure amount is numeric and positive
        $amount = floatval($amount);
        if ($amount <= 0) {
            $this->log('Amount is not positive after conversion: ' . $amount, 'error');
            return [
                'success' => false,
                'message' => 'Invalid payment amount'
            ];
        }
        
        // Check if amount is within PayPal's acceptable range (0.01 to 999999.99)
        if ($amount < 0.01 || $amount > 999999.99) {
            $this->log('Amount outside PayPal range: ' . $amount, 'error');
            return [
                'success' => false,
                'message' => 'Payment amount must be between $0.01 and $999,999.99'
            ];
        }
        
        // Ensure amount is properly formatted for PayPal
        $formatted_amount = sprintf('%.2f', $amount);
        $this->log('Original amount: ' . $amount);
        $this->log('Formatted amount: ' . $formatted_amount);
        
        $this->log('Invoice number: ' . $invoice_number);
        $this->log('Invoice total: ' . ($invoice->total ?? 0));
        $this->log('Original currency: ' . ($data['currency'] ?? $invoice->getCurrencyCode() ?? 'USD'));
        $this->log('Final currency: ' . $currency);
        $this->log('Amount parameter: ' . $amount);
        $this->log('Formatted amount: ' . sprintf('%.2f', $amount));
        $this->log('Amount type: ' . gettype($amount));
        $this->log('Amount > 0: ' . ($amount > 0 ? 'true' : 'false'));
        $this->log('Amount is numeric: ' . (is_numeric($amount) ? 'true' : 'false'));
        
        // Get customer information
        $customer_id = $data['customer_id'] ?? $invoice->client_id ?? 0;
        $customer_name = '';
        if ($customer_id) {
            $customer = get_post($customer_id);
            if ($customer) {
                $customer_name = $customer->post_title;
            }
        }

        // Create item description
        $item_description = !empty($data['description']) ? $data['description'] : sprintf(
            'Invoice #%s',
            $invoice_number
        );
        
        // Ensure item description is not empty and properly formatted
        $item_description = trim($item_description);
        if (empty($item_description)) {
            $item_description = 'Invoice Payment';
        }
        
        // Limit description length for PayPal
        if (strlen($item_description) > 127) {
            $item_description = substr($item_description, 0, 124) . '...';
        }
        
        // Ensure no trailing spaces
        $item_description = rtrim($item_description);
        
        $this->log('Item description: ' . $item_description);

        // Create return URLs
        $return_url = $data['return_url'] ?? home_url('/payment-success');
        $cancel_url = $data['cancel_url'] ?? home_url('/payment-cancelled');
        $notify_url = $data['notify_url'] ?? admin_url('admin-ajax.php?action=easy_invoice_payment_callback&gateway=paypal&invoice_id=' . $data['invoice_id']);

        // Create PayPal payment URL with enhanced parameters
        $paypal_params = [
            'cmd' => '_xclick',
            'business' => $paypal_email,
            'item_name' => $item_description,
            'amount' => $formatted_amount,
            'currency_code' => $currency,
            'return' => $return_url,
            'cancel_return' => $cancel_url
        ];
        
        // Debug: Check if amount is actually in the parameters
        $this->log('Amount in parameters: ' . ($paypal_params['amount'] ?? 'NOT SET'));
        $this->log('Amount parameter type: ' . gettype($paypal_params['amount']));
        $this->log('Amount parameter value: "' . $paypal_params['amount'] . '"');
        
        $this->log('PayPal parameters before URL creation: ' . json_encode($paypal_params));
        $this->log('PayPal business email: ' . $paypal_email);
        $this->log('PayPal item name: ' . $item_description);
        $this->log('PayPal item name length: ' . strlen($item_description));
        $this->log('PayPal amount: ' . sprintf('%.2f', $amount));
        $this->log('PayPal amount type: ' . gettype(sprintf('%.2f', $amount)));
        
        // Build URL manually to ensure proper encoding
        $query_string = http_build_query($paypal_params);
        $payment_url = $paypal_url_base . '?' . $query_string;
        
        // Test URL length
        $this->log('PayPal URL length: ' . strlen($payment_url));
        if (strlen($payment_url) > 2048) {
            $this->log('WARNING: PayPal URL is very long: ' . strlen($payment_url) . ' characters', 'warning');
        }

        $this->log('PayPal redirect URL created: ' . $payment_url);
        $this->log('PayPal payment parameters: ' . json_encode([
            'amount' => sprintf('%.2f', $amount),
            'currency' => $currency,
            'invoice_number' => $invoice_number,
            'item_description' => $item_description,
            'return_url' => $return_url,
            'cancel_url' => $cancel_url
        ]));

        // Create initial payment record
        try {
            $payment_data = [
                'invoice_id' => $data['invoice_id'],
                'amount' => $amount,
                'payment_method' => 'paypal',
                'payment_date' => current_time('mysql'),
                'notes' => 'Payment initiated via PayPal',
                'status' => 'pending',
                'payment_type' => 'gateway',
                'transaction_id' => 'PAYPAL-' . time() . '-' . $data['invoice_id'],
                'currency' => $currency,
                'currency_symbol' => $currency_symbol,
                'gateway_response' => json_encode([
                    'gateway' => 'paypal',
                    'mode' => $this->settings['mode'] ?? 'live',
                    'paypal_email' => $paypal_email,
                    'item_description' => $item_description,
                    'redirect_url' => $payment_url
            ])
            ];
            
            // Create payment record using WordPress post creation
            $payment_post_data = [
                'post_title' => sprintf('PayPal Payment for Invoice #%s', $invoice_number),
                'post_type' => 'easy_invoice_payment',
                'post_status' => 'publish',
                'post_author' => 1,
                'meta_input' => [
                    '_invoice_id' => $data['invoice_id'],
                    '_amount' => $amount,
                    '_payment_method' => 'paypal',
                    '_status' => 'pending',
                    '_transaction_id' => 'PAYPAL-' . time() . '-' . $data['invoice_id'],
                    '_payment_date' => current_time('mysql'),
                    '_notes' => 'Payment initiated via PayPal',
                    '_payment_type' => 'gateway',
                    '_currency' => $currency,
                    '_currency_symbol' => $currency_symbol,
                    '_gateway_response' => json_encode([
                        'gateway' => 'paypal',
                        'mode' => $this->settings['mode'] ?? 'live',
                        'paypal_email' => $paypal_email,
                        'item_description' => $item_description,
                        'redirect_url' => $payment_url
                    ])
                ]
            ];
            
            $payment_post_id = wp_insert_post($payment_post_data);
            if ($payment_post_id && !is_wp_error($payment_post_id)) {
                $this->log('Initial payment record created with ID: ' . $payment_post_id);
            } else {
                $this->log('Failed to create payment record: ' . (is_wp_error($payment_post_id) ? $payment_post_id->get_error_message() : 'Unknown error'), 'error');
            }
        } catch (\Exception $e) {
            $this->log('Error creating initial payment record: ' . $e->getMessage(), 'error');
        }

        $this->log('PayPal payment URL created successfully: ' . $payment_url);
        
        return [
            'success' => true,
            'redirect_url' => $payment_url,
            'message' => 'Redirecting to PayPal to complete your payment...'
        ];
    }

    /**
     * Handle payment callback
     * 
     * @param array $data
     * @return array
     */
    public function handleCallback(array $data): array {
        $this->log('Received PayPal callback/IPN');
        
        $raw_post_data = file_get_contents('php://input');
        $this->log('Raw IPN data: ' . $raw_post_data);

        // Use the raw POST data for verification, not the $data array which might be processed already
        if (empty($raw_post_data)) {
            $this->log('No raw POST data received for IPN.', 'error');
            return [
                'success' => false,
                'message' => 'Invalid callback data (no raw post)'
            ];
        }

        // Determine PayPal verification URL based on mode
        $paypal_verify_url = 'https://ipnpb.paypal.com/cgi-bin/webscr'; // Live IPN verification URL
        if (isset($this->settings['mode']) && $this->settings['mode'] === 'sandbox') {
            $paypal_verify_url = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr'; // Sandbox IPN verification URL
            $this->log('Using Sandbox PayPal IPN verification URL.');
        } else {
            $this->log('Using Live PayPal IPN verification URL.');
        }

        // Prepare data for verification by prepending cmd=_notify-validate
        $params = 'cmd=_notify-validate&' . $raw_post_data;

        $response = wp_remote_post($paypal_verify_url, [
            'method'    => 'POST',
            'timeout'   => 45,
            'sslverify' => true, // Should be true in production
            'body'      => $params,
            'headers'   => [
                'Connection' => 'Close'
            ]
        ]);

        if (is_wp_error($response)) {
            $this->log('IPN verification request failed: ' . $response->get_error_message(), 'error');
            return [
                'success' => false,
                'message' => 'IPN verification request failed: ' . $response->get_error_message()
            ];
        }

        $verification_result = wp_remote_retrieve_body($response);
        $this->log('IPN Verification response: ' . $verification_result);

        if (strcmp(trim($verification_result), 'VERIFIED') !== 0) {
            $this->log('IPN verification failed. Result: ' . $verification_result, 'error');
            return [
                'success' => false,
                'message' => 'IPN verification failed'
            ];
        }
        
        $this->log('IPN VERIFIED.');

        // IPN is verified, now parse the raw post data into an array
        // The $data array passed to this function might already be this, 
        // but it's safer to parse from raw_post_data after verification.
        parse_str($raw_post_data, $payload);
        if (empty($payload)) {
            $this->log('Failed to parse IPN payload after verification.', 'error');
             return [
                'success' => false,
                'message' => 'Failed to parse IPN payload'
            ];
        }

        // Check #1: Receiver Email (ensure payment went to your account)
        $configured_paypal_email = $this->settings['email'] ?? '';
        $ipn_receiver_email = $payload['receiver_email'] ?? $payload['business'] ?? '';
        if (empty($configured_paypal_email) || strtolower(trim($ipn_receiver_email)) !== strtolower(trim($configured_paypal_email))) {
            $this->log('IPN Receiver Email mismatch. Configured: ' . $configured_paypal_email . ' | Received: ' . $ipn_receiver_email, 'error');
            return [
                'success' => false,
                'message' => 'IPN Receiver Email mismatch'
            ];
        }
        $this->log('Receiver email verified: ' . $ipn_receiver_email);

        // Check #2: Payment status (already in place, but now more reliable after verification)
        if (($payload['payment_status'] ?? '') !== 'Completed') {
            $this->log('Payment status not Completed. Status: ' . ($payload['payment_status'] ?? 'N/A'), 'warning');
            return [
                'success' => false,
                'message' => 'Payment not completed. Status: ' . ($payload['payment_status'] ?? 'N/A')
            ];
        }
        $this->log('Payment status VERIFIED as Completed.');

        // (Optional but recommended: check txn_id for duplicates, mc_gross and mc_currency against invoice)

        // Get invoice ID from custom data
        $custom_data = json_decode($payload['custom'] ?? '{}', true);
        $invoice_id = $custom_data['invoice_id'] ?? 0;
        if (!$invoice_id) {
            return [
                'success' => false,
                'message' => 'Invalid invoice ID'
            ];
        }

        // Update invoice status
        $invoice_post = get_post($invoice_id); // Fetch WP_Post object first
        if (!$invoice_post) {
            return [
                'success' => false,
                'message' => 'Invoice not found'
            ];
        }
        $invoice = new \EasyInvoice\Models\Invoice($invoice_post); // Pass WP_Post to constructor

        // Store payment details as meta
        $paypal_payment_date = $payload['payment_date'] ?? current_time('mysql');
        $paypal_txn_id = $payload['txn_id'] ?? '';
        $payment_amount = floatval($payload['mc_gross'] ?? 0);

        $invoice->setMeta('_easy_invoice_payment_method', 'paypal'); 
        $invoice->setMeta('_easy_invoice_payment_date', $paypal_payment_date);
        $invoice->setMeta('_easy_invoice_transaction_id', $paypal_txn_id);
        
        // Update the overall payment status (as seen in PaymentController logic)
        $invoice->setMeta('_payment_status', 'completed'); 
        
        // Update invoice status to paid only if total payments are sufficient
        // This will trigger 'easy_invoice_payment_completed' hook which sends admin notification
        $this->updateInvoiceStatusIfPaid($invoice_id, $invoice, 'paypal');
        
        // Update the WordPress post status to publish (if not already, or if 'paid' isn't a WP status)
        $current_wp_status = $invoice->getPost()->post_status;
        if ($current_wp_status !== 'publish' && $current_wp_status !== 'paid') { // Assuming 'paid' could be a custom registered status
             wp_update_post(['ID' => $invoice_id, 'post_status' => 'publish']);
        }
        
        // Store payment details for the hook
        $invoice->setMeta('_payment_method', 'paypal');
        $invoice->setMeta('_transaction_id', $paypal_txn_id);

        return [
            'success' => true,
            'message' => 'Payment processed successfully'
        ];
    }
    

} 
```
