Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fix

Attempt to ensure no incorrect data for subscription orders.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,37 @@ trait WC_Payment_Gateway_WCPay_Subscriptions_Trait {

use WC_Payments_Subscriptions_Utilities;

/**
* Stores the payment method meta table name
*
* @var string
*/
private static $payment_method_meta_table = 'wc_order_tokens';

/**
* Stores the payment method meta key name
*
* @var string
*/
private static $payment_method_meta_key = 'token';

/**
* Stores a flag to indicate if the subscription integration hooks have been attached.
*
* The callbacks attached as part of maybe_init_subscriptions() only need to be attached once to avoid duplication.
*
* @var bool False by default, true once the callbacks have been attached.
*/
private static $has_attached_integration_hooks = false;

/**
* Used to temporary keep the state of the order_pay value on the Pay for order page with the SCA authorization flow.
* For more details, see remove_order_pay_var and restore_order_pay_var hooks.
*
* @var string|int
*/
private $order_pay_var;

/**
* Retrieve payment token from a subscription or order.
*
Expand Down Expand Up @@ -81,37 +112,6 @@ abstract protected function get_user_formatted_tokens_array( $user_id );
*/
abstract protected function prepare_payment_information( $order );

/**
* Stores the payment method meta table name
*
* @var string
*/
private static $payment_method_meta_table = 'wc_order_tokens';

/**
* Stores the payment method meta key name
*
* @var string
*/
private static $payment_method_meta_key = 'token';

/**
* Stores a flag to indicate if the subscription integration hooks have been attached.
*
* The callbacks attached as part of maybe_init_subscriptions() only need to be attached once to avoid duplication.
*
* @var bool False by default, true once the callbacks have been attached.
*/
private static $has_attached_integration_hooks = false;

/**
* Used to temporary keep the state of the order_pay value on the Pay for order page with the SCA authorization flow.
* For more details, see remove_order_pay_var and restore_order_pay_var hooks.
*
* @var string|int
*/
private $order_pay_var;

/**
* Initialize subscription support and hooks.
*/
Expand Down Expand Up @@ -226,6 +226,9 @@ public function maybe_init_subscriptions_hooks() {
// Update subscriptions token when user sets a default payment method.
add_filter( 'woocommerce_subscriptions_update_subscription_token', [ $this, 'update_subscription_token' ], 10, 3 );
add_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', [ $this, 'update_payment_method_for_subscriptions' ], 10, 3 );

// Exclude WooPayments meta keys from being copied from parent orders to subscriptions.
add_filter( 'wcs_copy_payment_meta_to_order', [ $this, 'exclude_wcpay_meta_from_subscription_copy' ], 10, 3 );
}

/**
Expand Down Expand Up @@ -323,27 +326,6 @@ public function change_no_available_methods_message() {
return wpautop( __( "Almost there!\n\nYour order has already been created, the only thing that still needs to be done is for you to authorize the payment with your bank.", 'woocommerce-payments' ) );
}

/**
* Prepares the payment information object.
*
* @param Payment_Information $payment_information The payment information from parent gateway.
* @param int $order_id The order ID whose payment will be processed.
* @return Payment_Information An object, which describes the payment.
*/
protected function maybe_prepare_subscription_payment_information( $payment_information, $order_id ) {
if ( ! $this->is_payment_recurring( $order_id ) ) {
return $payment_information;
}

// Subs-specific behavior starts here.
$payment_information->set_payment_type( Payment_Type::RECURRING() );
// The payment method is always saved for subscriptions.
$payment_information->must_save_payment_method_to_store();
$payment_information->set_is_changing_payment_method_for_subscription( $this->is_changing_payment_method_for_subscription() );

return $payment_information;
}

/**
* Process a scheduled subscription payment.
*
Expand Down Expand Up @@ -421,25 +403,6 @@ public function update_failing_payment_method( $subscription, $renewal_order ) {
$this->add_token_to_order( $subscription, $renewal_token );
}

/**
* Return the payment meta data for this payment gateway.
*
* @param WC_Subscription $subscription The subscription order.
* @return array
*/
private function get_payment_meta( $subscription ) {
$active_token = $this->get_payment_token( $subscription );

return [
self::$payment_method_meta_table => [
self::$payment_method_meta_key => [
'label' => __( 'Saved payment method', 'woocommerce-payments' ),
'value' => empty( $active_token ) ? '' : (string) $active_token->get_id(),
],
],
];
}

/**
* Append payment meta if order and subscription are using WCPay as payment method and if passed payment meta is an array.
*
Expand Down Expand Up @@ -828,7 +791,9 @@ public function maybe_schedule_subscription_order_tracking( $order_id, $order =
* @return string
*/
public function update_renewal_meta_data( $order_meta_query, $to_order, $from_order ) {
$order_meta_query .= " AND `meta_key` NOT IN ('_new_order_tracking_complete')";
$excluded_meta_keys = $this->get_excluded_meta_keys_for_subscription_copying();
$excluded_meta_keys_string = "'" . implode( "', '", $excluded_meta_keys ) . "'";
$order_meta_query .= " AND `meta_key` NOT IN ({$excluded_meta_keys_string})";

return $order_meta_query;
}
Expand All @@ -841,7 +806,12 @@ public function update_renewal_meta_data( $order_meta_query, $to_order, $from_or
* @return array The renewal order data with the data we don't want copied removed
*/
public function remove_data_renewal_order( $order_data ) {
unset( $order_data['_new_order_tracking_complete'] );
$excluded_meta_keys = $this->get_excluded_meta_keys_for_subscription_copying();

foreach ( $excluded_meta_keys as $meta_key ) {
unset( $order_data[ $meta_key ] );
}

return $order_data;
}

Expand Down Expand Up @@ -903,29 +873,6 @@ public function update_subscription_token( $updated, $subscription, $new_token )
return true;
}

/**
* Checks if a renewal order is linked to a WCPay subscription.
*
* @param WC_Order $renewal_order The renewal order to check.
*
* @return bool True if the renewal order is linked to a renewal order. Otherwise false.
*/
private function is_wcpay_subscription_renewal_order( WC_Order $renewal_order ) {
// Renewal orders copy metadata from the parent subscription, so we can first check if it has the `_wcpay_subscription_id` meta.
if ( ! class_exists( 'WC_Payments_Subscription_Service' ) || ! $renewal_order->meta_exists( WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY ) ) {
return false;
}

// Confirm the renewal order is linked to a subscription which is a WCPay Subscription.
foreach ( wcs_get_subscriptions_for_renewal_order( $renewal_order ) as $subscription ) {
if ( WC_Payments_Subscription_Service::is_wcpay_subscription( $subscription ) ) {
return true;
}
}

return false;
}

/**
* Get card mandate parameters for the order payment intent if needed.
* Only required for subscriptions creation for cards issued in India.
Expand Down Expand Up @@ -1051,4 +998,154 @@ public function get_mandate_param_for_renewal_order( WC_Order $renewal_order ):

return $mandate;
}

/**
* Exclude WooPayments meta keys from being copied from parent orders to subscriptions.
*
* This filter is applied when WooCommerce Subscriptions copies payment meta from parent orders
* to subscriptions, preventing stale intent data and other WooPayments-specific metadata
* from being copied.
*
* @param array $payment_meta Associative array of meta data required for automatic payments.
* @param WC_Order $order The subscription's related order.
* @param WC_Subscription $subscription The subscription order.
* @return array
*/
public function exclude_wcpay_meta_from_subscription_copy( $payment_meta, $order, $subscription ) {
if ( $this->id !== $order->get_payment_method() || $this->id !== $subscription->get_payment_method() ) {
return $payment_meta;
}

if ( ! is_array( $payment_meta ) ) {
return $payment_meta;
}

$excluded_meta_keys = $this->get_excluded_meta_keys_for_subscription_copying();

// Remove excluded meta keys from the payment meta array.
foreach ( $excluded_meta_keys as $meta_key ) {
unset( $payment_meta[ $meta_key ] );
}

return $payment_meta;
}

/**
* Prepares the payment information object.
*
* @param Payment_Information $payment_information The payment information from parent gateway.
* @param int $order_id The order ID whose payment will be processed.
* @return Payment_Information An object, which describes the payment.
*/
protected function maybe_prepare_subscription_payment_information( $payment_information, $order_id ) {
if ( ! $this->is_payment_recurring( $order_id ) ) {
return $payment_information;
}

// Subs-specific behavior starts here.
$payment_information->set_payment_type( Payment_Type::RECURRING() );
// The payment method is always saved for subscriptions.
$payment_information->must_save_payment_method_to_store();
$payment_information->set_is_changing_payment_method_for_subscription( $this->is_changing_payment_method_for_subscription() );

return $payment_information;
}

/**
* Return the payment meta data for this payment gateway.
*
* @param WC_Subscription $subscription The subscription order.
* @return array
*/
private function get_payment_meta( $subscription ) {
$active_token = $this->get_payment_token( $subscription );

return [
self::$payment_method_meta_table => [
self::$payment_method_meta_key => [
'label' => __( 'Saved payment method', 'woocommerce-payments' ),
'value' => empty( $active_token ) ? '' : (string) $active_token->get_id(),
],
],
];
}

/**
* Checks if a renewal order is linked to a WCPay subscription.
*
* @param WC_Order $renewal_order The renewal order to check.
*
* @return bool True if the renewal order is linked to a renewal order. Otherwise false.
*/
private function is_wcpay_subscription_renewal_order( WC_Order $renewal_order ) {
// Renewal orders copy metadata from the parent subscription, so we can first check if it has the `_wcpay_subscription_id` meta.
if ( ! class_exists( 'WC_Payments_Subscription_Service' ) || ! $renewal_order->meta_exists( WC_Payments_Subscription_Service::SUBSCRIPTION_ID_META_KEY ) ) {
return false;
}

// Confirm the renewal order is linked to a subscription which is a WCPay Subscription.
foreach ( wcs_get_subscriptions_for_renewal_order( $renewal_order ) as $subscription ) {
if ( WC_Payments_Subscription_Service::is_wcpay_subscription( $subscription ) ) {
return true;
}
}

return false;
}

/**
* Get the list of WooPayments meta keys that should be excluded from subscription data copying.
*
* This prevents stale intent data and other WooPayments-specific metadata from being copied
* from parent orders to subscriptions, which can cause issues with 3DS failures and re-attempts.
*
* @return array Array of meta keys to exclude from copying.
*/
private function get_excluded_meta_keys_for_subscription_copying() {
return [
// Order tracking and completion.
'_new_order_tracking_complete',

// Intent-related meta keys (prevent stale intent data).
'_intent_id',
'_intention_status',
'_wcpay_intent_currency',

// Payment method and charge data (prevent stale payment data).
'_payment_method_id',
'_charge_id',
'_charge_risk_level',
'_wcpay_payment_method_details',
'_wcpay_payment_transaction_id',

// Customer and fraud data (should be fresh for each order).
'_stripe_customer_id',
'_wcpay_fraud_meta_box_type',
'_wcpay_fraud_outcome_status',

// Refund data (not relevant for subscriptions).
'_wcpay_refund_id',
'_wcpay_refund_transaction_id',
'_wcpay_refund_status',

// Transaction fees (calculated per transaction).
'_wcpay_transaction_fee',

// Mode and environment data.
'_wcpay_mode',

// Multibanco-specific data (payment method specific).
'_wcpay_multibanco_entity',
'_wcpay_multibanco_reference',
'_wcpay_multibanco_expiry',
'_wcpay_multibanco_url',

// Mandate data (should be fresh for each subscription).
'_stripe_mandate_id',

// Multi-currency data (should be calculated fresh).
'_wcpay_multi_currency_order_exchange_rate',
'_wcpay_multi_currency_order_default_currency',
];
}
}
Loading
Loading