whoami7 - Manager
:
/
home
/
analuakl
/
somethingsmushy.com
/
wp-content
/
plugins
/
wp-user-frontend
/
Lib
/
Gateway
/
Upload File:
files >> /home/analuakl/somethingsmushy.com/wp-content/plugins/wp-user-frontend/Lib/Gateway/Paypal.php
<?php namespace WeDevs\Wpuf\Lib\Gateway; use WeDevs\Wpuf\Frontend\Payment; /** * WP User Frontend PayPal gateway * * @since 0.8 * @updated 4.1.5 */ class Paypal { private $gateway_url; private $test_mode; private $webhook_id; private $api_version = '2.0'; private $settings_url; private $setup_guide_url; private $paypal_icon_url; public function __construct() { $this->gateway_url = 'https://www.paypal.com/webscr/?'; $this->test_mode = 'test' === wpuf_get_option( 'paypal_test_mode', 'wpuf_payment' ); $this->webhook_id = wpuf_get_option( 'paypal_webhook_id', 'wpuf_payment' ); // Initialize URL properties $this->settings_url = admin_url( 'admin.php?page=wpuf-settings&tab=wpuf_payment' ); $this->setup_guide_url = 'https://wedevs.com/docs/wp-user-frontend-pro/settings/paypal-payement-gateway/'; $this->paypal_icon_url = WPUF_ASSET_URI . '/images/wpuf_paypal.png'; // Initialize hooks add_action( 'init', [ $this, 'check_paypal_return' ], 20 ); add_action( 'init', [ $this, 'handle_pending_payment' ] ); add_action( 'init', [ $this, 'register_webhook_endpoint' ] ); add_action( 'wpuf_gateway_paypal', [ $this, 'prepare_to_send' ] ); add_filter( 'wpuf_options_payment', [ $this, 'payment_options' ] ); add_action( 'wpuf_cancel_payment_paypal', [ $this, 'cancel_subscription' ] ); add_action( 'wpuf_cancel_subscription_paypal', [ $this, 'cancel_subscription' ] ); add_action( 'wpuf_paypal_webhook', [ $this, 'process_webhook' ] ); add_action( 'template_redirect', [ $this, 'handle_webhook_request' ] ); // Add admin notice for PayPal settings update add_action( 'admin_notices', [ $this, 'paypal_settings_update_notice' ] ); add_action( 'wp_ajax_wpuf_dismiss_paypal_notice', [ $this, 'dismiss_paypal_notice' ] ); // Add CSS for webhook configuration section add_action( 'admin_head', [ $this, 'inject_webhook_css' ] ); } /** * Inject CSS for webhook configuration */ public function inject_webhook_css() { // Only inject on WPUF settings page $screen = get_current_screen(); if ( ! $screen || false === strpos( $screen->base, 'wpuf' ) ) { return; } ?> <style> .wpuf-paypal-webhook-container { padding: 10px 15px 15px 15px !important; border: 1px solid #ddd !important; background-color: #f9f9f9 !important; border-radius: 4px !important; box-sizing: border-box !important; } </style> <?php } /** * Display admin notice for PayPal settings update */ public function paypal_settings_update_notice() { // Only show to administrators if ( ! current_user_can( 'manage_options' ) ) { return; } // Check if notice has been dismissed if ( get_option( 'wpuf_paypal_settings_notice_dismissed' ) ) { return; } // Only show in admin area if ( ! is_admin() ) { return; } ?> <div class="notice notice-info is-dismissible wpuf-paypal-notice" id="wpuf-paypal-settings-notice"> <div class="wpuf-notice-content" style="display: flex; align-items: center; padding: 10px 0;"> <div class="wpuf-notice-icon" style="margin-right: 15px;"> <img src="<?php echo esc_url( $this->paypal_icon_url ); ?>" alt="PayPal" style="width: 45px; height: 45px;"> </div> <div class="wpuf-notice-message" style="flex: 1;"> <h3 style="margin: 0 0 5px 0;"><?php esc_html_e( 'WPUF PayPal Integration Enhanced', 'wp-user-frontend' ); ?></h3> <p style="margin: 0;"> <?php echo wp_kses_post( sprintf( /* translators: %s: PayPal setup guide URL */ __( "We've enhanced <strong>WPUF's PayPal integration</strong> to be more secure and reliable with webhook support. If you're using PayPal, please follow our %s to update your settings and ensure uninterrupted payments.", 'wp-user-frontend' ), '<a href="' . esc_url( $this->setup_guide_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'PayPal setup guide', 'wp-user-frontend' ) . '</a>' ) ); ?> </p> </div> <div class="wpuf-notice-action" style="margin-left: 15px;"> <a href="<?php echo esc_url( $this->settings_url ); ?>" class="button button-primary"> <?php esc_html_e( 'Update PayPal Settings', 'wp-user-frontend' ); ?> </a> </div> </div> </div> <script type="text/javascript"> jQuery(document).ready(function($) { $(document).on('click', '.wpuf-paypal-notice .notice-dismiss', function() { $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'wpuf_dismiss_paypal_notice', nonce: '<?php echo esc_js( wp_create_nonce( 'wpuf_dismiss_paypal_notice' ) ); ?>' }, success: function(response) { // Handle success if needed }, error: function(xhr, status, error) { console.error('Failed to dismiss notice:', error); } }); }); }); </script> <style> .wpuf-paypal-notice { border-left-color: #0073aa; } .wpuf-paypal-notice .wpuf-notice-content { padding: 10px 0; } .wpuf-paypal-notice h3 { color: #23282d; font-size: 14px; font-weight: 600; } .wpuf-paypal-notice p { color: #666; font-size: 13px; line-height: 1.5; } .wpuf-paypal-notice a { color: #0073aa; text-decoration: none; } .wpuf-paypal-notice a:hover { text-decoration: underline; } .wpuf-paypal-notice strong { font-weight: 600; color: #23282d; } </style> <?php } /** * Handle dismissal of PayPal settings notice */ public function dismiss_paypal_notice() { // Verify nonce if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'wpuf_dismiss_paypal_notice' ) ) { wp_die( 'Security check failed' ); } // Check user capabilities if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Insufficient permissions' ); } // Mark notice as dismissed update_option( 'wpuf_paypal_settings_notice_dismissed', true ); wp_die(); } /** * Main webhook processor */ public function process_webhook( $raw_input ) { try { // Set headers for webhook acknowledgment header( 'HTTP/1.1 200 OK' ); header( 'Content-Type: application/json' ); // Verify webhook signature if ( ! $this->verify_webhook_signature_from_input( $raw_input ) ) { http_response_code( 401 ); echo wp_json_encode( [ 'status' => 'error', 'message' => 'Unauthorized', ] ); exit; } // Decode the webhook data $event = json_decode( $raw_input, true ); if ( JSON_ERROR_NONE !== json_last_error() ) { throw new \Exception( 'Invalid JSON in webhook data' ); } switch ( $event['event_type'] ) { case 'BILLING.SUBSCRIPTION.CANCELLED': if ( isset( $event['resource'] ) ) { $this->handle_subscription_cancelled( $event['resource'] ); } break; case 'BILLING.SUBSCRIPTION.CREATED': if ( isset( $event['resource'] ) ) { $this->handle_subscription_created( $event['resource'] ); } break; case 'BILLING.SUBSCRIPTION.ACTIVATED': if ( isset( $event['resource'] ) ) { $this->handle_subscription_activated( $event['resource'] ); } break; case 'PAYMENT.SALE.COMPLETED': if ( isset( $event['resource'] ) ) { $payment = $event['resource']; if ( isset( $payment['billing_agreement_id'] ) ) { $this->process_subscription_payment( $payment ); } } break; case 'PAYMENT.CAPTURE.COMPLETED': if ( isset( $event['resource'] ) ) { $this->process_payment_capture( $event['resource'] ); } break; } echo wp_json_encode( [ 'status' => 'success', 'message' => 'Webhook processed successfully', ] ); } catch ( \Exception $e ) { echo wp_json_encode( [ 'status' => 'error', 'message' => $e->getMessage(), ] ); } } /** * Process payment capture */ private function process_payment_capture( $payment ) { global $wpdb; try { // Get custom data from custom_id if ( ! isset( $payment['custom_id'] ) ) { throw new \Exception( 'Missing custom_id in payment' ); } $custom_data = json_decode( $payment['custom_id'], true ); if ( ! $custom_data ) { throw new \Exception( 'Invalid custom data' ); } // Verify payment amount // Payment amount verification is handled via breakdown calculation hook $payment_amount = number_format( $payment['amount']['value'], 2, '.', '' ); if ( $payment['amount']['value'] < 0 ) { throw new \Exception( 'Invalid payment amount: negative value' ); } // Check if transaction already exists $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}wpuf_transaction WHERE transaction_id = %s", $payment['id'] ) ); if ( $existing ) { return; // Exit if transaction already processed } // Get user $user = get_user_by( 'id', $custom_data['user_id'] ); if ( ! $user ) { throw new \Exception( 'Invalid user' ); } // Extract tax and subtotal from PayPal's breakdown // PayPal returns the breakdown we sent during order creation $total_amount = floatval( $payment['amount']['value'] ); $subtotal = $total_amount; // Default to total $tax = 0; if ( isset( $payment['amount']['breakdown'] ) ) { // Extract item_total (subtotal) from breakdown if ( isset( $payment['amount']['breakdown']['item_total']['value'] ) ) { $subtotal = floatval( $payment['amount']['breakdown']['item_total']['value'] ); } // Extract tax_total from breakdown if ( isset( $payment['amount']['breakdown']['tax_total']['value'] ) ) { $tax = floatval( $payment['amount']['breakdown']['tax_total']['value'] ); } // Validate breakdown: item_total + tax_total should equal total (within rounding) $breakdown_total = $subtotal + $tax; $difference = abs( $breakdown_total - $total_amount ); // If breakdown doesn't add up correctly, recalculate from total // This handles cases where PayPal returns incorrect breakdown values if ( $difference > 0.01 ) { // Breakdown is incorrect, try to fix it // If we have custom_data, use that instead if ( isset( $custom_data['subtotal'] ) && isset( $custom_data['tax'] ) ) { $subtotal = floatval( $custom_data['subtotal'] ); $tax = floatval( $custom_data['tax'] ); } else { // Recalculate: assume tax is correct, adjust subtotal // Or if subtotal seems wrong (equals total), calculate from total - tax if ( abs( $subtotal - $total_amount ) < 0.01 && $tax > 0 ) { // Subtotal equals total, which is wrong - recalculate $subtotal = $total_amount - $tax; } elseif ( $tax > 0 && $subtotal > 0 ) { // Both exist but don't add up - trust the total and recalculate $subtotal = $total_amount - $tax; } } } } elseif ( isset( $custom_data['subtotal'] ) && isset( $custom_data['tax'] ) ) { // Fallback: Use custom_id data if breakdown not available $subtotal = floatval( $custom_data['subtotal'] ); $tax = floatval( $custom_data['tax'] ); } else { // If no breakdown and no custom_data, try to recalculate from subscription pack // This ensures accurate tax calculation based on current settings if ( 'pack' === $custom_data['type'] && ! empty( $custom_data['item_number'] ) ) { /** * Filter: wpuf_recalculate_tax_from_pack * * Allows extensions (like WPUF Pro Tax) to recalculate tax from subscription pack * when PayPal breakdown data is missing or incorrect. * * @since WPUF_PRO_SINCE * * @param false|array $tax_data Array with 'subtotal', 'tax', and 'cost' keys, or false * @param int $pack_id Subscription pack ID * @param int $user_id User ID * @param float $total_amount Total amount from PayPal (for validation) * @param string $payment_type Payment type ('pack' or 'post') * * @return array|false Array with tax breakdown or false if unable to calculate */ $recalculated = apply_filters( 'wpuf_recalculate_tax_from_pack', false, $custom_data['item_number'], $custom_data['user_id'], $total_amount, $custom_data['type'] ); if ( $recalculated && is_array( $recalculated ) ) { $subtotal = isset( $recalculated['subtotal'] ) ? floatval( $recalculated['subtotal'] ) : $subtotal; $tax = isset( $recalculated['tax'] ) ? floatval( $recalculated['tax'] ) : $tax; } } } // Ensure cost is always subtotal + tax (for consistency) // Use total_amount as fallback if calculation seems off $calculated_cost = $subtotal + $tax; $cost = abs( $calculated_cost - $total_amount ) < 0.01 ? $calculated_cost : $total_amount; // Create payment record $data = [ 'user_id' => $custom_data['user_id'], 'status' => 'completed', 'subtotal' => $subtotal, 'tax' => $tax, 'cost' => $cost, 'post_id' => ( 'post' === $custom_data['type'] ) ? $custom_data['item_number'] : 0, 'pack_id' => ( 'pack' === $custom_data['type'] ) ? $custom_data['item_number'] : 0, 'payer_first_name' => $user->first_name, 'payer_last_name' => $user->last_name, 'payer_email' => $user->user_email, 'payment_type' => 'paypal', 'transaction_id' => $payment['id'], 'created' => gmdate( 'Y-m-d H:i:s' ), ]; // Insert payment record Payment::insert_payment( $data, $payment['id'], false ); // Handle coupon if present if ( ! empty( $custom_data['coupon_id'] ) ) { $this->update_coupon_usage( $custom_data['coupon_id'] ); } } catch ( \Exception $e ) { throw $e; } } /** * Get payer information */ private function get_payer_info( $payment, $user ) { $payer_info = [ 'first_name' => $user->first_name, 'last_name' => $user->last_name, 'email' => $user->user_email, ]; if ( isset( $payment['payer'] ) ) { if ( isset( $payment['payer']['name'] ) ) { $name_parts = explode( ' ', $payment['payer']['name'] ); $payer_info['first_name'] = $name_parts[0]; $payer_info['last_name'] = count( $name_parts ) > 1 ? implode( ' ', array_slice( $name_parts, 1 ) ) : ''; } if ( isset( $payment['payer']['email_address'] ) ) { $payer_info['email'] = $payment['payer']['email_address']; } } return $payer_info; } /** * Handle additional payment features */ private function handle_payment_features( $custom_data, $payment ) { // Handle coupon if ( ! empty( $custom_data['coupon_id'] ) ) { $this->update_coupon_usage( $custom_data['coupon_id'] ); } } /** * Register webhook endpoint */ public function register_webhook_endpoint() { // Add rewrite rule for webhook endpoint add_rewrite_rule( '^webhook_triggered/?$', 'index.php?action=webhook_triggered=1', 'top' ); // Add query var filter add_filter( 'query_vars', function ( $vars ) { $vars[] = 'action'; return $vars; } ); // Flush rewrite rules only once if ( ! get_option( 'wpuf_paypal_webhook_flushed' ) ) { flush_rewrite_rules(); update_option( 'wpuf_paypal_webhook_flushed', true ); } } /** * Handle webhook request */ public function handle_webhook_request() { if ( 'action' === get_query_var( 'action' ) && isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_SERVER['HTTP_PAYPAL_TRANSMISSION_ID'] ) ) { $raw_input = file_get_contents( 'php://input' ); $acknowledged = false; try { // Log and basic checks if ( empty( $raw_input ) ) { throw new \Exception( 'Empty webhook payload' ); } // Verify signature if ( ! $this->verify_webhook_signature_from_input( $raw_input ) ) { throw new \Exception( 'Invalid webhook signature' ); } // Decode and validate $webhook_data = json_decode( $raw_input, true ); if ( JSON_ERROR_NONE !== json_last_error() ) { throw new \Exception( 'Invalid JSON' ); } if ( ! isset( $webhook_data['event_type'] ) ) { throw new \Exception( 'Missing event type' ); } // Log the event type // Process the webhook $this->process_webhook( $raw_input ); $acknowledged = true; } catch ( \Exception $e ) { throw new \Exception( 'Webhook processing failed: ' . esc_html( $e->getMessage() ) ); } // Always acknowledge to PayPal http_response_code( 200 ); echo wp_json_encode( [ 'status' => $acknowledged ? 'ok' : 'error' ] ); exit; } } /** * Verify webhook signature from raw input */ private function verify_webhook_signature_from_input( $raw_input ) { try { // Get all headers, lowercased $headers = function_exists( 'getallheaders' ) ? array_change_key_case( getallheaders(), CASE_LOWER ) : array_change_key_case( $_SERVER, CASE_LOWER ); // Required PayPal webhook headers $required_headers = [ 'paypal-transmission-id', 'paypal-transmission-time', 'paypal-transmission-sig', 'paypal-cert-url', 'paypal-auth-algo', ]; foreach ( $required_headers as $header ) { if ( empty( $headers[ $header ] ) ) { return false; } } $access_token = $this->get_access_token(); if ( ! $access_token ) { return false; } $verification_url = $this->test_mode ? 'https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature' : 'https://api.paypal.com/v1/notifications/verify-webhook-signature'; $webhook_id = $this->webhook_id; if ( empty( $webhook_id ) ) { return false; } $verification_data = [ 'transmission_id' => $headers['paypal-transmission-id'], 'transmission_time' => $headers['paypal-transmission-time'], 'cert_url' => $headers['paypal-cert-url'], 'webhook_id' => $webhook_id, 'webhook_event' => json_decode( $raw_input, false ), 'transmission_sig' => $headers['paypal-transmission-sig'], 'auth_algo' => $headers['paypal-auth-algo'], ]; $response = wp_remote_post( $verification_url, [ 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $access_token, ], 'body' => wp_json_encode( $verification_data ), 'timeout' => 30, ] ); if ( is_wp_error( $response ) ) { return false; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); $is_verified = isset( $body['verification_status'] ) && $body['verification_status'] === 'SUCCESS'; return $is_verified; } catch ( \Exception $e ) { return false; } } /** * Get PayPal access token */ private function get_access_token() { $client_id = wpuf_get_option( 'paypal_client_id', 'wpuf_payment' ); $client_secret = wpuf_get_option( 'paypal_client_secret', 'wpuf_payment' ); $token_url = $this->test_mode ? 'https://api-m.sandbox.paypal.com/v1/oauth2/token' : 'https://api-m.paypal.com/v1/oauth2/token'; $response = wp_remote_post( $token_url, [ 'headers' => [ 'Authorization' => 'Basic ' . base64_encode( $client_id . ':' . $client_secret ), 'Content-Type' => 'application/x-www-form-urlencoded', ], 'body' => 'grant_type=client_credentials', ] ); if ( is_wp_error( $response ) ) { throw new \Exception( 'Failed to get access token: ' . esc_html( $response->get_error_message() ) ); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); return $body['access_token']; } /** * Handle subscription creation */ private function handle_subscription_created( $subscription ) { try { // Extract custom data $custom_data = []; if ( isset( $subscription['custom_id'] ) ) { $custom_data = json_decode( $subscription['custom_id'], true ); } if ( ! $custom_data || ! isset( $custom_data['user_id'] ) ) { throw new \Exception( 'Invalid custom data in subscription' ); } $user_id = $custom_data['user_id']; $subscription_id = $subscription['id']; // This is the PayPal subscription ID $trial_period_days = isset( $custom_data['trial_period_days'] ) ? $custom_data['trial_period_days'] : 0; $is_trial = $trial_period_days > 0; // Check if we're in a trial period $is_in_trial = false; if ( isset( $subscription['billing_info']['cycle_executions'] ) ) { foreach ( $subscription['billing_info']['cycle_executions'] as $cycle ) { if ( 'TRIAL' === $cycle['tenure_type'] && $cycle['cycles_completed'] < $cycle['cycles_remaining'] ) { $is_in_trial = true; break; } } } // Get the pack details $pack = get_post( $custom_data['item_number'] ); $pack_meta = array_map( function ( $value ) { return maybe_unserialize( $value[0] ); }, get_post_meta( $pack->ID ) ); // Get subscription period and interval from pack meta $period = isset( $pack_meta['_cycle_period'] ) ? $pack_meta['_cycle_period'] : 'month'; $interval = isset( $pack_meta['_billing_cycle_number'] ) ? intval( $pack_meta['_billing_cycle_number'] ) : 1; // Create subscription data structure with all necessary meta $subscription_data = [ 'pack_id' => $custom_data['item_number'], 'posts' => isset( $pack_meta['_post_types'] ) ? $pack_meta['_post_types'] : [], 'total_feature_item' => isset( $pack_meta['_total_feature_item'] ) ? $pack_meta['_total_feature_item'] : '-1', 'remove_feature_item' => isset( $pack_meta['_remove_feature_item'] ) ? $pack_meta['_remove_feature_item'] : '-1', 'status' => 'completed', 'expire' => isset( $pack_meta['expire'] ) ? $pack_meta['expire'] : '', 'profile_id' => $subscription_id, 'recurring' => 'yes', 'cycle_period' => $period, 'cycle_number' => $interval, 'postnum_rollback_on_delete' => isset( $pack_meta['postnum_rollback_on_delete'] ) ? $pack_meta['postnum_rollback_on_delete'] : '', '_enable_post_expiration' => isset( $pack_meta['_enable_post_expiration'] ) ? $pack_meta['_enable_post_expiration'] : 'no', '_post_expiration_time' => isset( $pack_meta['_post_expiration_time'] ) ? $pack_meta['_post_expiration_time'] : '', '_expired_post_status' => isset( $pack_meta['_expired_post_status'] ) ? $pack_meta['_expired_post_status'] : 'publish', '_enable_mail_after_expired' => isset( $pack_meta['_enable_mail_after_expired'] ) ? $pack_meta['_enable_mail_after_expired'] : 'no', '_post_expiration_message' => isset( $pack_meta['_post_expiration_message'] ) ? $pack_meta['_post_expiration_message'] : '', 'subscription_id' => $subscription_id, 'trial' => $is_trial ? 'yes' : 'no', 'created' => gmdate( 'Y-m-d H:i:s' ), ]; // If posts meta is empty, set default values if ( empty( $subscription_data['posts'] ) ) { $subscription_data['posts'] = [ 'post' => '-1', 'page' => '-1', 'user_request' => '-1', 'wp_block' => '-1', 'wp_template' => '-1', 'wp_template_part' => '-1', 'wp_global_styles' => '-1', 'wp_navigation' => '-1', 'wp_font_family' => '-1', 'wp_font_face' => '-1', ]; } // Update user meta with complete subscription data update_user_meta( $user_id, '_wpuf_subscription_pack', $subscription_data ); // Create a trial payment record if this is a trial if ( $is_in_trial ) { $this->create_trial_payment_record( $user_id, $custom_data['item_number'], $subscription_id ); } } catch ( \Exception $e ) { throw $e; } } /** * Create a trial payment record with zero cost */ private function create_trial_payment_record( $user_id, $pack_id, $subscription_id ) { $user = get_user_by( 'id', $user_id ); if ( ! $user ) { return; } // Create payment data with zero cost for trial $payment_data = [ 'user_id' => $user_id, 'status' => 'completed', 'tax' => 0, // the payment record structure in the database expects a tax field 'subtotal' => 0, 'cost' => 0, 'post_id' => 0, 'pack_id' => $pack_id, 'payer_first_name' => $user->first_name, 'payer_last_name' => $user->last_name, 'payer_email' => $user->user_email, 'payment_type' => 'PayPal', 'transaction_id' => $subscription_id . '_trial', 'profile_id' => $subscription_id, 'created' => gmdate( 'Y-m-d H:i:s' ), ]; Payment::insert_payment( $payment_data, $subscription_id . '_trial', true ); } /** * Handle subscription cancellation */ public function cancel_subscription( $data ) { try { global $wpdb; // Extract user_id from the input $user_id = null; // Get user ID from form data or current user if ( isset( $data['user_id'] ) ) { $user_id = $data['user_id']; } else { $user_id = get_current_user_id(); } if ( ! $user_id || ! is_numeric( $user_id ) ) { throw new \Exception( 'Invalid user ID provided for cancellation' ); } $subscription = get_user_meta( $user_id, '_wpuf_subscription_pack', true ); $subscription_id = get_user_meta( $user_id, '_wpuf_paypal_subscription_id', true ); // If we have a profile ID and subscription is recurring if ( ! empty( $subscription_id ) && $subscription['recurring'] === 'yes' ) { try { // Get access token $access_token = $this->get_access_token(); $plan_id_url = ( $this->test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com' ) . '/v1/billing/subscriptions/' . $subscription_id; $plan_id_response = wp_remote_get( $plan_id_url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, ], ] ); if ( is_wp_error( $plan_id_response ) ) { throw new \Exception( 'Failed to get plan ID from PayPal: ' . $plan_id_response->get_error_message() ); } $plan_response_body = json_decode( wp_remote_retrieve_body( $plan_id_response ), true ); if ( ! $plan_response_body || ! isset( $plan_response_body['plan_id'] ) ) { throw new \Exception( 'Invalid plan response from PayPal' ); } $plan_id = $plan_response_body['plan_id']; if ( empty( $plan_id ) ) { throw new \Exception( 'Plan ID not found for subscription: ' . $subscription_id ); } // Cancel the subscription in PayPal $cancel_url = ( $this->test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com' ) . '/v1/billing/plans/' . $plan_id . '/deactivate'; $response = wp_remote_post( $cancel_url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', 'Prefer' => 'return=representation', ], 'body' => wp_json_encode( [ 'reason' => 'Customer requested cancellation', ] ), ] ); if ( is_wp_error( $response ) ) { throw new \Exception( 'Failed to cancel subscription in PayPal: ' . $response->get_error_message() ); } $response_code = wp_remote_retrieve_response_code( $response ); $response_body = wp_remote_retrieve_body( $response ); if ( $response_code !== 204 ) { throw new \Exception( 'Unexpected response from PayPal: ' . $response_body ); } } catch ( \Exception $e ) { throw new \Exception( 'PayPal cancellation failed: ' . $e->getMessage() ); } } // Update local subscription status regardless of PayPal API result $updated_subscription = [ 'profile_id' => $subscription_id, 'status' => 'cancel', 'updated' => gmdate( 'Y-m-d H:i:s' ), ]; update_user_meta( $user_id, '_wpuf_subscription_pack', $updated_subscription ); update_user_meta( $user_id, '_wpuf_paypal_subscription_status', 'cancel' ); // Update subscriber table $update_result = $wpdb->update( $wpdb->prefix . 'wpuf_subscribers', [ 'subscribtion_status' => 'cancel', 'expire' => gmdate( 'd-m-Y' ), ], [ 'user_id' => $user_id, 'gateway' => 'paypal', ], [ '%s', '%s' ], [ '%d', '%s' ] ); return true; } catch ( \Exception $e ) { throw $e; } } /** * Handle subscription payment */ private function process_subscription_payment( $payment ) { global $wpdb; try { // Get transaction ID - could be in different locations based on event type $transaction_id = isset( $payment['id'] ) ? $payment['id'] : ''; // Get subscription ID $subscription_id = isset( $payment['billing_agreement_id'] ) ? $payment['billing_agreement_id'] : ''; // If no subscription ID or transaction ID, exit if ( empty( $subscription_id ) || empty( $transaction_id ) ) { return; } // Check if transaction already exists $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}wpuf_transaction WHERE transaction_id = %s", $transaction_id ) ); if ( $existing ) { // Even if transaction exists, clean up any transients $this->clean_up_transients( $subscription_id ); return; // Exit if transaction already processed } // Get payment amount $amount = 0; if ( isset( $payment['amount']['total'] ) ) { $amount = $payment['amount']['total']; } elseif ( isset( $payment['amount']['value'] ) ) { $amount = $payment['amount']['value']; } // Get currency $currency = ''; if ( isset( $payment['amount']['currency'] ) ) { $currency = $payment['amount']['currency']; } elseif ( isset( $payment['amount']['currency_code'] ) ) { $currency = $payment['amount']['currency_code']; } // Initialize custom data $custom_data = []; // Get custom data if ( isset( $payment['custom'] ) ) { $custom_data = json_decode( $payment['custom'], true ); } elseif ( isset( $payment['custom_id'] ) ) { $custom_data = json_decode( $payment['custom_id'], true ); } // If JSON decode failed, initialize as empty array if ( ! is_array( $custom_data ) ) { $custom_data = []; } // Check for pending subscription in transient $pending_subscription = get_transient( 'wpuf_paypal_pending_' . $subscription_id ); $user_id = null; $pack_id = 0; // If we have pending subscription data, use it if ( $pending_subscription && isset( $pending_subscription['user_id'] ) ) { $user_id = $pending_subscription['user_id']; if ( isset( $pending_subscription['pack_id'] ) ) { $pack_id = $pending_subscription['pack_id']; } } else { // Get user ID from subscription if not found in transient $user_id = $this->get_user_id_by_subscription( $subscription_id ); } // Try to get user ID from custom data if still not found if ( ! $user_id && isset( $custom_data['user_id'] ) ) { $user_id = $custom_data['user_id']; } if ( ! $user_id ) { // Clean up any transients even if user not found $this->clean_up_transients( $subscription_id ); return; } // Get user data $user = get_user_by( 'id', $user_id ); if ( ! $user ) { $this->clean_up_transients( $subscription_id ); return; } // If pack ID not found in transient, try to get it from custom data or meta if ( 0 === $pack_id ) { if ( isset( $custom_data['item_number'] ) && isset( $custom_data['type'] ) && $custom_data['type'] === 'pack' ) { $pack_id = $custom_data['item_number']; } else { // Try to get pack ID from user meta $pack_id = $this->get_pack_id_by_subscription( $user_id, $subscription_id ); } } // Check if a subscriber record already exists for this subscription $existing_subscriber = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}wpuf_subscribers WHERE user_id = %d AND transaction_id = %s", $user_id, $subscription_id ) ); // If no subscriber record exists yet and we have all the data, create one if ( ! $existing_subscriber && $pack_id > 0 ) { // Update user subscription status in WordPress - this should be the only record wpuf_get_user( $user_id )->subscription()->add_pack( $pack_id, $subscription_id, true, 'recurring' ); update_user_meta( $user_id, '_wpuf_paypal_subscription_status', 'completed' ); } // Extract tax and subtotal from PayPal's breakdown if available // For subscription payments, PayPal may include breakdown $total_amount = floatval( $amount ); $subtotal = $total_amount; // Default to total amount $tax = 0; if ( isset( $payment['amount']['breakdown'] ) ) { // Extract item_total (subtotal) from breakdown if ( isset( $payment['amount']['breakdown']['item_total']['value'] ) ) { $subtotal = floatval( $payment['amount']['breakdown']['item_total']['value'] ); } // Extract tax_total from breakdown if ( isset( $payment['amount']['breakdown']['tax_total']['value'] ) ) { $tax = floatval( $payment['amount']['breakdown']['tax_total']['value'] ); } // Validate breakdown: item_total + tax_total should equal total (within rounding) $breakdown_total = $subtotal + $tax; $difference = abs( $breakdown_total - $total_amount ); // If breakdown doesn't add up correctly, recalculate from total // This handles cases where PayPal returns incorrect breakdown values if ( $difference > 0.01 ) { // Breakdown is incorrect, try to fix it // If we have custom_data, use that instead if ( isset( $custom_data['subtotal'] ) && isset( $custom_data['tax'] ) ) { $subtotal = floatval( $custom_data['subtotal'] ); $tax = floatval( $custom_data['tax'] ); } else { // Recalculate: assume tax is correct, adjust subtotal // Or if subtotal seems wrong (equals total), calculate from total - tax if ( abs( $subtotal - $total_amount ) < 0.01 && $tax > 0 ) { // Subtotal equals total, which is wrong - recalculate $subtotal = $total_amount - $tax; } elseif ( $tax > 0 && $subtotal > 0 ) { // Both exist but don't add up - trust the total and recalculate $subtotal = $total_amount - $tax; } } } } elseif ( isset( $custom_data['subtotal'] ) && isset( $custom_data['tax'] ) ) { // Fallback: Use custom_id data if breakdown not available $subtotal = floatval( $custom_data['subtotal'] ); $tax = floatval( $custom_data['tax'] ); } else { // Fallback: Try to recalculate from subscription pack // This ensures accurate tax calculation based on current settings if ( $pack_id > 0 ) { /** * Filter: wpuf_recalculate_tax_from_pack * * Allows extensions (like WPUF Pro Tax) to recalculate tax from subscription pack * when PayPal breakdown data is missing or incorrect. * * @since WPUF_PRO_SINCE * * @param false|array $tax_data Array with 'subtotal', 'tax', and 'cost' keys, or false * @param int $pack_id Subscription pack ID * @param int $user_id User ID * @param float $total_amount Total amount from PayPal (for validation) * @param string $payment_type Payment type ('pack' or 'post') * * @return array|false Array with tax breakdown or false if unable to calculate */ $recalculated = apply_filters( 'wpuf_recalculate_tax_from_pack', false, $pack_id, $user_id, $total_amount, 'pack' ); if ( $recalculated && is_array( $recalculated ) ) { $subtotal = isset( $recalculated['subtotal'] ) ? floatval( $recalculated['subtotal'] ) : $subtotal; $tax = isset( $recalculated['tax'] ) ? floatval( $recalculated['tax'] ) : $tax; } else { // If recalculation failed, try subscription plan tax percentage $tax_percentage = $this->get_subscription_tax_percentage( $subscription_id ); if ( $tax_percentage > 0 ) { // Reverse calculate: subtotal = total / (1 + tax_percentage/100) $subtotal = $total_amount / ( 1 + ( $tax_percentage / 100 ) ); $tax = $total_amount - $subtotal; } } } else { // If no pack_id, try subscription plan tax percentage $tax_percentage = $this->get_subscription_tax_percentage( $subscription_id ); if ( $tax_percentage > 0 ) { // Reverse calculate: subtotal = total / (1 + tax_percentage/100) $subtotal = $total_amount / ( 1 + ( $tax_percentage / 100 ) ); $tax = $total_amount - $subtotal; } } } // Ensure cost is always subtotal + tax (for consistency) // Use total_amount as fallback if calculation seems off $calculated_cost = $subtotal + $tax; $cost = abs( $calculated_cost - $total_amount ) < 0.01 ? $calculated_cost : $total_amount; // Prepare payment data $data = [ 'user_id' => $user_id, 'status' => 'completed', 'subtotal' => $subtotal, 'profile_id' => $subscription_id, 'tax' => $tax, 'cost' => $cost, 'post_id' => 0, 'pack_id' => $pack_id, 'payer_first_name' => $user->first_name, 'payer_last_name' => $user->last_name, 'payer_email' => $user->user_email, 'payment_type' => 'paypal', 'transaction_id' => $transaction_id, 'created' => gmdate( 'Y-m-d H:i:s' ), ]; // Add custom meta information if available if ( isset( $payment['time'] ) ) { $data['created'] = gmdate( 'Y-m-d H:i:s', strtotime( $payment['time'] ) ); } // Insert payment record Payment::insert_payment( $data, $transaction_id, true ); // Final cleanup of any transients after successful processing $this->clean_up_transients( $subscription_id ); } catch ( \Exception $e ) { // Even in case of error, try to clean up transients if ( isset( $subscription_id ) ) { $this->clean_up_transients( $subscription_id ); } } } /** * Clean up all transients related to a subscription */ private function clean_up_transients( $subscription_id ) { // Delete the specific transient delete_transient( 'wpuf_paypal_pending_' . $subscription_id ); } /** * Get pack ID by subscription ID */ private function get_pack_id_by_subscription( $user_id, $subscription_id ) { global $wpdb; // First try to get from subscribers table $pack_id = $wpdb->get_var( $wpdb->prepare( "SELECT subscribtion_id FROM {$wpdb->prefix}wpuf_subscribers WHERE user_id = %d AND transaction_id = %s", $user_id, $subscription_id ) ); if ( $pack_id ) { return $pack_id; } // Then try to get from user meta $subscription = get_user_meta( $user_id, '_wpuf_subscription_pack', true ); if ( $subscription && isset( $subscription['pack_id'] ) ) { return $subscription['pack_id']; } return 0; } /** * Update payment options */ public function payment_options( $options ) { // Existing options $options[] = [ 'name' => 'paypal_email', 'label' => __( 'PayPal Email', 'wp-user-frontend' ), ]; $options[] = [ 'name' => 'gate_instruct_paypal', 'label' => __( 'PayPal Instruction', 'wp-user-frontend' ), 'type' => 'wysiwyg', 'default' => "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account", ]; // New REST API options $options[] = [ 'name' => 'paypal_client_id', 'label' => __( 'PayPal Client ID', 'wp-user-frontend' ), 'desc' => __( 'Enter your PayPal Client ID from your PayPal developer dashboard.', 'wp-user-frontend' ), ]; $options[] = [ 'name' => 'paypal_client_secret', 'label' => __( 'PayPal Client Secret', 'wp-user-frontend' ), 'desc' => __( 'Enter your PayPal Client Secret from your PayPal developer dashboard.', 'wp-user-frontend' ), ]; // Webhook URL field (read-only, auto-generated) $webhook_url = home_url( '/?action=webhook_triggered' ); $options[] = [ 'name' => 'paypal_webhook_url', 'label' => __( 'PayPal Webhook URL', 'wp-user-frontend' ), 'type' => 'text', 'default' => $webhook_url, 'attr' => 'disabled', 'readonly' => true, 'desc' => sprintf( /* translators: %s: webhook URL */ __( 'Copy this URL and add it to your PayPal webhook configuration in your PayPal developer dashboard. This URL is: <code>%s</code>', 'wp-user-frontend' ), esc_html( $webhook_url ) ), 'css' => 'background-color: #f9f9f9; color: #666;', ]; $options[] = [ 'name' => 'paypal_webhook_id', 'label' => __( 'PayPal Webhook ID', 'wp-user-frontend' ), 'desc' => sprintf( /* translators: %1$s: live dashboard link, %2$s: sandbox dashboard link */ __( 'Enter the Webhook ID provided by PayPal after creating the webhook. This is required for webhook verification. Find your webhook ID in your <a href="%1$s" target="_blank" rel="noopener noreferrer">PayPal dashboard</a>', 'wp-user-frontend' ), 'https://developer.paypal.com/' ), ]; // Legacy API options (keep for backward compatibility) $options[] = [ 'name' => 'paypal_api_username', 'label' => __( 'PayPal API username', 'wp-user-frontend' ), 'desc' => sprintf( /* translators: %1$s: live dashboard link, %2$s: sandbox dashboard link */ __( '(Legacy) <a href="%1$s" target="_blank" rel="noopener noreferrer">PayPal API username</a> for older integrations.', 'wp-user-frontend' ), 'https://developer.paypal.com/' ), ]; $options[] = [ 'name' => 'paypal_api_password', 'label' => __( 'PayPal API password', 'wp-user-frontend' ), 'desc' => __( '(Legacy) PayPal API password for older integrations.', 'wp-user-frontend' ), ]; $options[] = [ 'name' => 'paypal_api_signature', 'label' => __( 'PayPal API signature', 'wp-user-frontend' ), 'desc' => __( '(Legacy) PayPal API signature for older integrations.', 'wp-user-frontend' ), ]; // Replace sandbox mode checkbox with test mode radio button $options[] = [ 'name' => 'paypal_test_mode', 'label' => __( 'PayPal Mode', 'wp-user-frontend' ), 'type' => 'radio', 'default' => 'live', 'options' => [ 'live' => __( 'Live Mode', 'wp-user-frontend' ), 'test' => __( 'Test Mode (Sandbox)', 'wp-user-frontend' ), ], 'desc' => __( 'Choose whether to process real payments or test payments. Test mode uses PayPal Sandbox environment. Make sure to configure separate webhook URLs for each environment.', 'wp-user-frontend' ), ]; // Add error page setting $options[] = [ 'name' => 'paypal_error_page', 'label' => __( 'Payment Error Page', 'wp-user-frontend' ), 'type' => 'select', 'options' => $this->get_pages_dropdown(), 'desc' => __( 'Select the page where users will be redirected when payment errors occur. If not set, users will be redirected to home page with error message.', 'wp-user-frontend' ), ]; // Add webhook events information $options[] = [ 'name' => 'paypal_webhook_events_info', 'label' => '', // Empty label since we have both headings in the description 'type' => 'html', 'desc' => $this->get_webhook_events_notice(), ]; return $options; } /** * Get pages dropdown for settings */ private function get_pages_dropdown() { $pages = get_pages(); $options = [ '' => __( 'Select a page', 'wp-user-frontend' ) ]; foreach ( $pages as $page ) { $options[ $page->ID ] = $page->post_title; } return $options; } /** * Get error page URL */ private function get_error_page_url( $error_message = '' ) { $error_page_id = wpuf_get_option( 'paypal_error_page', 'wpuf_payment' ); if ( ! empty( $error_page_id ) && is_numeric( $error_page_id ) ) { $error_url = get_permalink( $error_page_id ); if ( ! empty( $error_message ) ) { $error_url = add_query_arg( 'wpuf_paypal_error', rawurlencode( $error_message ), $error_url ); } return $error_url; } // Fallback to home URL with error parameter if ( ! empty( $error_message ) ) { return home_url( '/?wpuf_paypal_error=' . rawurlencode( $error_message ) ); } return home_url(); } /** * Get webhook events notice HTML */ private function get_webhook_events_notice() { ob_start(); ?> <div class="wpuf-paypal-webhook-container"> <p><?php esc_html_e( 'Setup Guide:', 'wp-user-frontend' ); ?> <a href="<?php echo esc_url( $this->setup_guide_url ); ?>" target="_blank" rel="noopener noreferrer"> <?php esc_html_e( 'Follow our PayPal integration guide', 'wp-user-frontend' ); ?> </a> <?php esc_html_e( 'for step-by-step webhook configuration instructions.', 'wp-user-frontend' ); ?></p> </div> <?php return ob_get_clean(); } public function subscription_cancel( $user_id ) { try { $this->cancel_subscription( [ 'user_id' => $user_id ] ); } catch ( \Exception $e ) { // Update local meta even if PayPal API call fails $sub_meta = 'cancel'; wpuf_get_user( $user_id )->subscription()->update_meta( $sub_meta ); } } /** * Get PayPal allowed redirect hosts * * @return array Array of PayPal domains */ private function get_paypal_allowed_hosts() { return [ 'www.paypal.com', 'paypal.com', 'www.sandbox.paypal.com', 'sandbox.paypal.com', ]; } /** * Update coupon usage count * * @param int $coupon_id */ private function update_coupon_usage( $coupon_id ) { if ( empty( $coupon_id ) ) { return; } $pre_usage = get_post_meta( $coupon_id, '_coupon_used', true ); $pre_usage = empty( $pre_usage ) ? 0 : $pre_usage; $new_use = $pre_usage + 1; update_post_meta( $coupon_id, '_coupon_used', $new_use ); } /** * Prepare and send payment to PayPal * * @param array $data Payment data */ public function prepare_to_send( $data ) { try { $user_id = $data['user_info']['id']; $return_url = add_query_arg( [ 'action' => 'wpuf_paypal_success', 'type' => $data['type'], 'item_number' => $data['item_number'], 'wpuf_payment_method' => 'paypal', '_wpnonce' => wp_create_nonce( 'wpuf_paypal_return' ), ], wpuf_payment_success_page( [ 'type' => isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : 'pack', 'item_number' => isset( $_GET['item_number'] ) ? sanitize_text_field( wp_unslash( $_GET['item_number'] ) ) : '', 'wpuf_payment_method' => 'paypal', ] ) ); $cancel_url = $return_url; $billing_amount = empty( $data['price'] ) ? 0 : $data['price']; // Check if pricing fields payment is enabled and update price accordingly $post_id = isset( $data['item_number'] ) && $data['type'] === 'post' ? $data['item_number'] : 0; if ( $post_id && $data['type'] === 'post' ) { $form_id = get_post_meta( $post_id, '_wpuf_form_id', true ); if ( $form_id ) { $form_settings = wpuf_get_form_settings( $form_id ); $pricing_enabled = isset( $form_settings['enable_pricing_payment'] ) && wpuf_is_checkbox_or_toggle_on( $form_settings['enable_pricing_payment'] ); if ( $pricing_enabled ) { $pricing_cost = get_post_meta( $post_id, '_wpuf_pricing_field_cost', true ); if ( $pricing_cost && is_numeric( $pricing_cost ) ) { $billing_amount = floatval( $pricing_cost ); } } } } // Handle coupon if present if ( isset( $_POST['coupon_id'] ) && ! empty( $_POST['coupon_id'] ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'wpuf_payment_coupon' ) && is_numeric( sanitize_text_field( wp_unslash( $_POST['coupon_id'] ) ) ) ) { $coupon_id = absint( sanitize_text_field( wp_unslash( $_POST['coupon_id'] ) ) ); $billing_amount = wpuf_pro()->coupon->discount( $billing_amount, $coupon_id, $data['item_number'] ); } else { $coupon_id = ''; } // Build standardized payment data structure $payment_data = [ 'amount' => $billing_amount, // Base amount before modifications 'currency' => $data['currency'], 'type' => $data['type'], 'item_number' => $data['item_number'], 'item_name' => $data['item_name'], 'user_id' => $user_id, 'post_id' => $post_id, 'coupon_id' => $coupon_id, 'custom' => isset( $data['custom'] ) ? $data['custom'] : [], ]; /** * Filter: wpuf_payment_data_before_gateway * * Allows extensions to modify payment data before sending to gateway. * Extensions can add breakdown items (tax, fees, discounts) and calculate total. * * Expected structure after modifications: * [ * 'amount' => 100.00, // Original amount * 'total' => 110.00, // Final amount (set by extensions) * 'currency' => 'USD', * 'breakdown' => [ // Optional: detailed breakdown * 'item_total' => 100.00, * 'tax_total' => 10.00, * 'discount' => 0.00, * // Extensions can add more * ], * 'metadata' => [...], // Optional: additional data * ] * * @param array $payment_data Standardized payment data structure * @param array $original_data Original payment data from form submission * * @return array Modified payment data with 'total' set * * @since WPUF_PRO_SINCE */ $payment_data = apply_filters( 'wpuf_payment_data_before_gateway', $payment_data, $data ); // Get final amount (use 'total' if set by extensions, otherwise use 'amount') $billing_amount = isset( $payment_data['total'] ) ? $payment_data['total'] : $payment_data['amount']; // Apply legacy payment amount filter ONLY if total wasn't set by new system // This prevents double tax application if ( ! isset( $payment_data['total'] ) ) { $billing_amount = apply_filters( 'wpuf_payment_amount', $billing_amount, $post_id ); } // Update payment data with final amount $payment_data['total'] = $billing_amount; // Handle free payments if ( $billing_amount == 0 ) { wpuf_get_user( $user_id )->subscription()->add_pack( $data['item_number'], null, false, 'Free' ); wp_safe_redirect( $return_url ); exit(); } // Get access token $access_token = $this->get_access_token(); // Check if this is a recurring payment if ( 'pack' === $data['type'] && isset( $data['custom']['recurring_pay'] ) && wpuf_is_checkbox_or_toggle_on( $data['custom']['recurring_pay'] ) ) { // Get subscription details from pack $pack = get_post( $data['item_number'] ); $flattened_subscription_meta = array_map( function ( $value ) { return maybe_unserialize( $value[0] ); }, get_post_meta( $pack->ID ) ); if ( empty( $flattened_subscription_meta ) ) { throw new \Exception( 'Invalid subscription pack' ); } // Get subscription period and interval $period = isset( $flattened_subscription_meta['_cycle_period'] ) ? $flattened_subscription_meta['_cycle_period'] : 'month'; $interval = isset( $flattened_subscription_meta['_billing_cycle_number'] ) ? intval( $flattened_subscription_meta['_billing_cycle_number'] ) : 1; // Handle trial period $trial_period_days = 0; if ( isset( $data['custom']['trial_status'] ) && wpuf_is_checkbox_or_toggle_on( $data['custom']['trial_status'] ) ) { $trial_duration_type = $data['custom']['trial_duration_type']; $trial_duration = absint( $data['custom']['trial_duration'] ); switch ( $trial_duration_type ) { case 'week': $trial_period_days = $trial_duration * 7; break; case 'month': $trial_period_days = $trial_duration * 30; break; case 'year': $trial_period_days = $trial_duration * 365; break; case 'day': default: $trial_period_days = $trial_duration; break; } // Set trial meta for once per user if ( ! get_user_meta( $user_id, '_wpuf_used_trial', true ) ) { update_user_meta( $user_id, '_wpuf_used_trial', 'yes' ); } } // Create a plan with base amount (without tax, as tax will be added via subscription override) $plan_base_amount = isset( $payment_data['breakdown']['item_total'] ) ? $payment_data['breakdown']['item_total'] : $billing_amount; $plan_id = $this->get_or_create_plan( $pack, $plan_base_amount, $period, $interval, $trial_period_days ); if ( ! $plan_id ) { throw new \Exception( 'Failed to create or get subscription plan' ); } // Prepare subscription data $subscription_data = [ 'plan_id' => $plan_id, 'application_context' => [ 'brand_name' => get_bloginfo( 'name' ), 'shipping_preference' => 'NO_SHIPPING', 'user_action' => 'SUBSCRIBE_NOW', 'return_url' => $return_url, 'cancel_url' => $cancel_url, ], 'custom_id' => wp_json_encode( [ 'type' => $data['type'], 'user_id' => $user_id, 'item_number' => $data['item_number'], 'coupon_id' => $coupon_id, ] ), ]; // Add tax override if tax is included in payment_data if ( isset( $payment_data['breakdown']['tax_total'] ) && $payment_data['breakdown']['tax_total'] > 0 ) { $tax_amount = $payment_data['breakdown']['tax_total']; $subtotal = $payment_data['breakdown']['item_total'] ?? $payment_data['amount']; // Calculate tax percentage $tax_percentage = ( $tax_amount / $subtotal ) * 100; // Add plan override with tax $subscription_data['plan'] = [ 'taxes' => [ 'percentage' => number_format( $tax_percentage, 2, '.', '' ), 'inclusive' => false, ], ]; } /** * Filter: wpuf_paypal_subscription_data * * Modify PayPal subscription data before sending to API. * Allows adding/removing/modifying subscription parameters. * * @param array $subscription_data PayPal subscription API payload * @param array $data Original payment data * * @return array Modified subscription data * * @since WPUF_PRO_SINCE */ $subscription_data = apply_filters( 'wpuf_paypal_subscription_data', $subscription_data, $data ); // Create subscription $response = wp_remote_post( ( $this->test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com' ) . '/v1/billing/subscriptions', [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', 'Prefer' => 'return=representation', ], 'body' => wp_json_encode( $subscription_data ), ] ); if ( is_wp_error( $response ) ) { throw new \Exception( 'Failed to create PayPal subscription: ' . $response->get_error_message() ); } $response_code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! isset( $body['id'] ) ) { throw new \Exception( 'Invalid response from PayPal - no subscription ID' ); } // Store subscription details in transient instead of creating initial record set_transient( 'wpuf_paypal_pending_' . $body['id'], [ 'user_id' => $user_id, 'pack_id' => $data['item_number'], 'subscription_id' => $body['id'], 'status' => 'pending', 'created' => gmdate( 'Y-m-d H:i:s' ), 'trial_period_days' => $trial_period_days, ], HOUR_IN_SECONDS * 24 // Expire after 24 hours ); // Find approval URL $approval_url = ''; foreach ( $body['links'] as $link ) { if ( 'approve' === $link['rel'] ) { $approval_url = $link['href']; break; } } if ( empty( $approval_url ) ) { throw new \Exception( 'Approval URL not found in PayPal response' ); } // Add PayPal to allowed hosts just before redirect add_filter( 'allowed_redirect_hosts', function( $hosts ) { return array_merge( $hosts, $this->get_paypal_allowed_hosts() ); }, 10, 1 ); // Redirect to PayPal wp_safe_redirect( $approval_url ); exit(); } else { // Build PayPal order data $paypal_order_data = [ 'intent' => 'CAPTURE', 'purchase_units' => [ [ 'amount' => $this->build_paypal_amount( $payment_data ), 'description' => isset( $data['custom']['post_title'] ) ? $data['custom']['post_title'] : $data['item_name'], 'custom_id' => wp_json_encode( [ 'type' => $payment_data['type'], 'user_id' => $payment_data['user_id'], 'coupon_id' => $payment_data['coupon_id'], 'item_number' => $payment_data['item_number'], 'subtotal' => isset( $payment_data['breakdown']['item_total'] ) ? $payment_data['breakdown']['item_total'] : $payment_data['amount'], 'tax' => isset( $payment_data['breakdown']['tax_total'] ) ? $payment_data['breakdown']['tax_total'] : 0, ] ), ], ], 'application_context' => [ 'return_url' => $return_url, 'cancel_url' => $cancel_url, 'brand_name' => get_bloginfo( 'name' ), 'user_action' => 'PAY_NOW', 'shipping_preference' => 'NO_SHIPPING', ], ]; /** * Filter: wpuf_paypal_order_data * * Modify PayPal order/payment data before sending to API. * Allows adding/removing/modifying order parameters. * * @param array $paypal_order_data PayPal order API payload * @param array $data Original payment data * * @return array Modified order data * * @since WPUF_PRO_SINCE */ $paypal_order_data = apply_filters( 'wpuf_paypal_order_data', $paypal_order_data, $data ); // Create order $response = wp_remote_post( $this->test_mode ? 'https://api-m.sandbox.paypal.com/v2/checkout/orders' : 'https://api-m.paypal.com/v2/checkout/orders', [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', ], 'body' => wp_json_encode( $paypal_order_data ), ] ); if ( is_wp_error( $response ) ) { throw new \Exception( 'Failed to create PayPal order: ' . $response->get_error_message() ); } $response_code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! isset( $body['id'] ) ) { throw new \Exception( 'Invalid response from PayPal - no order ID' ); } // Find approval URL $approval_url = ''; foreach ( $body['links'] as $link ) { if ( 'approve' === $link['rel'] ) { $approval_url = $link['href']; break; } } if ( empty( $approval_url ) ) { throw new \Exception( 'Approval URL not found in PayPal response' ); } // Add PayPal to allowed hosts just before redirect add_filter( 'allowed_redirect_hosts', function( $hosts ) { return array_merge( $hosts, $this->get_paypal_allowed_hosts() ); }, 10, 1 ); wp_safe_redirect( $approval_url ); exit(); } } catch ( \Exception $e ) { wp_die( esc_html( $e->getMessage() ) ); } } /** * Get or create a PayPal subscription plan */ private function get_or_create_plan( $pack, $amount, $period, $interval, $trial_period_days = 0 ) { try { $access_token = $this->get_access_token(); if ( ! $access_token ) { return false; } // Create plan name (tax will be added separately via subscription override) $plan_name = 'WPUF-' . $pack->post_title . '-' . uniqid(); $plan_id = get_post_meta( $pack->ID, '_paypal_plan_id', true ); // If plan exists and is active, return it if ( $plan_id ) { $plan_url = ( $this->test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com' ) . '/v1/billing/plans/' . $plan_id; $response = wp_remote_get( $plan_url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', ], ] ); if ( ! is_wp_error( $response ) ) { $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( isset( $body['status'] ) && 'ACTIVE' === $body['status'] ) { return $plan_id; } } } // Create new plan (tax will be added separately via subscription override) // Ensure interval_count is at least 1 (PayPal doesn't accept 0 or negative values) $interval_count = max( 1, intval( $interval ) ); $plan_data = [ 'product_id' => $this->get_or_create_product( $pack ), 'name' => $plan_name, 'description' => $pack->post_title, 'status' => 'ACTIVE', 'billing_cycles' => [ [ 'frequency' => [ 'interval_unit' => strtoupper( $period ), 'interval_count' => $interval_count, ], 'tenure_type' => 'REGULAR', 'sequence' => 1, 'total_cycles' => 0, // Unlimited 'pricing_scheme' => [ 'fixed_price' => [ 'value' => number_format( $amount, 2, '.', '' ), 'currency_code' => wpuf_get_option( 'currency', 'wpuf_payment', 'USD' ), ], ], ], ], 'payment_preferences' => [ 'auto_bill_outstanding' => true, 'setup_fee' => [ 'value' => '0', 'currency_code' => wpuf_get_option( 'currency', 'wpuf_payment', 'USD' ), ], 'setup_fee_failure_action' => 'CONTINUE', 'payment_failure_threshold' => 3, ], ]; // Add trial period if specified if ( $trial_period_days > 0 ) { $plan_data['payment_preferences']['setup_fee_failure_action'] = 'CONTINUE'; // Add trial period as a billing cycle before the regular one array_unshift( $plan_data['billing_cycles'], [ 'frequency' => [ 'interval_unit' => 'DAY', 'interval_count' => $trial_period_days, ], 'tenure_type' => 'TRIAL', 'sequence' => 1, 'total_cycles' => 1, 'pricing_scheme' => [ 'fixed_price' => [ 'value' => '0', 'currency_code' => wpuf_get_option( 'currency', 'wpuf_payment', 'USD' ), ], ], ] ); // Update the regular billing cycle sequence $plan_data['billing_cycles'][1]['sequence'] = 2; } $response = wp_remote_post( ( $this->test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com' ) . '/v1/billing/plans', [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', 'Prefer' => 'return=representation', ], 'body' => wp_json_encode( $plan_data ), ] ); if ( is_wp_error( $response ) ) { throw new \Exception( 'Failed to create PayPal plan: ' . $response->get_error_message() ); } $response_code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! isset( $body['id'] ) ) { throw new \Exception( 'Invalid response from PayPal - no plan ID' ); } // Store plan ID update_post_meta( $pack->ID, '_paypal_plan_id', $body['id'] ); return $body['id']; } catch ( \Exception $e ) { return false; } } /** * Get or create a PayPal product */ private function get_or_create_product( $pack ) { try { $access_token = $this->get_access_token(); $product_id = get_post_meta( $pack->ID, '_paypal_product_id', true ); // If product exists, return it if ( $product_id ) { return $product_id; } // Create new product $product_data = [ 'name' => $pack->post_title, 'description' => $pack->post_excerpt ? $pack->post_excerpt : $pack->post_title, 'type' => 'SERVICE', 'category' => 'SERVICES', ]; $response = wp_remote_post( ( $this->test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com' ) . '/v1/catalogs/products', [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', ], 'body' => wp_json_encode( $product_data ), ] ); if ( is_wp_error( $response ) ) { throw new \Exception( 'Failed to create PayPal product: ' . $response->get_error_message() ); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! isset( $body['id'] ) ) { throw new \Exception( 'Invalid response from PayPal - no product ID' ); } // Store product ID update_post_meta( $pack->ID, '_paypal_product_id', $body['id'] ); return $body['id']; } catch ( \Exception $e ) { return false; } } /** * Get user ID by subscription ID */ private function get_user_id_by_subscription( $subscription_id ) { global $wpdb; // First try from subscribers table $user_id = $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM {$wpdb->prefix}wpuf_subscribers WHERE transaction_id = %s", $subscription_id ) ); if ( $user_id ) { return $user_id; } // Then try from usermeta table $user_id = $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = '_wpuf_subscription_pack' AND meta_value LIKE %s", '%' . $wpdb->esc_like( $subscription_id ) . '%' ) ); return $user_id; } /** * Handle subscription return from PayPal */ public function handle_subscription_return() { try { $subscription_id = isset( $_GET['subscription_id'] ) ? sanitize_text_field( wp_unslash( $_GET['subscription_id'] ) ) : ''; $ba_token = isset( $_GET['ba_token'] ) ? sanitize_text_field( wp_unslash( $_GET['ba_token'] ) ) : ''; $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; // If we have a token but no subscription_id, the token might be the subscription ID if ( empty( $subscription_id ) && ! empty( $token ) ) { $subscription_id = $token; } // Redirect to success page for subscriptions without requiring nonce $success_url = add_query_arg( [ 'action' => 'wpuf_paypal_success', 'type' => isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : 'pack', 'item_number' => isset( $_GET['item_number'] ) ? sanitize_text_field( wp_unslash( $_GET['item_number'] ) ) : '', 'wpuf_payment_method' => 'paypal', 'payment_status' => 'subscription_created', 'subscription_id' => $subscription_id, 'skip_nonce_check' => '1', // Skip nonce check for subscription success page ], wpuf_payment_success_page( [ 'type' => isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : 'pack', 'item_number' => isset( $_GET['item_number'] ) ? sanitize_text_field( wp_unslash( $_GET['item_number'] ) ) : '', 'wpuf_payment_method' => 'paypal', ] ) ); wp_safe_redirect( $success_url ); exit; } catch ( \Exception $e ) { wp_safe_redirect( $this->get_error_page_url( $e->getMessage() ) ); exit; } } /** * Handle PayPal return */ public function handle_paypal_return() { //nonce check if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wpuf_paypal_return' ) ) { wp_safe_redirect( $this->get_error_page_url( 'Invalid nonce' ) ); exit; } $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; $payer_id = isset( $_GET['PayerID'] ) ? sanitize_text_field( wp_unslash( $_GET['PayerID'] ) ) : ''; if ( empty( $token ) || empty( $payer_id ) ) { if ( isset( $_GET['payment_status'] ) ) { return; // Just show success page } // Redirect to subscription page with error message $error_url = add_query_arg( [ 'action' => 'wpuf_paypal_success', 'type' => isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : 'pack', 'item_number' => isset( $_GET['item_number'] ) ? sanitize_text_field( wp_unslash( $_GET['item_number'] ) ) : '', 'wpuf_payment_method' => 'paypal', 'payment_status' => 'failed', 'error' => rawurlencode( 'Invalid payment session. Please try again.' ), ], wpuf_payment_success_page( [ 'type' => isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : 'pack', 'item_number' => isset( $_GET['item_number'] ) ? sanitize_text_field( wp_unslash( $_GET['item_number'] ) ) : '', 'wpuf_payment_method' => 'paypal', ] ) ); wp_safe_redirect( $error_url ); exit; } try { // Get access token and capture payment $access_token = $this->get_access_token(); $capture_url = ( $this->test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com' ) . '/v2/checkout/orders/' . $token . '/capture'; $response = wp_remote_post( $capture_url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', ], ] ); if ( is_wp_error( $response ) ) { throw new \Exception( 'Failed to capture PayPal payment' ); } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! isset( $body['status'] ) || $body['status'] !== 'COMPLETED' ) { throw new \Exception( 'Payment not completed' ); } // Redirect to success page $success_url = add_query_arg( [ 'action' => 'wpuf_paypal_success', 'type' => isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : 'pack', 'item_number' => isset( $_GET['item_number'] ) ? sanitize_text_field( wp_unslash( $_GET['item_number'] ) ) : '', 'wpuf_payment_method' => 'paypal', 'payment_status' => 'completed', 'payment_completed' => '1', '_wpnonce' => wp_create_nonce( 'wpuf_paypal_return' ), ], wpuf_payment_success_page( [ 'type' => isset( $_GET['type'] ) ? sanitize_text_field( wp_unslash( $_GET['type'] ) ) : 'pack', 'item_number' => isset( $_GET['item_number'] ) ? sanitize_text_field( wp_unslash( $_GET['item_number'] ) ) : '', 'wpuf_payment_method' => 'paypal', ] ) ); wp_safe_redirect( $success_url ); exit; } catch ( \Exception $e ) { wp_safe_redirect( $this->get_error_page_url( $e->getMessage() ) ); exit; } } /** * Check PayPal return */ public function check_paypal_return() { // Only check nonce if this is actually a PayPal return request if ( ! isset( $_GET['action'] ) || 'wpuf_paypal_success' !== sanitize_text_field( wp_unslash( $_GET['action'] ) ) ) { return; } // Check if we're already on the success page to prevent redirect loops if ( isset( $_GET['skip_nonce_check'] ) || isset( $_GET['payment_completed'] ) || isset( $_GET['payment_status'] ) ) { // We're already on the success page, don't process again return; } // Check if this is a subscription return (has subscription_id parameter or type is pack with recurring) $is_subscription_return = isset( $_GET['subscription_id'] ) || isset( $_GET['ba_token'] ) || ( isset( $_GET['type'] ) && $_GET['type'] === 'pack' && ( isset( $_GET['token'] ) && strpos( $_GET['token'], 'I-' ) === 0 ) ); // For subscription returns, nonce verification might fail due to PayPal's redirect process // So we'll be more lenient with subscription returns if ( ! $is_subscription_return ) { // Only enforce strict nonce checking for regular payments if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wpuf_paypal_return' ) ) { wp_safe_redirect( $this->get_error_page_url( 'Invalid nonce' ) ); exit; } } // Handle subscription return differently if ( $is_subscription_return ) { $this->handle_subscription_return(); } else { $this->handle_paypal_return(); } } /** * Add pending payment page handler */ public function handle_pending_payment() { // Only check nonce if this is actually a PayPal pending request if ( ! isset( $_GET['action'] ) || 'wpuf_paypal_pending' !== sanitize_text_field( wp_unslash( $_GET['action'] ) ) ) { return; } // Now check nonce since we know this is a PayPal pending request if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wpuf_paypal_return' ) ) { wp_safe_redirect( $this->get_error_page_url( 'Invalid nonce' ) ); exit; } $capture_id = isset( $_GET['capture_id'] ) ? sanitize_text_field( wp_unslash( $_GET['capture_id'] ) ) : ''; if ( empty( $capture_id ) ) { wp_safe_redirect( $this->get_error_page_url( 'Invalid capture ID' ) ); exit; } // Show pending payment page include WPUF_ROOT . '/templates/payment-pending.php'; exit; } /** * Handle subscription cancellation from webhook */ private function handle_subscription_cancelled( $subscription ) { try { $subscription_id = isset( $subscription['id'] ) ? $subscription['id'] : 'UNKNOWN'; // Extract custom data $custom_data = []; if ( isset( $subscription['custom_id'] ) ) { $custom_data = json_decode( $subscription['custom_id'], true ); } if ( ! $custom_data || ! isset( $custom_data['user_id'] ) ) { throw new \Exception( 'Invalid custom data in subscription' ); } $user_id = $custom_data['user_id']; $subscription_id = $subscription['id']; // Update subscription status in user meta $subscription_data = [ 'profile_id' => $subscription_id, 'status' => 'cancel', 'updated' => gmdate( 'Y-m-d H:i:s' ), ]; update_user_meta( $user_id, '_wpuf_subscription_pack', $subscription_data ); // Update subscriber table global $wpdb; $result = $wpdb->update( $wpdb->prefix . 'wpuf_subscribers', [ 'subscribtion_status' => 'cancel', 'expire' => gmdate( 'd-m-Y' ), ], [ 'user_id' => $user_id, 'transaction_id' => $subscription_id, 'gateway' => 'PayPal', ], [ '%s', '%s' ], [ '%d', '%s', '%s' ] ); // Trigger action for other plugins do_action( 'wpuf_paypal_subscription_cancelled', $user_id, $subscription_id ); } catch ( \Exception $e ) { throw $e; } } /** * Handle subscription activation (after trial period) */ private function handle_subscription_activated( $subscription ) { try { $subscription_id = isset( $subscription['id'] ) ? $subscription['id'] : 'UNKNOWN'; // Extract custom data $custom_data = []; if ( isset( $subscription['custom_id'] ) ) { $custom_data = json_decode( $subscription['custom_id'], true ); } // Get pack_id from custom_data (item_number is the subscription pack ID) $pack_id = 0; if ( isset( $custom_data['item_number'] ) && 'pack' === $custom_data['type'] ) { $pack_id = intval( $custom_data['item_number'] ); } if ( ! $pack_id ) { throw new \Exception( 'No subscription pack ID found in custom_data' ); } // Get the subscription pack directly by pack ID $subscription_pack = wpuf()->subscription->get_subscription( $pack_id ); if ( ! $subscription_pack || ! isset( $subscription_pack->meta_value ) ) { throw new \Exception( sprintf( 'No subscription pack found for pack ID: %d', $pack_id ) ); } // Get user_id from custom_data or try to find by subscription ID $user_id = 0; if ( isset( $custom_data['user_id'] ) ) { $user_id = intval( $custom_data['user_id'] ); } else { // Try to find the user based on subscription ID $user_id = $this->get_user_id_by_subscription( $subscription_id ); } if ( ! $user_id ) { throw new \Exception( 'Could not find user for subscription: ' . $subscription_id ); } // Get user pack to check status and trial $user_pack = get_user_meta( $user_id, '_wpuf_subscription_pack', true ); // Update subscription status if needed if ( ! $user_pack || ! isset( $user_pack['pack_id'] ) || $user_pack['pack_id'] !== $pack_id ) { // User pack doesn't exist or doesn't match, create/update it $user_pack = [ 'pack_id' => $pack_id, 'status' => 'completed', ]; } if ( isset( $user_pack['status'] ) && 'completed' !== $user_pack['status'] ) { $user_pack['status'] = 'completed'; } // Update user meta with subscription pack and PayPal subscription ID update_user_meta( $user_id, '_wpuf_subscription_pack', $user_pack ); update_user_meta( $user_id, '_wpuf_paypal_subscription_id', $subscription_id ); // If this is the first payment after a trial, create a payment record if ( isset( $user_pack['trial'] ) && 'yes' === $user_pack['trial'] ) { // Get subscription details from PayPal $access_token = $this->get_access_token(); $subscription_url = ( $this->test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com' ) . '/v1/billing/subscriptions/' . $subscription_id; $response = wp_remote_get( $subscription_url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', ], ] ); if ( is_wp_error( $response ) ) { throw new \Exception( 'Failed to fetch subscription details: ' . $response->get_error_message() ); } $subscription_details = json_decode( wp_remote_retrieve_body( $response ), true ); // Create payment record for the first real payment if ( isset( $subscription_details['billing_info']['last_payment'] ) ) { $payment = $subscription_details['billing_info']['last_payment']; $payment_data = [ 'user_id' => $user_id, 'status' => 'completed', 'subtotal' => $payment['amount']['value'], 'tax' => 0, // the payment record structure in the database expects a tax field 'cost' => $payment['amount']['value'], 'post_id' => 0, 'pack_id' => $pack_id, 'payer_first_name' => get_user_meta( $user_id, 'first_name', true ), 'payer_last_name' => get_user_meta( $user_id, 'last_name', true ), 'payer_email' => get_user_by( 'id', $user_id )->user_email, 'payment_type' => 'PayPal', 'transaction_id' => $payment['id'], 'profile_id' => $subscription_id, 'created' => gmdate( 'Y-m-d H:i:s' ), ]; Payment::insert_payment( $payment_data, $payment['id'], true ); } } } catch ( \Exception $e ) { throw new \Exception( 'Error handling subscription activation: ' . $e->getMessage(), 0, $e ); } } /** * Get tax percentage from subscription plan * * @since WPUF_PRO_SINCE * * @param string $subscription_id PayPal subscription ID * * @return float Tax percentage (0 if not found) */ private function get_subscription_tax_percentage( $subscription_id ) { if ( empty( $subscription_id ) ) { return 0; } try { $access_token = $this->get_access_token(); $subscription_url = ( $this->test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com' ) . '/v1/billing/subscriptions/' . $subscription_id; $response = wp_remote_get( $subscription_url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', ], ] ); if ( ! is_wp_error( $response ) ) { $subscription_details = json_decode( wp_remote_retrieve_body( $response ), true ); if ( isset( $subscription_details['plan']['taxes']['percentage'] ) ) { return floatval( $subscription_details['plan']['taxes']['percentage'] ); } } } catch ( \Exception $e ) {} return 0; } /** * Build PayPal amount structure from payment data * * Supports breakdown for tax, discounts, fees, etc. * * @since WPUF_PRO_SINCE * * @param array $payment_data Payment data with optional breakdown * * @return array PayPal amount structure */ private function build_paypal_amount( $payment_data ) { $amount = [ 'currency_code' => $payment_data['currency'], 'value' => number_format( $payment_data['total'], 2, '.', '' ), ]; // Add breakdown if provided by extensions if ( isset( $payment_data['breakdown'] ) && is_array( $payment_data['breakdown'] ) ) { $breakdown = $this->build_paypal_breakdown( $payment_data['breakdown'], $payment_data['currency'] ); // Only add breakdown if it has items if ( ! empty( $breakdown ) ) { $amount['breakdown'] = $breakdown; } } return $amount; } /** * Build PayPal breakdown structure * * Converts WPUF breakdown format to PayPal API format. * PayPal supports: item_total, tax_total, shipping, handling, insurance, shipping_discount, discount * * @since WPUF_PRO_SINCE * * @param array $breakdown Breakdown data from payment_data * @param string $currency Currency code * * @return array PayPal breakdown structure */ private function build_paypal_breakdown( $breakdown, $currency ) { $paypal_breakdown = []; // Map of WPUF breakdown keys to PayPal breakdown keys $breakdown_map = [ 'item_total' => 'item_total', 'tax_total' => 'tax_total', 'shipping' => 'shipping', 'handling' => 'handling', 'insurance' => 'insurance', 'shipping_discount' => 'shipping_discount', 'discount' => 'discount', ]; foreach ( $breakdown_map as $wpuf_key => $paypal_key ) { if ( isset( $breakdown[ $wpuf_key ] ) && $breakdown[ $wpuf_key ] > 0 ) { $paypal_breakdown[ $paypal_key ] = [ 'currency_code' => $currency, 'value' => number_format( $breakdown[ $wpuf_key ], 2, '.', '' ), ]; } } // Handle custom fees or other breakdown items not directly supported by PayPal // Add them to item_total $supported_keys = array_keys( $breakdown_map ); foreach ( $breakdown as $key => $value ) { if ( ! in_array( $key, $supported_keys ) && is_numeric( $value ) && $value > 0 ) { // Add unsupported breakdown items to item_total if ( ! isset( $paypal_breakdown['item_total'] ) ) { $paypal_breakdown['item_total'] = [ 'currency_code' => $currency, 'value' => '0.00', ]; } $current_item_total = (float) $paypal_breakdown['item_total']['value']; $paypal_breakdown['item_total']['value'] = number_format( $current_item_total + (float) $value, 2, '.', '' ); } } return $paypal_breakdown; } } // Register webhook endpoint add_action( 'init', function () { add_rewrite_rule( '^webhook_triggered/?$', 'index.php?action=webhook_triggered=1', 'top' ); } ); // Add query var add_filter( 'query_vars', function ( $vars ) { $vars[] = 'action'; return $vars; } ); // Handle webhook request add_action( 'template_redirect', function () { if ( get_query_var( 'action' ) === 'webhook_triggered' ) { $paypal = new \WeDevs\Wpuf\Lib\Gateway\Paypal(); $raw_input = file_get_contents( 'php://input' ); $paypal->process_webhook( $raw_input ); exit; } } );
Copyright ©2021 || Defacer Indonesia