From 9bc9c0305398721467bf4a3ac48fc172e04d4f48 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Fri, 13 Sep 2024 15:35:13 +0200 Subject: [PATCH 1/7] Extract renewal handler logic to new class --- .../ppcp-paypal-subscriptions/services.php | 3 + .../src/RenewalHandler.php | 70 +++++++++++++++++++ modules/ppcp-webhooks/services.php | 2 +- .../src/Handler/PaymentSaleCompleted.php | 46 ++++++------ 4 files changed, 96 insertions(+), 25 deletions(-) create mode 100644 modules/ppcp-paypal-subscriptions/src/RenewalHandler.php diff --git a/modules/ppcp-paypal-subscriptions/services.php b/modules/ppcp-paypal-subscriptions/services.php index 4e64e678b..32f32b193 100644 --- a/modules/ppcp-paypal-subscriptions/services.php +++ b/modules/ppcp-paypal-subscriptions/services.php @@ -40,4 +40,7 @@ return array( dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' ); }, + 'paypal-subscriptions.renewal-handler' => static function ( ContainerInterface $container ): RenewalHandler { + return new RenewalHandler( $container->get( 'woocommerce.logger.woocommerce' ) ); + }, ); diff --git a/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php b/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php new file mode 100644 index 000000000..1261f418e --- /dev/null +++ b/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php @@ -0,0 +1,70 @@ +logger = $logger; + } + + /** + * Process subscription renewal. + * + * @param WC_Subscription[] $subscriptions WC Subscriptions. + * @param string $transaction_id PayPal transaction ID. + * @return void + * @throws WC_Data_Exception If something goes wrong while setting payment method. + */ + public function process( array $subscriptions, string $transaction_id ): void { + foreach ( $subscriptions as $subscription ) { + $is_renewal = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? ''; + if ( $is_renewal ) { + $renewal_order = wcs_create_renewal_order( $subscription ); + if ( is_a( $renewal_order, WC_Order::class ) ) { + $renewal_order->set_payment_method( $subscription->get_payment_method() ); + $renewal_order->payment_complete(); + $this->update_transaction_id( $transaction_id, $renewal_order, $this->logger ); + break; + } + } + + $parent_order = wc_get_order( $subscription->get_parent() ); + if ( is_a( $parent_order, WC_Order::class ) ) { + $subscription->update_meta_data( '_ppcp_is_subscription_renewal', 'true' ); + $subscription->save_meta_data(); + $this->update_transaction_id( $transaction_id, $parent_order, $this->logger ); + } + } + } +} diff --git a/modules/ppcp-webhooks/services.php b/modules/ppcp-webhooks/services.php index 0f75a58a0..e179fd24a 100644 --- a/modules/ppcp-webhooks/services.php +++ b/modules/ppcp-webhooks/services.php @@ -99,7 +99,7 @@ return array( new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory, $payment_token_helper ), new VaultPaymentTokenDeleted( $logger ), new PaymentCapturePending( $logger ), - new PaymentSaleCompleted( $logger ), + new PaymentSaleCompleted( $logger, $container->get( 'paypal-subscriptions.renewal-handler' ) ), new PaymentSaleRefunded( $logger, $refund_fees_updater ), new BillingSubscriptionCancelled( $logger ), new BillingPlanPricingChangeActivated( $logger ), diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php index 1679c46db..757cfd163 100644 --- a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php @@ -10,8 +10,8 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Webhooks\Handler; use Psr\Log\LoggerInterface; -use WC_Order; -use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; +use WC_Data_Exception; +use WooCommerce\PayPalCommerce\PayPalSubscriptions\RenewalHandler; use WP_REST_Request; use WP_REST_Response; @@ -20,7 +20,14 @@ use WP_REST_Response; */ class PaymentSaleCompleted implements RequestHandler { - use TransactionIdHandlingTrait, RequestHandlerTrait; + use RequestHandlerTrait; + + /** + * Renewal handler. + * + * @var RenewalHandler + */ + private $renewal_handler; /** * The logger. @@ -33,9 +40,11 @@ class PaymentSaleCompleted implements RequestHandler { * PaymentSaleCompleted constructor. * * @param LoggerInterface $logger The logger. + * @param RenewalHandler $renewal_handler Renewal handler. */ - public function __construct( LoggerInterface $logger ) { - $this->logger = $logger; + public function __construct( LoggerInterface $logger, RenewalHandler $renewal_handler ) { + $this->logger = $logger; + $this->renewal_handler = $renewal_handler; } /** @@ -68,7 +77,7 @@ class PaymentSaleCompleted implements RequestHandler { */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { if ( is_null( $request['resource'] ) ) { - return $this->failure_response(); + return $this->failure_response( 'Could not retrieve resource.' ); } if ( ! function_exists( 'wcs_get_subscriptions' ) ) { @@ -85,7 +94,7 @@ class PaymentSaleCompleted implements RequestHandler { return $this->failure_response( 'Could not retrieve transaction id for subscription.' ); } - $args = array( + $args = array( // phpcs:ignore WordPress.DB.SlowDBQuery 'meta_query' => array( array( @@ -95,24 +104,13 @@ class PaymentSaleCompleted implements RequestHandler { ), ), ); - $subscriptions = wcs_get_subscriptions( $args ); - foreach ( $subscriptions as $subscription ) { - $is_renewal = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? ''; - if ( $is_renewal ) { - $renewal_order = wcs_create_renewal_order( $subscription ); - if ( is_a( $renewal_order, WC_Order::class ) ) { - $renewal_order->set_payment_method( $subscription->get_payment_method() ); - $renewal_order->payment_complete(); - $this->update_transaction_id( $transaction_id, $renewal_order, $this->logger ); - break; - } - } - $parent_order = wc_get_order( $subscription->get_parent() ); - if ( is_a( $parent_order, WC_Order::class ) ) { - $subscription->update_meta_data( '_ppcp_is_subscription_renewal', 'true' ); - $subscription->save_meta_data(); - $this->update_transaction_id( $transaction_id, $parent_order, $this->logger ); + $subscriptions = wcs_get_subscriptions( $args ); + if ( $subscriptions ) { + try { + $this->renewal_handler->process( $subscriptions, $transaction_id ); + } catch ( WC_Data_Exception $exception ) { + return $this->failure_response( 'Could not update payment method.' ); } } From 17cff6bd09f182dd4f11e823ad54aab2c913e76f Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Fri, 13 Sep 2024 17:05:39 +0200 Subject: [PATCH 2/7] Add test for renewal handler --- .../PayPalSubscriptionsRenewalTest.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php diff --git a/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php b/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php new file mode 100644 index 000000000..f67b6be55 --- /dev/null +++ b/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php @@ -0,0 +1,42 @@ +getContainer(); + $handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce')); + + $order = wc_create_order(); + $order->set_customer_id( 1 ); + $order->save(); + + $subscription = wcs_create_subscription( + array( + 'order_id' => $order->get_id(), + 'status' => 'active', + 'billing_period' => WC_Subscriptions_Product::get_period( $_ENV['PAYPAL_SUBSCRIPTIONS_PRODUCT_ID'] ), + 'billing_interval' => WC_Subscriptions_Product::get_interval( $_ENV['PAYPAL_SUBSCRIPTIONS_PRODUCT_ID'] ), + 'customer_id' => $order->get_customer_id(), + ) + ); + + $parent = $subscription->get_related_orders( 'ids', array( 'parent' ) ); + $this->assertEquals(count($parent), 1); + $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) ); + $this->assertEquals(count($renewal), 0); + + $handler->process([$subscription], 'TRANSACTION-ID'); + $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) ); + $this->assertEquals(count($renewal), 0); + + $handler->process([$subscription], 'TRANSACTION-ID'); + $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) ); + $this->assertEquals(count($renewal), 1); + } +} From 80e64575025e059392cbe606022dbab7fac9d295 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Mon, 16 Sep 2024 14:58:25 +0200 Subject: [PATCH 3/7] Extract is renewal logic to method --- .../src/RenewalHandler.php | 18 ++++++++- .../PayPalSubscriptionsRenewalTest.php | 39 +++++++++---------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php b/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php index 1261f418e..898a81c57 100644 --- a/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php +++ b/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php @@ -48,8 +48,7 @@ class RenewalHandler { */ public function process( array $subscriptions, string $transaction_id ): void { foreach ( $subscriptions as $subscription ) { - $is_renewal = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? ''; - if ( $is_renewal ) { + if ( $this->is_renewal( $subscription ) ) { $renewal_order = wcs_create_renewal_order( $subscription ); if ( is_a( $renewal_order, WC_Order::class ) ) { $renewal_order->set_payment_method( $subscription->get_payment_method() ); @@ -67,4 +66,19 @@ class RenewalHandler { } } } + + /** + * Checks whether subscription order is for renewal or not. + * + * @param WC_Subscription $subscription WC Subscription. + * @return bool + */ + private function is_renewal( WC_Subscription $subscription ): bool { + $subscription_renewal_meta = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? ''; + if ( $subscription_renewal_meta === 'true' ) { + return true; + } + + return false; + } } diff --git a/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php b/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php index f67b6be55..f951aadc2 100644 --- a/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php +++ b/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php @@ -2,34 +2,16 @@ namespace WooCommerce\PayPalCommerce\Tests\E2e; -use WC_Subscriptions_Product; use WooCommerce\PayPalCommerce\PayPalSubscriptions\RenewalHandler; class PayPalSubscriptionsRenewalTest extends TestCase { - public function test_process() + public function test_is_renewal_by_meta() { $c = $this->getContainer(); $handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce')); - $order = wc_create_order(); - $order->set_customer_id( 1 ); - $order->save(); - - $subscription = wcs_create_subscription( - array( - 'order_id' => $order->get_id(), - 'status' => 'active', - 'billing_period' => WC_Subscriptions_Product::get_period( $_ENV['PAYPAL_SUBSCRIPTIONS_PRODUCT_ID'] ), - 'billing_interval' => WC_Subscriptions_Product::get_interval( $_ENV['PAYPAL_SUBSCRIPTIONS_PRODUCT_ID'] ), - 'customer_id' => $order->get_customer_id(), - ) - ); - - $parent = $subscription->get_related_orders( 'ids', array( 'parent' ) ); - $this->assertEquals(count($parent), 1); - $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) ); - $this->assertEquals(count($renewal), 0); + $subscription = $this->createSubscription(); $handler->process([$subscription], 'TRANSACTION-ID'); $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) ); @@ -39,4 +21,21 @@ class PayPalSubscriptionsRenewalTest extends TestCase $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) ); $this->assertEquals(count($renewal), 1); } + + private function createSubscription() + { + $order = wc_create_order(); + $order->set_customer_id(1); + $order->save(); + + return wcs_create_subscription( + array( + 'order_id' => $order->get_id(), + 'status' => 'active', + 'billing_period' => 'day', + 'billing_interval' => '1', + 'customer_id' => $order->get_customer_id(), + ) + ); + } } From 3d018945f49d2227d834fa6dfd5b82de5793239f Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Tue, 17 Sep 2024 12:05:35 +0200 Subject: [PATCH 4/7] Create subscription via rest api for testing --- .../PayPalSubscriptionsRenewalTest.php | 73 ++++++++++++++++--- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php b/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php index f951aadc2..2e1229685 100644 --- a/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php +++ b/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php @@ -24,18 +24,73 @@ class PayPalSubscriptionsRenewalTest extends TestCase private function createSubscription() { - $order = wc_create_order(); - $order->set_customer_id(1); - $order->save(); + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Basic ' . base64_encode( 'admin:admin' ), + 'Content-Type' => 'application/json', + ], + 'body' => wp_json_encode([ + 'customer_id' => 1, + 'set_paid' => true, + 'payment_method' => 'ppcp-gateway', + 'billing' => [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + 'email' => 'john.doe@example.com', + 'phone' => '(555) 555-5555' + ], + 'line_items' => [ + [ + 'product_id' => 156, + 'quantity' => 1 + ] + ], + ]), + ]; - return wcs_create_subscription( - array( - 'order_id' => $order->get_id(), + $response = wp_remote_request( + 'https://woocommerce-paypal-payments.ddev.site/wp-json/wc/v3/orders', + $args + ); + + $body = json_decode( $response['body'] ); + + $args = [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Basic ' . base64_encode( 'admin:admin' ), + 'Content-Type' => 'application/json', + ], + 'body' => wp_json_encode([ + 'parent_id' => $body->id, + 'customer_id' => 1, 'status' => 'active', 'billing_period' => 'day', - 'billing_interval' => '1', - 'customer_id' => $order->get_customer_id(), - ) + 'billing_interval' => 1, + 'payment_method' => 'ppcp-gateway', + 'line_items' => [ + [ + 'product_id' => $_ENV['PAYPAL_SUBSCRIPTIONS_PRODUCT_ID'], + 'quantity' => 1 + ] + ], + ]), + ]; + + $response = wp_remote_request( + 'https://woocommerce-paypal-payments.ddev.site/wp-json/wc/v3/subscriptions?per_page=1', + $args ); + + $body = json_decode( $response['body'] ); + + return wcs_get_subscription($body->id); } } From eb19f05226184e737489d087faf094b631e9b54d Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 18 Sep 2024 11:52:28 +0200 Subject: [PATCH 5/7] Do not return if `_ppcp_enable_subscription_product` not exist in the post request --- .../src/PayPalSubscriptionsModule.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php b/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php index 07dd5c8c6..1319feae9 100644 --- a/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php +++ b/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php @@ -66,9 +66,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu function( $product_id ) use ( $c ) { $subscriptions_helper = $c->get( 'wc-subscriptions.helper' ); assert( $subscriptions_helper instanceof SubscriptionHelper ); - - $connect_subscription = wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) ); - if ( ! $subscriptions_helper->plugin_is_active() || $connect_subscription !== 'yes' ) { + if ( ! $subscriptions_helper->plugin_is_active() ) { return; } From 750e5891c41480b1b380c55c11d9ed8c48f854a3 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 18 Sep 2024 12:15:32 +0200 Subject: [PATCH 6/7] Revert latest change, will be included in its own PR --- .../src/PayPalSubscriptionsModule.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php b/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php index 1319feae9..07dd5c8c6 100644 --- a/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php +++ b/modules/ppcp-paypal-subscriptions/src/PayPalSubscriptionsModule.php @@ -66,7 +66,9 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu function( $product_id ) use ( $c ) { $subscriptions_helper = $c->get( 'wc-subscriptions.helper' ); assert( $subscriptions_helper instanceof SubscriptionHelper ); - if ( ! $subscriptions_helper->plugin_is_active() ) { + + $connect_subscription = wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) ); + if ( ! $subscriptions_helper->plugin_is_active() || $connect_subscription !== 'yes' ) { return; } From 2f21df65b38c27af0e8d73bb7e85553ce7436211 Mon Sep 17 00:00:00 2001 From: Emili Castells Guasch Date: Wed, 18 Sep 2024 14:43:41 +0200 Subject: [PATCH 7/7] Add time based logic for checking if is order is parent or for renewal --- .../src/RenewalHandler.php | 29 +++++++++++++++++-- .../PayPalSubscriptionsRenewalTest.php | 17 +++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php b/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php index 898a81c57..f88fea7d8 100644 --- a/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php +++ b/modules/ppcp-paypal-subscriptions/src/RenewalHandler.php @@ -48,9 +48,17 @@ class RenewalHandler { */ public function process( array $subscriptions, string $transaction_id ): void { foreach ( $subscriptions as $subscription ) { - if ( $this->is_renewal( $subscription ) ) { + if ( $this->is_for_renewal_order( $subscription ) ) { $renewal_order = wcs_create_renewal_order( $subscription ); if ( is_a( $renewal_order, WC_Order::class ) ) { + $this->logger->info( + sprintf( + 'Processing renewal order #%s for subscription #%s', + $renewal_order->get_id(), + $subscription->get_id() + ) + ); + $renewal_order->set_payment_method( $subscription->get_payment_method() ); $renewal_order->payment_complete(); $this->update_transaction_id( $transaction_id, $renewal_order, $this->logger ); @@ -60,6 +68,14 @@ class RenewalHandler { $parent_order = wc_get_order( $subscription->get_parent() ); if ( is_a( $parent_order, WC_Order::class ) ) { + $this->logger->info( + sprintf( + 'Processing parent order #%s for subscription #%s', + $parent_order->get_id(), + $subscription->get_id() + ) + ); + $subscription->update_meta_data( '_ppcp_is_subscription_renewal', 'true' ); $subscription->save_meta_data(); $this->update_transaction_id( $transaction_id, $parent_order, $this->logger ); @@ -73,12 +89,19 @@ class RenewalHandler { * @param WC_Subscription $subscription WC Subscription. * @return bool */ - private function is_renewal( WC_Subscription $subscription ): bool { + private function is_for_renewal_order( WC_Subscription $subscription ): bool { $subscription_renewal_meta = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? ''; if ( $subscription_renewal_meta === 'true' ) { return true; } - return false; + if ( + time() >= $subscription->get_time( 'start' ) + && ( time() - $subscription->get_time( 'start' ) ) <= ( 8 * HOUR_IN_SECONDS ) + ) { + return false; + } + + return true; } } diff --git a/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php b/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php index 2e1229685..322b54731 100644 --- a/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php +++ b/tests/e2e/PHPUnit/PayPalSubscriptionsRenewalTest.php @@ -6,23 +6,33 @@ use WooCommerce\PayPalCommerce\PayPalSubscriptions\RenewalHandler; class PayPalSubscriptionsRenewalTest extends TestCase { - public function test_is_renewal_by_meta() + public function test_parent_order() { $c = $this->getContainer(); $handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce')); - $subscription = $this->createSubscription(); + // Simulates receiving webhook 1 minute after subscription start. + $subscription = $this->createSubscription('-1 minute'); $handler->process([$subscription], 'TRANSACTION-ID'); $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) ); $this->assertEquals(count($renewal), 0); + } + + public function test_renewal_order() + { + $c = $this->getContainer(); + $handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce')); + + // Simulates receiving webhook 9 hours after subscription start. + $subscription = $this->createSubscription('-9 hour'); $handler->process([$subscription], 'TRANSACTION-ID'); $renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) ); $this->assertEquals(count($renewal), 1); } - private function createSubscription() + private function createSubscription(string $startDate) { $args = [ 'method' => 'POST', @@ -69,6 +79,7 @@ class PayPalSubscriptionsRenewalTest extends TestCase 'Content-Type' => 'application/json', ], 'body' => wp_json_encode([ + 'start_date' => gmdate( 'Y-m-d H:i:s', strtotime($startDate) ), 'parent_id' => $body->id, 'customer_id' => 1, 'status' => 'active',