--- a/wp-user-frontend/Lib/Gateway/Bank_Gateway.php
+++ b/wp-user-frontend/Lib/Gateway/Bank_Gateway.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace WeDevsWpufLibGateway;
+
+use WeDevsWpufAbstractsPayment_Gateway;
+
+/**
+ * Bank Payment Gateway
+ *
+ * Handles bank transfer payment processing using the new gateway architecture.
+ *
+ * @since WPUF_PRO_SINCE
+ */
+class Bank_Gateway extends Payment_Gateway {
+
+ /**
+ * Initialize gateway properties
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @return void
+ */
+ protected function init() {
+ $this->id = 'bank';
+ $this->admin_label = __( 'Bank Payment', 'wp-user-frontend' );
+ $this->checkout_label = __( 'Bank Payment', 'wp-user-frontend' );
+ $this->icon = '';
+ $this->supports_subscription = false;
+ }
+
+ /**
+ * Setup WordPress hooks
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @return void
+ */
+ protected function setup_hooks() {
+ parent::setup_hooks();
+
+ // Bank-specific hooks
+ add_action( 'wpuf_gateway_bank_order_submit', [ $this, 'order_notify_admin' ] );
+ add_action( 'wpuf_gateway_bank_order_complete', [ $this, 'order_notify_user' ], 10, 2 );
+ }
+
+ /**
+ * Process payment
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param array $payment_data Payment data
+ *
+ * @return void
+ */
+ public function process_payment( $payment_data ) {
+ // Delegate to the existing Bank class for now
+ // This maintains backward compatibility while we transition
+ $legacy_bank = isset( wpuf()->bank ) ? wpuf()->bank : null;
+ if ( $legacy_bank && method_exists( $legacy_bank, 'prepare_to_send' ) ) {
+ $legacy_bank->prepare_to_send( $payment_data );
+ }
+ }
+
+ /**
+ * Register gateway settings
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param array $options Existing payment options
+ *
+ * @return array Modified options array
+ */
+ public function register_settings( $options ) {
+ // Delegate to the existing Bank class for now
+ $legacy_bank = isset( wpuf()->bank ) ? wpuf()->bank : null;
+ if ( $legacy_bank && method_exists( $legacy_bank, 'payment_options' ) ) {
+ return $legacy_bank->payment_options( $options );
+ }
+
+ return $options;
+ }
+
+ /**
+ * Send payment received mail to admin
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param array $info Payment information
+ *
+ * @return void
+ */
+ public function order_notify_admin( $info ) {
+ $legacy_bank = isset( wpuf()->bank ) ? wpuf()->bank : null;
+ if ( $legacy_bank && method_exists( $legacy_bank, 'order_notify_admin' ) ) {
+ $legacy_bank->order_notify_admin( $info );
+ }
+ }
+
+ /**
+ * Send payment confirm mail to the user
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param array $transaction Transaction data
+ * @param int $order_id Order ID
+ *
+ * @return void
+ */
+ public function order_notify_user( $transaction, $order_id ) {
+ $legacy_bank = isset( wpuf()->bank ) ? wpuf()->bank : null;
+ if ( $legacy_bank && method_exists( $legacy_bank, 'order_notify_user' ) ) {
+ $legacy_bank->order_notify_user( $transaction, $order_id );
+ }
+ }
+}
--- a/wp-user-frontend/Lib/Gateway/Gateway_Manager.php
+++ b/wp-user-frontend/Lib/Gateway/Gateway_Manager.php
@@ -0,0 +1,194 @@
+<?php
+
+namespace WeDevsWpufLibGateway;
+
+use WeDevsWpufAbstractsPayment_Gateway;
+
+/**
+ * Payment Gateway Manager
+ *
+ * Manages registration and initialization of payment gateways.
+ *
+ * @since WPUF_PRO_SINCE
+ */
+class Gateway_Manager {
+
+ /**
+ * Registered gateway instances
+ *
+ * @var Payment_Gateway[]
+ */
+ private $gateways = [];
+
+ /**
+ * Constructor
+ *
+ * @since WPUF_PRO_SINCE
+ */
+ public function __construct() {
+ $this->init_gateways();
+ $this->setup_hooks();
+ }
+
+ /**
+ * Initialize all payment gateways
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @return void
+ */
+ private function init_gateways() {
+ // Initialize core gateways
+ $this->register_gateway( new Paypal_Gateway() );
+ $this->register_gateway( new Bank_Gateway() );
+
+ /**
+ * Allow third-party plugins to register custom gateways
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param Gateway_Manager $manager Gateway manager instance
+ */
+ do_action( 'wpuf_register_payment_gateways', $this );
+ }
+
+ /**
+ * Setup WordPress hooks
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @return void
+ */
+ private function setup_hooks() {
+ add_filter( 'wpuf_payment_gateways', [ $this, 'get_registered_gateways' ], 5 );
+ }
+
+ /**
+ * Register a payment gateway
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param Payment_Gateway $gateway Gateway instance
+ *
+ * @return bool True if registered successfully, false otherwise
+ */
+ public function register_gateway( Payment_Gateway $gateway ) {
+ $gateway_id = $gateway->get_id();
+
+ if ( empty( $gateway_id ) ) {
+ $this->log_error( 'Attempted to register gateway without ID' );
+ return false;
+ }
+
+ if ( isset( $this->gateways[ $gateway_id ] ) ) {
+ $this->log_error( sprintf( 'Gateway "%s" is already registered', $gateway_id ) );
+ return false;
+ }
+
+ $this->gateways[ $gateway_id ] = $gateway;
+
+ return true;
+ }
+
+ /**
+ * Unregister a payment gateway
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param string $gateway_id Gateway ID
+ *
+ * @return bool True if unregistered successfully, false otherwise
+ */
+ public function unregister_gateway( $gateway_id ) {
+ if ( ! isset( $this->gateways[ $gateway_id ] ) ) {
+ return false;
+ }
+
+ unset( $this->gateways[ $gateway_id ] );
+
+ return true;
+ }
+
+ /**
+ * Get a specific gateway instance
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param string $gateway_id Gateway ID
+ *
+ * @return Payment_Gateway|null Gateway instance or null if not found
+ */
+ public function get_gateway( $gateway_id ) {
+ return isset( $this->gateways[ $gateway_id ] ) ? $this->gateways[ $gateway_id ] : null;
+ }
+
+ /**
+ * Get all registered gateways
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param array $gateways Existing gateways array (for filter compatibility)
+ *
+ * @return array Gateway configurations
+ */
+ public function get_registered_gateways( $gateways = [] ) {
+ $registered = [];
+
+ foreach ( $this->gateways as $gateway_id => $gateway ) {
+ $registered[ $gateway_id ] = $gateway->get_config();
+ }
+
+ // Merge with any gateways added via the old filter system for backward compatibility
+ return array_merge( $gateways, $registered );
+ }
+
+ /**
+ * Get all gateway instances
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @return Payment_Gateway[]
+ */
+ public function get_gateway_instances() {
+ return $this->gateways;
+ }
+
+ /**
+ * Check if a gateway is registered
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param string $gateway_id Gateway ID
+ *
+ * @return bool
+ */
+ public function is_gateway_registered( $gateway_id ) {
+ return isset( $this->gateways[ $gateway_id ] );
+ }
+
+ /**
+ * Get gateway IDs
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @return array
+ */
+ public function get_gateway_ids() {
+ return array_keys( $this->gateways );
+ }
+
+ /**
+ * Log error message
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param string $message Error message
+ *
+ * @return void
+ */
+ private function log_error( $message ) {
+ if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+ error_log( sprintf( '[WPUF Gateway Manager] %s', $message ) );
+ }
+ }
+}
--- a/wp-user-frontend/Lib/Gateway/Manager.php
+++ b/wp-user-frontend/Lib/Gateway/Manager.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace WeDevsWpufLibGateway;
+
+/**
+ * Payment Gateway Manager
+ *
+ * Manages registration and initialization of payment gateways.
+ *
+ * @since WPUF_PRO_SINCE
+ */
+class Manager {
+
+ /**
+ * Registered gateway instances
+ *
+ * @var Abstract_Gateway[]
+ */
+ private $gateways = [];
+
+ /**
+ * Constructor
+ *
+ * @since WPUF_PRO_SINCE
+ */
+ public function __construct() {
+ $this->init_gateways();
+ $this->setup_hooks();
+ }
+
+ /**
+ * Initialize all payment gateways
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @return void
+ */
+ private function init_gateways() {
+ // Initialize core gateways
+ $this->register_gateway( new Paypal_Gateway() );
+ $this->register_gateway( new Bank_Gateway() );
+
+ /**
+ * Allow third-party plugins to register custom gateways
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param _Manager $manager Gateway manager instance
+ */
+ do_action( 'wpuf_register_payment_gateways', $this );
+ }
+
+ /**
+ * Setup WordPress hooks
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @return void
+ */
+ private function setup_hooks() {
+ add_filter( 'wpuf_payment_gateways', [ $this, 'get_registered_gateways' ], 5 );
+ }
+
+ /**
+ * Register a payment gateway
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param Abstract_Gateway $gateway Gateway instance
+ *
+ * @return bool True if registered successfully, false otherwise
+ */
+ public function register_gateway( Abstract_Gateway $gateway ) {
+ $gateway_id = $gateway->get_id();
+
+ if ( empty( $gateway_id ) ) {
+ $this->log_error( 'Attempted to register gateway without ID' );
+ return false;
+ }
+
+ if ( isset( $this->gateways[ $gateway_id ] ) ) {
+ $this->log_error( sprintf( 'Gateway "%s" is already registered', $gateway_id ) );
+ return false;
+ }
+
+ $this->gateways[ $gateway_id ] = $gateway;
+
+ return true;
+ }
+
+ /**
+ * Unregister a payment gateway
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param string $gateway_id Gateway ID
+ *
+ * @return bool True if unregistered successfully, false otherwise
+ */
+ public function unregister_gateway( $gateway_id ) {
+ if ( ! isset( $this->gateways[ $gateway_id ] ) ) {
+ return false;
+ }
+
+ unset( $this->gateways[ $gateway_id ] );
+
+ return true;
+ }
+
+ /**
+ * Get a specific gateway instance
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param string $gateway_id Gateway ID
+ *
+ * @return Abstract_Gateway|null Gateway instance or null if not found
+ */
+ public function get_gateway( $gateway_id ) {
+ return isset( $this->gateways[ $gateway_id ] ) ? $this->gateways[ $gateway_id ] : null;
+ }
+
+ /**
+ * Get all registered gateways
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param array $gateways Existing gateways array (for filter compatibility)
+ *
+ * @return array Gateway configurations
+ */
+ public function get_registered_gateways( $gateways = [] ) {
+ $registered = [];
+
+ foreach ( $this->gateways as $gateway_id => $gateway ) {
+ $registered[ $gateway_id ] = $gateway->get_config();
+ }
+
+ // Merge with any gateways added via the old filter system for backward compatibility
+ return array_merge( $gateways, $registered );
+ }
+
+ /**
+ * Get all gateway instances
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @return Abstract_Gateway[]
+ */
+ public function get_gateway_instances() {
+ return $this->gateways;
+ }
+
+ /**
+ * Check if a gateway is registered
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param string $gateway_id Gateway ID
+ *
+ * @return bool
+ */
+ public function is_gateway_registered( $gateway_id ) {
+ return isset( $this->gateways[ $gateway_id ] );
+ }
+
+ /**
+ * Get gateway IDs
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @return array
+ */
+ public function get_gateway_ids() {
+ return array_keys( $this->gateways );
+ }
+
+ /**
+ * Log error message
+ *
+ * @since WPUF_PRO_SINCE
+ *
+ * @param string $message Error message
+ *
+ * @return void
+ */
+ private function log_error( $message ) {
+ if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+ error_log( sprintf( '[WPUF Gateway Manager] %s', $message ) );
+ }
+ }
+}
--- a/wp-user-frontend/Lib/Gateway/Paypal.php
+++ b/wp-user-frontend/Lib/Gateway/Paypal.php
@@ -3,7 +3,6 @@
namespace WeDevsWpufLibGateway;
use WeDevsWpufFrontendPayment;
-use WeDevsWpufTraitsTaxableTrait;
/**
* WP User Frontend PayPal gateway
@@ -12,7 +11,6 @@
* @updated 4.1.5
*/
class Paypal {
- use TaxableTrait;
private $gateway_url;
private $test_mode;
@@ -33,15 +31,14 @@
$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( 'init', [ $this, 'check_paypal_return' ], 20 );
add_action( 'wpuf_cancel_payment_paypal', [ $this, 'cancel_subscription' ] );
add_action( 'wpuf_cancel_subscription_paypal', [ $this, 'cancel_subscription' ] );
- add_action( 'init', [ $this, 'handle_pending_payment' ] );
add_action( 'wpuf_paypal_webhook', [ $this, 'process_webhook' ] );
- // Add webhook endpoint handler
- add_action( 'init', [ $this, 'register_webhook_endpoint' ] );
add_action( 'template_redirect', [ $this, 'handle_webhook_request' ] );
// Add admin notice for PayPal settings update
@@ -208,7 +205,7 @@
http_response_code( 401 );
echo wp_json_encode(
[
- 'status' => 'error',
+ 'status' => 'error',
'message' => 'Unauthorized',
]
);
@@ -236,7 +233,6 @@
case 'BILLING.SUBSCRIPTION.ACTIVATED':
if ( isset( $event['resource'] ) ) {
- // Handle when a subscription becomes active (after trial)
$this->handle_subscription_activated( $event['resource'] );
}
break;
@@ -291,17 +287,13 @@
}
// Verify payment amount
+ // Payment amount verification is handled via breakdown calculation hook
$payment_amount = number_format( $payment['amount']['value'], 2, '.', '' );
- $expected_amount = number_format( $custom_data['subtotal'], 2, '.', '' );
if ( $payment['amount']['value'] < 0 ) {
throw new Exception( 'Invalid payment amount: negative value' );
}
- if ( $payment_amount !== $expected_amount ) {
- throw new Exception( 'Payment amount mismatch' );
- }
-
// Check if transaction already exists
$existing = $wpdb->get_var(
$wpdb->prepare(
@@ -321,25 +313,99 @@
throw new Exception( 'Invalid user' );
}
- // Calculate tax
- $tax_amount = 0;
- if ( $this->wpuf_tax_enabled() ) {
- $tax_rate = $this->wpuf_current_tax_rate();
- $payment_amount = $payment['amount']['value'];
- // Calculate tax from total amount
- $tax_amount = ( $payment_amount * $tax_rate ) / ( 100 + $tax_rate );
- $subtotal = $payment_amount - $tax_amount;
+ // 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 {
- $subtotal = $payment['amount']['value'];
+ // 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_amount,
- 'cost' => $payment['amount']['value'],
+ '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,
@@ -394,11 +460,6 @@
if ( ! empty( $custom_data['coupon_id'] ) ) {
$this->update_coupon_usage( $custom_data['coupon_id'] );
}
-
- // Verify payment amount
- if ( $payment['amount']['value'] !== number_format( $custom_data['subtotal'], 2, '.', '' ) ) {
- throw new Exception( 'Payment amount mismatch' );
- }
}
@@ -459,7 +520,7 @@
}
// Log the event type
-
+
// Process the webhook
$this->process_webhook( $raw_input );
@@ -509,11 +570,16 @@
'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' => $this->webhook_id,
+ 'webhook_id' => $webhook_id,
'webhook_event' => json_decode( $raw_input, false ),
'transmission_sig' => $headers['paypal-transmission-sig'],
'auth_algo' => $headers['paypal-auth-algo'],
@@ -617,12 +683,6 @@
$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;
- // Get tax rate if enabled
- $tax_rate = 0;
- if ( $this->wpuf_tax_enabled() ) {
- $tax_rate = $this->wpuf_current_tax_rate();
- }
-
// Create subscription data structure with all necessary meta
$subscription_data = [
'pack_id' => $custom_data['item_number'],
@@ -669,6 +729,7 @@
if ( $is_in_trial ) {
$this->create_trial_payment_record( $user_id, $custom_data['item_number'], $subscription_id );
}
+
} catch ( Exception $e ) {
throw $e;
}
@@ -688,8 +749,8 @@
$payment_data = [
'user_id' => $user_id,
'status' => 'completed',
+ 'tax' => 0, // the payment record structure in the database expects a tax field
'subtotal' => 0,
- 'tax' => 0,
'cost' => 0,
'post_id' => 0,
'pack_id' => $pack_id,
@@ -698,6 +759,7 @@
'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' ),
];
@@ -947,16 +1009,118 @@
// 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' );
- // Log subscription creation
}
+ // 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' => $amount,
- 'tax' => 0,
- 'cost' => $amount,
+ 'subtotal' => $subtotal,
+ 'profile_id' => $subscription_id,
+ 'tax' => $tax,
+ 'cost' => $cost,
'post_id' => 0,
'pack_id' => $pack_id,
'payer_first_name' => $user->first_name,
@@ -1138,11 +1302,11 @@
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;
}
@@ -1151,7 +1315,7 @@
*/
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 ) ) {
@@ -1159,12 +1323,12 @@
}
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();
}
@@ -1228,6 +1392,7 @@
}
+
/**
* Prepare and send payment to PayPal
*
@@ -1255,7 +1420,6 @@
$cancel_url = $return_url;
$billing_amount = empty( $data['price'] ) ? 0 : $data['price'];
- $tax_amount = 0;
// Check if pricing fields payment is enabled and update price accordingly
$post_id = isset( $data['item_number'] ) && $data['type'] === 'post' ? $data['item_number'] : 0;
@@ -1273,13 +1437,6 @@
}
}
- // Handle tax if enabled
- if ( $this->wpuf_tax_enabled() ) {
- $tax_rate = $this->wpuf_current_tax_rate();
- $tax_amount = $billing_amount * ( $tax_rate / 100 );
- $billing_amount = $billing_amount + $tax_amount;
- }
-
// 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' ) &&
@@ -1290,9 +1447,59 @@
$coupon_id = '';
}
- $data['subtotal'] = $billing_amount - $tax_amount;
- $data['tax'] = $tax_amount;
- $billing_amount = apply_filters( 'wpuf_payment_amount', $billing_amount, $post_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 ) {
@@ -1352,25 +1559,20 @@
}
}
- // Create a plan if not exists
- $plan_id = $this->get_or_create_plan( $pack, $billing_amount, $period, $interval, $trial_period_days );
+ // 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' );
}
- // Get tax rate if enabled
- $tax_rate = 0;
- if ( $this->wpuf_tax_enabled() ) {
- $tax_rate = $this->wpuf_current_tax_rate();
- }
-
// Prepare subscription data
$subscription_data = [
'plan_id' => $plan_id,
'application_context' => [
'brand_name' => get_bloginfo( 'name' ),
- 'locale' => 'en-US',
'shipping_preference' => 'NO_SHIPPING',
'user_action' => 'SUBSCRIBE_NOW',
'return_url' => $return_url,
@@ -1381,15 +1583,43 @@
'type' => $data['type'],
'user_id' => $user_id,
'item_number' => $data['item_number'],
- 'subtotal' => $data['subtotal'],
- 'tax_rate' => $tax_rate,
- 'tax' => $data['tax'],
'coupon_id' => $coupon_id,
- 'trial_period_days' => $trial_period_days,
]
),
];
+ // 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',
@@ -1407,6 +1637,7 @@
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'] ) ) {
@@ -1417,11 +1648,11 @@
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' ),
+ '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
@@ -1449,31 +1680,26 @@
10,
1
);
-
+
// Redirect to PayPal
wp_safe_redirect( $approval_url );
exit();
} else {
- $payment_data = [
+ // Build PayPal order data
+ $paypal_order_data = [
'intent' => 'CAPTURE',
'purchase_units' => [
[
- 'amount' => [
- 'currency_code' => $data['currency'],
- 'value' => number_format( $billing_amount, 2, '.', '' ),
- ],
+ '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' => $data['type'],
- 'user_id' => $user_id,
- 'coupon_id' => $coupon_id,
- 'subtotal' => $data['subtotal'],
- 'tax' => $data['tax'],
- 'item_number' => $data['item_number'],
- 'first_name' => $data['user_info']['first_name'],
- 'last_name' => $data['user_info']['last_name'],
- 'email' => $data['user_info']['email'],
+ '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,
]
),
],
@@ -1482,14 +1708,27 @@
'return_url' => $return_url,
'cancel_url' => $cancel_url,
'brand_name' => get_bloginfo( 'name' ),
- 'landing_page' => 'LOGIN',
'user_action' => 'PAY_NOW',
'shipping_preference' => 'NO_SHIPPING',
],
];
- // Add debug logging
- // Create order
+ /**
+ * 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',
[
@@ -1497,7 +1736,7 @@
'Authorization' => 'Bearer ' . $access_token,
'Content-Type' => 'application/json',
],
- 'body' => wp_json_encode( $payment_data ),
+ 'body' => wp_json_encode( $paypal_order_data ),
]
);
@@ -1505,6 +1744,7 @@
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'] ) ) {
@@ -1523,7 +1763,7 @@
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',
@@ -1533,7 +1773,7 @@
10,
1
);
-
+
wp_safe_redirect( $approval_url );
exit();
}
@@ -1548,6 +1788,12 @@
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 );
@@ -1573,7 +1819,10 @@
}
}
- // Create new plan
+ // 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,
@@ -1583,7 +1832,7 @@
[
'frequency' => [
'interval_unit' => strtoupper( $period ),
- 'interval_count' => $interval,
+ 'interval_count' => $interval_count,
],
'tenure_type' => 'REGULAR',
'sequence' => 1,
@@ -1633,7 +1882,7 @@
// 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',
[
@@ -1650,7 +1899,9 @@
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' );
@@ -1756,12 +2007,12 @@
$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(
[
@@ -1783,7 +2034,7 @@
wp_safe_redirect( $success_url );
exit;
-
+
} catch ( Exception $e ) {
wp_safe_redirect( $this->get_error_page_url( $e->getMessage() ) );
exit;
@@ -1897,10 +2148,10 @@
}
// 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' &&
+ $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 ) {
@@ -1950,6 +2201,8 @@
*/
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'] ) ) {
@@ -1974,7 +2227,7 @@
// Update subscriber table
global $wpdb;
- $wpdb->update(
+ $result = $wpdb->update(
$wpdb->prefix . 'wpuf_subscribers',
[
'subscribtion_status' => 'cancel',
@@ -2001,41 +2254,64 @@
*/
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 );
}
- if ( ! $custom_data || ! isset( $custom_data['user_id'] ) ) {
+ // 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
- $subscription_id = $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 );
- }
- } else {
- $user_id = $custom_data['user_id'];
+ if ( ! $user_id ) {
+ throw new Exception( 'Could not find user for subscription: ' . $subscription_id );
}
- // Get the subscription pack
- $subscription_id = $subscription['id'];
+ // Get user pack to check status and trial
$user_pack = get_user_meta( $user_id, '_wpuf_subscription_pack', true );
- if ( ! $user_pack || ! isset( $user_pack['pack_id'] ) ) {
- throw new Exception( 'No subscription pack found for user: ' . $user_id );
+ // 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',
+ ];
}
- $pack_id = $user_pack['pack_id'];
-
- // Update subscription status if needed
if ( isset( $user_pack['status'] ) && 'completed' !== $user_pack['status'] ) {
$user_pack['status'] = 'completed';
- update_user_meta( $user_id, '_wpuf_subscription_pack', $user_pack );
- update_user_meta( $user_id, '_wpuf_paypal_subscription_id', $subscription_id );
}
+ // 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
@@ -2066,7 +2342,7 @@
'user_id' => $user_id,
'status' => 'completed',
'subtotal' => $payment['amount']['value'],
- 'tax' => 0, // You may need to calculate tax
+ 'tax' => 0, // the payment record structure in the database expects a tax field
'cost' => $payment['amount']['value'],
'post_id' => 0,
'pack_id' => $pack_id,
@@ -2075,6 +2351,7 @@
'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' ),
];
@@ -2085,6 +2362,137 @@
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