Merge pull request #3046 from woocommerce/PCP-4110-incorrect-subscription-cancellation-handling-with-pay-pal-subscriptions

incorrect subscription cancellation handling with pay pal subscriptions (4110)
This commit is contained in:
Emili Castells 2025-03-24 11:32:01 +01:00 committed by GitHub
commit 3042e3d6b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 344 additions and 350 deletions

View file

@ -2094,6 +2094,16 @@ function wcs_order_contains_product($order, $product)
*/ */
function wc_get_page_screen_id( $for ) {} function wc_get_page_screen_id( $for ) {}
/**
* Checks if manual renewals are enabled.
*
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v4.0.0
* @return bool Whether manual renewal is enabled.
*/
function wcs_is_manual_renewal_enabled()
{
}
/** /**
* Subscription Product Variation Class * Subscription Product Variation Class
* *

View file

@ -180,11 +180,22 @@ class ApplePayGateway extends WC_Payment_Gateway {
); );
} }
do_action( 'woocommerce_paypal_payments_before_process_order', $wc_order ); do_action_deprecated( 'woocommerce_paypal_payments_before_process_order', array( $wc_order ), '3.0.1', 'woocommerce_paypal_payments_before_order_process', __( 'Usage of this action is deprecated. Please use the filter woocommerce_paypal_payments_before_order_process instead.', 'woocommerce-paypal-payments' ) );
try { try {
try { try {
$this->order_processor->process( $wc_order ); /**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process( $wc_order );
}
do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order ); do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order );

View file

@ -254,7 +254,18 @@ class AxoGateway extends WC_Payment_Gateway {
$order = $this->create_paypal_order( $wc_order, $token ); $order = $this->create_paypal_order( $wc_order, $token );
$this->order_processor->process_captured_and_authorized( $wc_order, $order ); /**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
}
} catch ( Exception $exception ) { } catch ( Exception $exception ) {
return $this->handle_payment_failure( $wc_order, $exception ); return $this->handle_payment_failure( $wc_order, $exception );
} }

View file

@ -189,11 +189,22 @@ class GooglePayGateway extends WC_Payment_Gateway {
} }
//phpcs:enable WordPress.Security.NonceVerification.Recommended //phpcs:enable WordPress.Security.NonceVerification.Recommended
do_action( 'woocommerce_paypal_payments_before_process_order', $wc_order ); do_action_deprecated( 'woocommerce_paypal_payments_before_process_order', array( $wc_order ), '3.0.1', 'woocommerce_paypal_payments_before_order_process', __( 'Usage of this action is deprecated. Please use the filter woocommerce_paypal_payments_before_order_process instead.', 'woocommerce-paypal-payments' ) );
try { try {
try { try {
$this->order_processor->process( $wc_order ); /**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process( $wc_order );
}
do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order ); do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order );

View file

@ -9,7 +9,7 @@ document.addEventListener( 'DOMContentLoaded', () => {
const variableId = children[ i ] const variableId = children[ i ]
.querySelector( 'h3' ) .querySelector( 'h3' )
.getElementsByClassName( 'variable_post_id' )[ 0 ].value; .getElementsByClassName( 'variable_post_id' )[ 0 ].value;
if ( parseInt( variableId ) === productId ) { if ( variableId === productId ) {
children[ i ] children[ i ]
.querySelector( '.woocommerce_variable_attributes' ) .querySelector( '.woocommerce_variable_attributes' )
.getElementsByClassName( .getElementsByClassName(
@ -122,21 +122,26 @@ document.addEventListener( 'DOMContentLoaded', () => {
jQuery( '.wc_input_subscription_price' ).trigger( 'change' ); jQuery( '.wc_input_subscription_price' ).trigger( 'change' );
PayPalCommerceGatewayPayPalSubscriptionProducts?.forEach( let variationProductIds = [ PayPalCommerceGatewayPayPalSubscriptionProducts.product_id ];
( product ) => { const variationsInput = document.querySelectorAll( '.variable_post_id' );
if ( product.product_connected === 'yes' ) { for ( let i = 0; i < variationsInput.length; i++ ) {
disableFields( product.product_id ); variationProductIds.push( variationsInput[ i ].value );
} }
variationProductIds?.forEach(
( productId ) => {
const linkBtn = document.getElementById( const linkBtn = document.getElementById(
`ppcp_enable_subscription_product-${ product.product_id }` `ppcp_enable_subscription_product-${ productId }`
); );
if ( linkBtn.checked && linkBtn.value === 'yes' ) {
disableFields( productId );
}
linkBtn?.addEventListener( 'click', ( event ) => { linkBtn?.addEventListener( 'click', ( event ) => {
const unlinkBtnP = document.getElementById( const unlinkBtnP = document.getElementById(
`ppcp-enable-subscription-${ product.product_id }` `ppcp-enable-subscription-${ productId }`
); );
const titleP = document.getElementById( const titleP = document.getElementById(
`ppcp_subscription_plan_name_p-${ product.product_id }` `ppcp_subscription_plan_name_p-${ productId }`
); );
if (event.target.checked === true) { if (event.target.checked === true) {
if ( unlinkBtnP ) { if ( unlinkBtnP ) {
@ -156,26 +161,26 @@ document.addEventListener( 'DOMContentLoaded', () => {
}); });
const unlinkBtn = document.getElementById( const unlinkBtn = document.getElementById(
`ppcp-unlink-sub-plan-${ product.product_id }` `ppcp-unlink-sub-plan-${ productId }`
); );
unlinkBtn?.addEventListener( 'click', ( event ) => { unlinkBtn?.addEventListener( 'click', ( event ) => {
event.preventDefault(); event.preventDefault();
unlinkBtn.disabled = true; unlinkBtn.disabled = true;
const spinner = document.getElementById( const spinner = document.getElementById(
'spinner-unlink-plan' `spinner-unlink-plan-${ productId }`
); );
spinner.style.display = 'inline-block'; spinner.style.display = 'inline-block';
fetch( product.ajax.deactivate_plan.endpoint, { fetch( PayPalCommerceGatewayPayPalSubscriptionProducts.ajax.deactivate_plan.endpoint, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
credentials: 'same-origin', credentials: 'same-origin',
body: JSON.stringify( { body: JSON.stringify( {
nonce: product.ajax.deactivate_plan.nonce, nonce: PayPalCommerceGatewayPayPalSubscriptionProducts.ajax.deactivate_plan.nonce,
plan_id: product.plan_id, plan_id: linkBtn.dataset.subsPlan,
product_id: product.product_id, product_id: productId,
} ), } ),
} ) } )
.then( function ( res ) { .then( function ( res ) {

View file

@ -12,10 +12,7 @@ namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use ActionScheduler_Store; use ActionScheduler_Store;
use WC_Order; use WC_Order;
use WC_Product; use WC_Product;
use WC_Product_Subscription;
use WC_Product_Subscription_Variation; use WC_Product_Subscription_Variation;
use WC_Product_Variable;
use WC_Product_Variable_Subscription;
use WC_Subscription; use WC_Subscription;
use WC_Subscriptions_Product; use WC_Subscriptions_Product;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
@ -28,6 +25,7 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameI
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule; use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WP_Post; use WP_Post;
@ -56,6 +54,146 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* {@inheritDoc} * {@inheritDoc}
*/ */
public function run( ContainerInterface $c ): bool { public function run( ContainerInterface $c ): bool {
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if ( ! $subscriptions_helper->plugin_is_active() ) {
return true;
}
add_filter(
'woocommerce_available_payment_gateways',
function ( array $gateways ) use ( $c ) {
if ( is_account_page() || is_admin() || ! WC()->cart || WC()->cart->is_empty() || wcs_is_manual_renewal_enabled() ) {
return $gateways;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$subscriptions_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( $subscriptions_mode !== 'subscriptions_api' ) {
return $gateways;
}
$pp_subscriptions_product = false;
foreach ( WC()->cart->get_cart() as $cart_item ) {
$cart_product = wc_get_product( $cart_item['product_id'] );
if ( isset( $cart_item['subscription_renewal']['subscription_id'] ) ) {
$subscription_renewal = wcs_get_subscription( $cart_item['subscription_renewal']['subscription_id'] );
if ( $subscription_renewal && $subscription_renewal->get_meta( 'ppcp_subscription' ) ) {
$pp_subscriptions_product = true;
break;
}
} elseif ( $cart_product instanceof \WC_Product_Subscription || $cart_product instanceof \WC_Product_Variable_Subscription ) {
if ( $cart_product->get_meta( '_ppcp_enable_subscription_product' ) === 'yes' ) {
$pp_subscriptions_product = true;
break;
}
}
}
if ( $pp_subscriptions_product ) {
foreach ( $gateways as $id => $gateway ) {
if ( $gateway->id !== PayPalGateway::ID ) {
unset( $gateways[ $id ] );
}
}
return $gateways;
}
return $gateways;
}
);
add_filter(
'woocommerce_subscription_payment_gateway_supports',
function ( bool $payment_gateway_supports, string $payment_gateway_feature, \WC_Subscription $wc_order ): bool {
if ( ! in_array( $payment_gateway_feature, array( 'gateway_scheduled_payments', 'subscription_date_changes', 'subscription_amount_changes', 'subscription_payment_method_change', 'subscription_payment_method_change_customer', 'subscription_payment_method_change_admin' ), true ) ) {
return $payment_gateway_supports;
}
$subscription = wcs_get_subscription( $wc_order->get_id() );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return $payment_gateway_supports;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_id ) {
return $payment_gateway_supports;
}
if ( $payment_gateway_feature === 'gateway_scheduled_payments' ) {
return true;
}
return false;
},
100,
3
);
add_filter(
'woocommerce_can_subscription_be_updated_to_active',
function ( bool $can_be_updated, \WC_Subscription $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id && $subscription->get_status() === 'pending-cancel' ) {
return true;
}
return $can_be_updated;
},
10,
2
);
add_filter(
'woocommerce_can_subscription_be_updated_to_new-payment-method',
function ( bool $can_be_updated, \WC_Subscription $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id ) {
return false;
}
return $can_be_updated;
},
10,
2
);
add_filter(
'woocommerce_paypal_payments_before_order_process',
function ( bool $process, \WC_Payment_Gateway $gateway, \WC_Order $wc_order ) use ( $c ) {
if ( ! $gateway instanceof PayPalGateway || $gateway::ID !== 'ppcp-gateway' ) {
return $process;
}
$paypal_subscription_id = \WC()->session->get( 'ppcp_subscription_id' );
if ( empty( $paypal_subscription_id ) || ! is_string( $paypal_subscription_id ) ) {
return $process;
}
$order = $c->get( 'session.handler' )->order();
$gateway->add_paypal_meta( $wc_order, $order, $c->get( 'settings.environment' ) );
$subscriptions = function_exists( 'wcs_get_subscriptions_for_order' ) ? wcs_get_subscriptions_for_order( $wc_order ) : array();
foreach ( $subscriptions as $subscription ) {
$subscription->update_meta_data( 'ppcp_subscription', $paypal_subscription_id );
$subscription->save();
// translators: %s PayPal Subscription id.
$subscription->add_order_note( sprintf( __( 'PayPal subscription %s added.', 'woocommerce-paypal-payments' ), $paypal_subscription_id ) );
}
$transaction_id = $gateway->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$gateway->update_transaction_id( $transaction_id, $wc_order, $c->get( 'woocommerce.logger.woocommerce' ) );
}
$wc_order->payment_complete();
return false;
},
10,
3
);
add_action( add_action(
'save_post', 'save_post',
/** /**
@ -64,12 +202,6 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* @psalm-suppress MissingClosureParamType * @psalm-suppress MissingClosureParamType
*/ */
function( $product_id ) use ( $c ) { function( $product_id ) use ( $c ) {
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if ( ! $subscriptions_helper->plugin_is_active() ) {
return;
}
$settings = $c->get( 'wcgateway.settings' ); $settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings ); assert( $settings instanceof Settings );
@ -82,6 +214,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
$nonce = wc_clean( wp_unslash( $_POST['_wcsnonce'] ?? '' ) ); $nonce = wc_clean( wp_unslash( $_POST['_wcsnonce'] ?? '' ) );
if ( if (
$subscriptions_mode !== 'subscriptions_api' $subscriptions_mode !== 'subscriptions_api'
|| wcs_is_manual_renewal_enabled()
|| ! is_string( $nonce ) || ! is_string( $nonce )
|| ! wp_verify_nonce( $nonce, 'wcs_subscription_meta' ) ) { || ! wp_verify_nonce( $nonce, 'wcs_subscription_meta' ) ) {
return; return;
@ -107,7 +240,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* @psalm-suppress MissingClosureParamType * @psalm-suppress MissingClosureParamType
*/ */
static function ( $passed_validation, $product_id ) use ( $c ) { static function ( $passed_validation, $product_id ) use ( $c ) {
if ( WC()->cart->is_empty() ) { if ( WC()->cart->is_empty() || wcs_is_manual_renewal_enabled() ) {
return $passed_validation; return $passed_validation;
} }
@ -163,12 +296,9 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
function( $variation_id ) use ( $c ) { function( $variation_id ) use ( $c ) {
$wcsnonce_save_variations = wc_clean( wp_unslash( $_POST['_wcsnonce_save_variations'] ?? '' ) ); $wcsnonce_save_variations = wc_clean( wp_unslash( $_POST['_wcsnonce_save_variations'] ?? '' ) );
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if ( if (
! $subscriptions_helper->plugin_is_active() ! WC_Subscriptions_Product::is_subscription( $variation_id )
|| ! WC_Subscriptions_Product::is_subscription( $variation_id ) || wcs_is_manual_renewal_enabled()
|| ! is_string( $wcsnonce_save_variations ) || ! is_string( $wcsnonce_save_variations )
|| ! wp_verify_nonce( $wcsnonce_save_variations, 'wcs_subscription_variations' ) || ! wp_verify_nonce( $wcsnonce_save_variations, 'wcs_subscription_variations' )
) { ) {
@ -234,127 +364,6 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
} }
); );
add_filter(
'woocommerce_order_actions',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $actions, $subscription = null ): array {
if ( ! is_array( $actions ) || ! is_a( $subscription, WC_Subscription::class ) ) {
return $actions;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id && isset( $actions['wcs_process_renewal'] ) ) {
unset( $actions['wcs_process_renewal'] );
}
return $actions;
},
20,
2
);
add_filter(
'wcs_view_subscription_actions',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $actions, $subscription ): array {
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return $actions;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id && $subscription->get_status() === 'active' ) {
$url = wp_nonce_url(
add_query_arg(
array(
'change_subscription_to' => 'cancelled',
'ppcp_cancel_subscription' => $subscription->get_id(),
)
),
'ppcp_cancel_subscription_nonce'
);
array_unshift(
$actions,
array(
'url' => esc_url( $url ),
'name' => esc_html__( 'Cancel', 'woocommerce-paypal-payments' ),
)
);
$actions['cancel']['name'] = esc_html__( 'Suspend', 'woocommerce-paypal-payments' );
unset( $actions['subscription_renewal_early'] );
}
return $actions;
},
11,
2
);
add_action(
'wp_loaded',
function() use ( $c ) {
if ( ! function_exists( 'wcs_get_subscription' ) ) {
return;
}
$cancel_subscription_id = wc_clean( wp_unslash( $_GET['ppcp_cancel_subscription'] ?? '' ) );
$subscription = wcs_get_subscription( absint( $cancel_subscription_id ) );
if ( ! wcs_is_subscription( $subscription ) || $subscription === false ) {
return;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
$nonce = wc_clean( wp_unslash( $_GET['_wpnonce'] ?? '' ) );
if ( ! is_string( $nonce ) ) {
return;
}
if (
$subscription_id
&& $cancel_subscription_id
&& $nonce
) {
if (
! wp_verify_nonce( $nonce, 'ppcp_cancel_subscription_nonce' )
|| ! user_can( get_current_user_id(), 'edit_shop_subscription_status', $subscription->get_id() )
) {
return;
}
$subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' );
$subscription_id = $subscription->get_meta( 'ppcp_subscription' );
try {
$subscriptions_endpoint->cancel( $subscription_id );
$subscription->update_status( 'cancelled' );
$subscription->add_order_note( __( 'Subscription cancelled by the subscriber from their account page.', 'woocommerce-paypal-payments' ) );
wc_add_notice( __( 'Your subscription has been cancelled.', 'woocommerce-paypal-payments' ) );
wp_safe_redirect( $subscription->get_view_order_url() );
exit;
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not cancel subscription product on PayPal. ' . $error );
}
}
},
100
);
add_action( add_action(
'woocommerce_subscription_before_actions', 'woocommerce_subscription_before_actions',
/** /**
@ -459,6 +468,9 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
add_action( add_action(
'woocommerce_product_options_general_product_data', 'woocommerce_product_options_general_product_data',
function() use ( $c ) { function() use ( $c ) {
if ( wcs_is_manual_renewal_enabled() ) {
return;
}
$settings = $c->get( 'wcgateway.settings' ); $settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings ); assert( $settings instanceof Settings );
@ -496,6 +508,9 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* @psalm-suppress MissingClosureParamType * @psalm-suppress MissingClosureParamType
*/ */
function( $loop, $variation_data, $variation ) use ( $c ) { function( $loop, $variation_data, $variation ) use ( $c ) {
if ( wcs_is_manual_renewal_enabled() ) {
return;
}
$settings = $c->get( 'wcgateway.settings' ); $settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings ); assert( $settings instanceof Settings );
@ -527,34 +542,12 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* @psalm-suppress MissingClosureParamType * @psalm-suppress MissingClosureParamType
*/ */
function( $hook ) use ( $c ) { function( $hook ) use ( $c ) {
if ( ! is_string( $hook ) ) { if ( ! is_string( $hook ) || wcs_is_manual_renewal_enabled() ) {
return; return;
} }
$settings = $c->get( 'wcgateway.settings' ); $settings = $c->get( 'wcgateway.settings' );
$subscription_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : ''; $subscription_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( $hook !== 'post.php' || $subscription_mode !== 'subscriptions_api' ) { if ( $hook !== 'post.php' && $hook !== 'post-new.php' && $subscription_mode !== 'subscriptions_api' ) {
return;
}
//phpcs:disable WordPress.Security.NonceVerification.Recommended
$post_id = wc_clean( wp_unslash( $_GET['post'] ?? '' ) );
$product = wc_get_product( $post_id );
if ( ! ( is_a( $product, WC_Product::class ) ) ) {
return;
}
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if (
! $subscriptions_helper->plugin_is_active()
|| ! (
is_a( $product, WC_Product_Subscription::class )
|| is_a( $product, WC_Product_Variable_Subscription::class )
|| is_a( $product, WC_Product_Subscription_Variation::class )
)
|| ! WC_Subscriptions_Product::is_subscription( $product )
) {
return; return;
} }
@ -562,7 +555,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
wp_enqueue_script( wp_enqueue_script(
'ppcp-paypal-subscription', 'ppcp-paypal-subscription',
untrailingslashit( $module_url ) . '/assets/js/paypal-subscription.js', untrailingslashit( $module_url ) . '/assets/js/paypal-subscription.js',
array( 'jquery', 'wc-admin-product-editor' ), array( 'jquery' ),
$c->get( 'ppcp.asset-version' ), $c->get( 'ppcp.asset-version' ),
true true
); );
@ -572,34 +565,23 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
); );
$products = array( $this->set_product_config( $product ) ); $product = wc_get_product();
if ( $product->get_type() === 'variable-subscription' ) { if ( ! $product ) {
$products = array(); return;
/**
* Suppress pslam.
*
* @psalm-suppress TypeDoesNotContainType
*
* WC_Product_Variable_Subscription extends WC_Product_Variable.
*/
assert( $product instanceof WC_Product_Variable );
$available_variations = $product->get_available_variations();
foreach ( $available_variations as $variation ) {
/**
* The method is defined in WooCommerce.
*
* @psalm-suppress UndefinedMethod
*/
$variation = wc_get_product_object( 'variation', $variation['variation_id'] );
$products[] = $this->set_product_config( $variation );
}
} }
wp_localize_script( wp_localize_script(
'ppcp-paypal-subscription', 'ppcp-paypal-subscription',
'PayPalCommerceGatewayPayPalSubscriptionProducts', 'PayPalCommerceGatewayPayPalSubscriptionProducts',
$products array(
'ajax' => array(
'deactivate_plan' => array(
'endpoint' => \WC_AJAX::get_endpoint( DeactivatePlanEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DeactivatePlanEndpoint::ENDPOINT ),
),
),
'product_id' => $product->get_id(),
)
); );
} }
); );
@ -747,29 +729,6 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
} }
} }
/**
* Returns subscription product configuration.
*
* @param WC_Product $product The product.
* @return array
*/
private function set_product_config( WC_Product $product ): array {
$plan = $product->get_meta( 'ppcp_subscription_plan' ) ?? array();
$plan_id = $plan['id'] ?? '';
return array(
'product_connected' => $product->get_meta( '_ppcp_enable_subscription_product' ) ?? '',
'plan_id' => $plan_id,
'product_id' => $product->get_id(),
'ajax' => array(
'deactivate_plan' => array(
'endpoint' => \WC_AJAX::get_endpoint( DeactivatePlanEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DeactivatePlanEndpoint::ENDPOINT ),
),
),
);
}
/** /**
* Render PayPal Subscriptions fields. * Render PayPal Subscriptions fields.
* *
@ -780,15 +739,19 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
private function render_paypal_subscription_fields( WC_Product $product, Environment $environment ): void { private function render_paypal_subscription_fields( WC_Product $product, Environment $environment ): void {
$enable_subscription_product = $product->get_meta( '_ppcp_enable_subscription_product' ); $enable_subscription_product = $product->get_meta( '_ppcp_enable_subscription_product' );
$style = $product->get_type() === 'subscription_variation' ? 'float:left; width:150px;' : ''; $style = $product->get_type() === 'subscription_variation' ? 'float:left; width:150px;' : '';
$subscription_product = $product->get_meta( 'ppcp_subscription_product' );
$subscription_plan = $product->get_meta( 'ppcp_subscription_plan' );
$subscription_plan_name = $product->get_meta( '_ppcp_subscription_plan_name' );
echo '<p class="form-field">'; echo '<p class="form-field">';
echo sprintf( echo sprintf(
// translators: %1$s and %2$s are label open and close tags. // translators: %1$s and %2$s are label open and close tags.
esc_html__( '%1$sConnect to PayPal%2$s', 'woocommerce-paypal-payments' ), esc_html__( '%1$sConnect to PayPal%2$s', 'woocommerce-paypal-payments' ),
'<label for="_ppcp_enable_subscription_product-' . esc_attr( (string) $product->get_id() ) . '" style="' . esc_attr( $style ) . '">', '<label for="ppcp_enable_subscription_product-' . esc_attr( (string) $product->get_id() ) . '" style="' . esc_attr( $style ) . '">',
'</label>' '</label>'
); );
echo '<input type="checkbox" id="ppcp_enable_subscription_product-' . esc_attr( (string) $product->get_id() ) . '" name="_ppcp_enable_subscription_product" value="yes" ' . checked( $enable_subscription_product, 'yes', false ) . '/>'; $plan_id = isset( $subscription_plan['id'] ) ?? '';
echo '<input type="checkbox" id="ppcp_enable_subscription_product-' . esc_attr( (string) $product->get_id() ) . '" data-subs-plan="' . esc_attr( (string) $plan_id ) . '" name="_ppcp_enable_subscription_product" value="yes" ' . checked( $enable_subscription_product, 'yes', false ) . '/>';
echo sprintf( echo sprintf(
// translators: %1$s and %2$s are label open and close tags. // translators: %1$s and %2$s are label open and close tags.
esc_html__( '%1$sConnect Product to PayPal Subscriptions Plan%2$s', 'woocommerce-paypal-payments' ), esc_html__( '%1$sConnect Product to PayPal Subscriptions Plan%2$s', 'woocommerce-paypal-payments' ),
@ -799,9 +762,6 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
echo wc_help_tip( esc_html__( 'Create a subscription product and plan to bill customers at regular intervals. Be aware that certain subscription settings cannot be modified once the PayPal Subscription is linked to this product. Unlink the product to edit disabled fields.', 'woocommerce-paypal-payments' ) ); echo wc_help_tip( esc_html__( 'Create a subscription product and plan to bill customers at regular intervals. Be aware that certain subscription settings cannot be modified once the PayPal Subscription is linked to this product. Unlink the product to edit disabled fields.', 'woocommerce-paypal-payments' ) );
echo '</p>'; echo '</p>';
$subscription_product = $product->get_meta( 'ppcp_subscription_product' );
$subscription_plan = $product->get_meta( 'ppcp_subscription_plan' );
$subscription_plan_name = $product->get_meta( '_ppcp_subscription_plan_name' );
if ( $subscription_product || $subscription_plan ) { if ( $subscription_product || $subscription_plan ) {
$display_unlink_p = 'display:none;'; $display_unlink_p = 'display:none;';
if ( $enable_subscription_product !== 'yes' ) { if ( $enable_subscription_product !== 'yes' ) {
@ -811,7 +771,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
// translators: %1$s and %2$s are button and wrapper html tags. // translators: %1$s and %2$s are button and wrapper html tags.
esc_html__( '%1$sUnlink PayPal Subscription Plan%2$s', 'woocommerce-paypal-payments' ), esc_html__( '%1$sUnlink PayPal Subscription Plan%2$s', 'woocommerce-paypal-payments' ),
'<p class="form-field ppcp-enable-subscription" id="ppcp-enable-subscription-' . esc_attr( (string) $product->get_id() ) . '" style="' . esc_attr( $display_unlink_p ) . '"><label></label><button class="button ppcp-unlink-sub-plan" id="ppcp-unlink-sub-plan-' . esc_attr( (string) $product->get_id() ) . '">', '<p class="form-field ppcp-enable-subscription" id="ppcp-enable-subscription-' . esc_attr( (string) $product->get_id() ) . '" style="' . esc_attr( $display_unlink_p ) . '"><label></label><button class="button ppcp-unlink-sub-plan" id="ppcp-unlink-sub-plan-' . esc_attr( (string) $product->get_id() ) . '">',
'</button><span class="spinner is-active" id="spinner-unlink-plan" style="float: none; display:none;"></span></p>' '</button><span class="spinner is-active" id="spinner-unlink-plan-' . esc_attr( (string) $product->get_id() ) . '" style="float: none; display:none;"></span></p>'
); );
echo sprintf( echo sprintf(
// translators: %1$s and %2$s is open and closing paragraph tag. // translators: %1$s and %2$s is open and closing paragraph tag.
@ -839,7 +799,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
} }
} else { } else {
$display_plan_name_p = ''; $display_plan_name_p = '';
if ( $enable_subscription_product !== 'yes' && $product->get_name() !== 'AUTO-DRAFT' ) { if ( $enable_subscription_product !== 'yes' ) {
$display_plan_name_p = 'display:none;'; $display_plan_name_p = 'display:none;';
} }
echo sprintf( echo sprintf(

View file

@ -55,7 +55,7 @@ class SubscriptionStatus {
* @return void * @return void
*/ */
public function update_status( string $subscription_status, string $subscription_id ): void { public function update_status( string $subscription_status, string $subscription_id ): void {
if ( $subscription_status === 'pending-cancel' || $subscription_status === 'cancelled' ) { if ( $subscription_status === 'cancelled' ) {
try { try {
$current_subscription = $this->subscriptions_endpoint->subscription( $subscription_id ); $current_subscription = $this->subscriptions_endpoint->subscription( $subscription_id );
if ( $current_subscription->status === 'CANCELLED' ) { if ( $current_subscription->status === 'CANCELLED' ) {
@ -81,7 +81,7 @@ class SubscriptionStatus {
} }
} }
if ( $subscription_status === 'on-hold' ) { if ( $subscription_status === 'on-hold' || $subscription_status === 'pending-cancel' ) {
try { try {
$this->logger->info( $this->logger->info(
sprintf( sprintf(

View file

@ -190,7 +190,12 @@ class CaptureCardPayment {
throw new RuntimeException( $response->get_error_message() ); throw new RuntimeException( $response->get_error_message() );
} }
return json_decode( $response['body'] ); $decoded_response = json_decode( $response['body'] );
if ( ! isset( $decoded_response->invoice_id ) ) {
$decoded_response->invoice_id = $invoice_id;
}
return $decoded_response;
} }
} }

View file

@ -285,7 +285,18 @@ class CardButtonGateway extends \WC_Payment_Gateway {
try { try {
try { try {
$this->order_processor->process( $wc_order ); /**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process( $wc_order );
}
do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order ); do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order );

View file

@ -518,7 +518,18 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
//phpcs:enable WordPress.Security.NonceVerification.Recommended //phpcs:enable WordPress.Security.NonceVerification.Recommended
try { try {
$this->order_processor->process( $wc_order ); /**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process( $wc_order );
}
do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order ); do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order );

View file

@ -614,30 +614,19 @@ class PayPalGateway extends \WC_Payment_Gateway {
//phpcs:enable WordPress.Security.NonceVerification.Recommended //phpcs:enable WordPress.Security.NonceVerification.Recommended
try { try {
$paypal_subscription_id = WC()->session->get( 'ppcp_subscription_id' ) ?? '';
if ( $paypal_subscription_id ) {
$order = $this->session_handler->order();
$this->add_paypal_meta( $wc_order, $order, $this->environment );
$subscriptions = function_exists( 'wcs_get_subscriptions_for_order' ) ? wcs_get_subscriptions_for_order( $order_id ) : array();
foreach ( $subscriptions as $subscription ) {
$subscription->update_meta_data( 'ppcp_subscription', $paypal_subscription_id );
$subscription->save();
$subscription->add_order_note( "PayPal subscription {$paypal_subscription_id} added." );
}
$transaction_id = $this->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$this->update_transaction_id( $transaction_id, $wc_order );
}
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
try { try {
$this->order_processor->process( $wc_order ); /**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process( $wc_order );
}
do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order ); do_action( 'woocommerce_paypal_payments_before_handle_payment_success', $wc_order );

View file

@ -28,7 +28,7 @@ trait OrderMetaTrait {
* @param Environment $environment The environment. * @param Environment $environment The environment.
* @param OrderTransient|null $order_transient The order transient helper. * @param OrderTransient|null $order_transient The order transient helper.
*/ */
protected function add_paypal_meta( public function add_paypal_meta(
WC_Order $wc_order, WC_Order $wc_order,
Order $order, Order $order,
Environment $environment, Environment $environment,

View file

@ -28,7 +28,7 @@ trait TransactionIdHandlingTrait {
* *
* @return bool * @return bool
*/ */
protected function update_transaction_id( public function update_transaction_id(
string $transaction_id, string $transaction_id,
WC_Order $wc_order, WC_Order $wc_order,
LoggerInterface $logger = null LoggerInterface $logger = null
@ -67,7 +67,7 @@ trait TransactionIdHandlingTrait {
* *
* @return string|null * @return string|null
*/ */
protected function get_paypal_order_transaction_id( Order $order ): ?string { public function get_paypal_order_transaction_id( Order $order ): ?string {
$purchase_unit = $order->purchase_units()[0] ?? null; $purchase_unit = $order->purchase_units()[0] ?? null;
if ( ! $purchase_unit ) { if ( ! $purchase_unit ) {
return null; return null;

View file

@ -37,6 +37,19 @@ class WcSubscriptionsModule implements ServiceModule, ExtendingModule, Executabl
use ModuleClassNameIdTrait; use ModuleClassNameIdTrait;
use TransactionIdHandlingTrait; use TransactionIdHandlingTrait;
private const VAULT_SUPPORTS_SUBSCRIPTIONS = array(
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions',
);
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -56,6 +69,7 @@ class WcSubscriptionsModule implements ServiceModule, ExtendingModule, Executabl
*/ */
public function run( ContainerInterface $c ): bool { public function run( ContainerInterface $c ): bool {
$this->add_gateways_support( $c ); $this->add_gateways_support( $c );
add_action( add_action(
'woocommerce_scheduled_subscription_payment_' . PayPalGateway::ID, 'woocommerce_scheduled_subscription_payment_' . PayPalGateway::ID,
/** /**
@ -236,31 +250,6 @@ class WcSubscriptionsModule implements ServiceModule, ExtendingModule, Executabl
} }
); );
// Remove `gateway_scheduled_payments` feature support for non PayPal Subscriptions at subscription level.
add_filter(
'woocommerce_subscription_payment_gateway_supports',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $is_supported, $feature, $subscription ) {
if (
$subscription->get_payment_method() === PayPalGateway::ID
&& $feature === 'gateway_scheduled_payments'
) {
$subscription_connected = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_connected ) {
$is_supported = false;
}
}
return $is_supported;
},
10,
3
);
return true; return true;
} }
@ -406,94 +395,64 @@ class WcSubscriptionsModule implements ServiceModule, ExtendingModule, Executabl
* @return void * @return void
*/ */
private function add_gateways_support( ContainerInterface $c ): void { private function add_gateways_support( ContainerInterface $c ): void {
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
if ( ! $subscriptions_helper->plugin_is_active() ) {
return;
}
add_filter( add_filter(
'woocommerce_paypal_payments_paypal_gateway_supports', 'woocommerce_paypal_payments_paypal_gateway_supports',
function ( array $supports ) use ( $c ): array { function ( array $supports ) use ( $c ): array {
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
$settings = $c->get( 'wcgateway.settings' ); $settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings ); assert( $settings instanceof Settings );
$subscriptions_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : ''; $subscriptions_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( 'disable_paypal_subscriptions' === $subscriptions_mode ) {
if ( 'disable_paypal_subscriptions' !== $subscriptions_mode && $subscriptions_helper->plugin_is_active() ) { return $supports;
$supports = array(
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions',
'gateway_scheduled_payments',
);
} }
return array_merge(
return $supports; $supports,
self::VAULT_SUPPORTS_SUBSCRIPTIONS
);
} }
); );
add_filter( add_filter(
'woocommerce_paypal_payments_credit_card_gateway_supports', 'woocommerce_paypal_payments_credit_card_gateway_supports',
function ( array $supports ) use ( $c ): array { function ( array $supports ) use ( $c ): array {
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
$settings = $c->get( 'wcgateway.settings' ); $settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings ); assert( $settings instanceof Settings );
$vaulting_enabled = $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' ); $subscriptions_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( 'disable_paypal_subscriptions' === $subscriptions_mode ) {
if ( $vaulting_enabled && $subscriptions_helper->plugin_is_active() ) { return $supports;
$supports = array(
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions',
);
} }
$vaulting_enabled = $settings->has( 'vault_enabled_dcc' ) && $settings->get( 'vault_enabled_dcc' );
return $supports; if ( ! $vaulting_enabled ) {
return $supports;
}
return array_merge(
$supports,
self::VAULT_SUPPORTS_SUBSCRIPTIONS
);
} }
); );
add_filter( add_filter(
'woocommerce_paypal_payments_card_button_gateway_supports', 'woocommerce_paypal_payments_card_button_gateway_supports',
function ( array $supports ) use ( $c ): array { function ( array $supports ) use ( $c ): array {
$subscriptions_helper = $c->get( 'wc-subscriptions.helper' );
assert( $subscriptions_helper instanceof SubscriptionHelper );
$settings = $c->get( 'wcgateway.settings' ); $settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings ); assert( $settings instanceof Settings );
$subscriptions_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : ''; $subscriptions_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( 'disable_paypal_subscriptions' === $subscriptions_mode ) {
if ( 'disable_paypal_subscriptions' !== $subscriptions_mode && $subscriptions_helper->plugin_is_active() ) { return $supports;
$supports = array(
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions',
);
} }
return array_merge(
return $supports; $supports,
self::VAULT_SUPPORTS_SUBSCRIPTIONS
);
} }
); );
} }

View file

@ -230,7 +230,18 @@ class CheckoutOrderApproved implements RequestHandler {
} }
try { try {
$this->order_processor->process( $wc_order ); /**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process( $wc_order );
}
} catch ( RuntimeException $exception ) { } catch ( RuntimeException $exception ) {
return $this->failure_response( return $this->failure_response(
sprintf( sprintf(