From de83e02e6b3160c520f30b7d5bcd1f6e96db8058 Mon Sep 17 00:00:00 2001 From: Rasmy Nguyen Date: Fri, 6 Dec 2024 13:28:46 -0500 Subject: [PATCH] feat(subscriptions): add cancellation reason metadata (#3568) This PR adds meta data to cancelled woo subscriptions and uses this new meta to sync cancellation reason to ESPs for cancelled subscriptions. --- .../class-on-hold-duration.php | 4 ++ .../class-subscriptions-meta.php | 72 +++++++++++++++++++ .../class-woocommerce-subscriptions.php | 18 +++-- .../reader-activation/sync/class-metadata.php | 1 + .../sync/class-woocommerce.php | 15 ++-- tests/mocks/wc-mocks.php | 12 ++++ .../class-subscriptions-meta.php | 61 ++++++++++++++++ .../class-woocommerce-subscriptions.php | 41 +++++++++++ .../reader-activation-sync-woocommerce.php | 1 + 9 files changed, 211 insertions(+), 14 deletions(-) create mode 100644 includes/plugins/woocommerce-subscriptions/class-subscriptions-meta.php create mode 100644 tests/unit-tests/plugins/woocommerce-subscriptions/class-subscriptions-meta.php create mode 100644 tests/unit-tests/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions.php diff --git a/includes/plugins/woocommerce-subscriptions/class-on-hold-duration.php b/includes/plugins/woocommerce-subscriptions/class-on-hold-duration.php index 7a584505ee..14e64b3077 100644 --- a/includes/plugins/woocommerce-subscriptions/class-on-hold-duration.php +++ b/includes/plugins/woocommerce-subscriptions/class-on-hold-duration.php @@ -17,6 +17,10 @@ class On_Hold_Duration { * Initialize hooks and filters. */ public static function init() { + if ( ! WooCommerce_Subscriptions::is_enabled() ) { + return; + } + add_filter( 'woocommerce_subscription_settings', [ __CLASS__, 'add_on_hold_duration_setting' ], 11, 1 ); add_filter( 'wcs_default_retry_rules', [ __CLASS__, 'maybe_apply_on_hold_duration_rule' ], 99, 1 ); } diff --git a/includes/plugins/woocommerce-subscriptions/class-subscriptions-meta.php b/includes/plugins/woocommerce-subscriptions/class-subscriptions-meta.php new file mode 100644 index 0000000000..dca7152c63 --- /dev/null +++ b/includes/plugins/woocommerce-subscriptions/class-subscriptions-meta.php @@ -0,0 +1,72 @@ +get_meta( self::CANCELLATION_REASON_META_KEY, true ); + if ( 'active' === $to_status && $meta_value ) { + $subscription->delete_meta_data( self::CANCELLATION_REASON_META_KEY ); + $subscription->save(); + } + if ( 'cancelled' === $to_status ) { + if ( self::CANCELLATION_REASON_USER_PENDING_CANCEL === $meta_value ) { + $subscription->update_meta_data( self::CANCELLATION_REASON_META_KEY, self::CANCELLATION_REASON_USER_CANCELLED ); + } elseif ( self::CANCELLATION_REASON_ADMIN_PENDING_CANCEL === $meta_value ) { + $subscription->update_meta_data( self::CANCELLATION_REASON_META_KEY, self::CANCELLATION_REASON_ADMIN_CANCELLED ); + } else { + $meta_value = is_admin() ? self::CANCELLATION_REASON_ADMIN_CANCELLED : self::CANCELLATION_REASON_USER_CANCELLED; + $subscription->update_meta_data( self::CANCELLATION_REASON_META_KEY, $meta_value ); + } + $subscription->save(); + } + if ( 'pending-cancel' === $to_status ) { + $meta_value = is_admin() ? self::CANCELLATION_REASON_ADMIN_PENDING_CANCEL : self::CANCELLATION_REASON_USER_PENDING_CANCEL; + $subscription->update_meta_data( self::CANCELLATION_REASON_META_KEY, $meta_value ); + $subscription->save(); + } + + add_action( 'woocommerce_subscription_status_updated', array( __CLASS__, 'maybe_record_cancelled_subscription_meta' ), 10, 3 ); + } +} diff --git a/includes/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions.php b/includes/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions.php index 7ff67e3d6e..582bb79b51 100644 --- a/includes/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions.php +++ b/includes/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions.php @@ -24,15 +24,13 @@ public static function init() { * Initialize WooCommerce Subscriptions Integration. */ public static function woocommerce_subscriptions_integration_init() { - // To be included only if WooCommerce Subscriptions Integration is enabled. - // See is_enabled() method. - if ( self::is_enabled() ) { - include_once __DIR__ . '/class-on-hold-duration.php'; - On_Hold_Duration::init(); - } - + include_once __DIR__ . '/class-on-hold-duration.php'; include_once __DIR__ . '/class-renewal.php'; + include_once __DIR__ . '/class-subscriptions-meta.php'; + + On_Hold_Duration::init(); Renewal::init(); + Subscriptions_Meta::init(); } @@ -49,9 +47,9 @@ public static function is_active() { * Check if WooCommerce Subscriptions Integration is enabled. * * True if: - * - WooCommerce Subscriptions is active and, - * - Reader Activation is enabled and, - * - The NEWSPACK_SUBSCRIPTIONS_EXPIRATION feature flag is defined + * - WooCommerce Subscriptions is active and, + * - Reader Activation is enabled and, + * - The NEWSPACK_SUBSCRIPTIONS_EXPIRATION feature flag is defined and true. * * @return bool */ diff --git a/includes/reader-activation/sync/class-metadata.php b/includes/reader-activation/sync/class-metadata.php index 7747d6f574..3f419aba49 100644 --- a/includes/reader-activation/sync/class-metadata.php +++ b/includes/reader-activation/sync/class-metadata.php @@ -231,6 +231,7 @@ public static function get_payment_fields() { 'payment_page_utm' => 'Payment UTM: ', 'sub_start_date' => 'Current Subscription Start Date', 'sub_end_date' => 'Current Subscription End Date', + 'cancellation_reason' => 'Subscription Cancellation Reason', // At what interval does the recurring payment occur – e.g. day, week, month or year. 'billing_cycle' => 'Billing Cycle', // The total value of the recurring payment. diff --git a/includes/reader-activation/sync/class-woocommerce.php b/includes/reader-activation/sync/class-woocommerce.php index cb14002b21..bc28252c93 100644 --- a/includes/reader-activation/sync/class-woocommerce.php +++ b/includes/reader-activation/sync/class-woocommerce.php @@ -10,6 +10,7 @@ use Newspack\Donations; use Newspack\WooCommerce_Connection; use Newspack\WooCommerce_Order_UTM; +use Newspack\Subscriptions_Meta; defined( 'ABSPATH' ) || exit; @@ -264,10 +265,10 @@ private static function get_order_metadata( $order, $payment_page_url = false ) $metadata['membership_status'] = $current_subscription->get_status(); } - $metadata['sub_start_date'] = $current_subscription->get_date( 'start' ); - $metadata['sub_end_date'] = $current_subscription->get_date( 'end' ) ? $current_subscription->get_date( 'end' ) : ''; - $metadata['billing_cycle'] = $current_subscription->get_billing_period(); - $metadata['recurring_payment'] = $current_subscription->get_total(); + $metadata['sub_start_date'] = $current_subscription->get_date( 'start' ); + $metadata['sub_end_date'] = $current_subscription->get_date( 'end' ) ? $current_subscription->get_date( 'end' ) : ''; + $metadata['billing_cycle'] = $current_subscription->get_billing_period(); + $metadata['recurring_payment'] = $current_subscription->get_total(); $metadata['last_payment_amount'] = $current_subscription->get_total(); $metadata['last_payment_date'] = $current_subscription->get_date( 'last_order_date_paid' ) ? $current_subscription->get_date( 'last_order_date_paid' ) : gmdate( Metadata::DATE_FORMAT ); @@ -285,6 +286,12 @@ private static function get_order_metadata( $order, $payment_page_url = false ) $metadata['product_name'] = reset( $subscription_order_items )->get_name(); } } + + // Record the cancellation reason if meta exists and is not a pending cancellation. + $cancellation_reason = $current_subscription->get_meta( Subscriptions_Meta::CANCELLATION_REASON_META_KEY ); + if ( ! empty( $cancellation_reason ) && ! in_array( $cancellation_reason, [ Subscriptions_Meta::CANCELLATION_REASON_USER_PENDING_CANCEL, Subscriptions_Meta::CANCELLATION_REASON_ADMIN_PENDING_CANCEL ], true ) ) { + $metadata['cancellation_reason'] = $cancellation_reason; + } } // Clear out any payment-related fields that don't relate to the current order. diff --git a/tests/mocks/wc-mocks.php b/tests/mocks/wc-mocks.php index ec0ee1e795..b17ef505e4 100644 --- a/tests/mocks/wc-mocks.php +++ b/tests/mocks/wc-mocks.php @@ -178,6 +178,12 @@ public function get_customer_id() { public function get_meta( $field_name ) { return isset( $this->meta[ $field_name ] ) ? $this->meta[ $field_name ] : ''; } + public function update_meta_data( $field_name, $value ) { + $this->meta[ $field_name ] = $value; + } + public function delete_meta_data( $field_name ) { + unset( $this->meta[ $field_name ] ); + } public function has_status( $statuses ) { return in_array( $this->data['status'], $statuses ); } @@ -226,6 +232,9 @@ public function save() { } } +class WC_Subscriptions { +} + function wc_create_order( $data ) { return new WC_Order( $data ); } @@ -235,6 +244,9 @@ function wc_get_checkout_url() { function wcs_is_subscription( $order ) { return false; } +function wcs_create_subscription( $data = [] ) { + return new WC_Subscription( $data ); +} function wcs_get_subscriptions_for_order( $order ) { return []; } diff --git a/tests/unit-tests/plugins/woocommerce-subscriptions/class-subscriptions-meta.php b/tests/unit-tests/plugins/woocommerce-subscriptions/class-subscriptions-meta.php new file mode 100644 index 0000000000..e7dd9ee597 --- /dev/null +++ b/tests/unit-tests/plugins/woocommerce-subscriptions/class-subscriptions-meta.php @@ -0,0 +1,61 @@ +assertEquals( + '', + $subscription->get_meta( Subscriptions_Meta::CANCELLATION_REASON_META_KEY ), + 'Cancellation reason meta should be empty before any updates.' + ); + Subscriptions_Meta::maybe_record_cancelled_subscription_meta( $subscription, 'pending-cancel', 'cancelled' ); + $this->assertEquals( + '', + $subscription->get_meta( Subscriptions_Meta::CANCELLATION_REASON_META_KEY ), + 'Cancellation reason meta should be empty when subscription from-status is cancelled.' + ); + Subscriptions_Meta::maybe_record_cancelled_subscription_meta( $subscription, 'pending-cancel', 'active' ); + $this->assertEquals( + Subscriptions_Meta::CANCELLATION_REASON_USER_PENDING_CANCEL, + $subscription->get_meta( Subscriptions_Meta::CANCELLATION_REASON_META_KEY ), + 'Cancellation reason meta should be user-pending-cancel when subscription is updated to pending-cancel from active status.' + ); + Subscriptions_Meta::maybe_record_cancelled_subscription_meta( $subscription, 'active', 'pending-cancel' ); + $this->assertEquals( + '', + $subscription->get_meta( Subscriptions_Meta::CANCELLATION_REASON_META_KEY ), + 'Cancellation reason meta should be reset when subscription is updated to active from pending-cancel status.' + ); + Subscriptions_Meta::maybe_record_cancelled_subscription_meta( $subscription, 'cancelled', 'pending-cancel', $subscription ); + $this->assertEquals( + Subscriptions_Meta::CANCELLATION_REASON_USER_CANCELLED, + $subscription->get_meta( Subscriptions_Meta::CANCELLATION_REASON_META_KEY ), + 'Cancellation reason meta should be set to user-cancelled when subscription is cancelled from pending-cancel status.' + ); + } +} diff --git a/tests/unit-tests/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions.php b/tests/unit-tests/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions.php new file mode 100644 index 0000000000..a257014c6e --- /dev/null +++ b/tests/unit-tests/plugins/woocommerce-subscriptions/class-woocommerce-subscriptions.php @@ -0,0 +1,41 @@ +assertTrue( $is_active, 'WooCommerce Subscriptions integration should be active when WC_Subscriptions class exists.' ); + } + + /** + * Test WooCommerce_Subscriptions::is_enabled. + */ + public function test_is_enabled() { + $is_enabled = WooCommerce_Subscriptions::is_enabled(); + $this->assertTrue( $is_enabled, 'WooCommerce Subscriptions integration should be enabled when Feature Flag is present.' ); + } +} diff --git a/tests/unit-tests/reader-activation-sync-woocommerce.php b/tests/unit-tests/reader-activation-sync-woocommerce.php index 186173351b..c9f3c8fa5c 100644 --- a/tests/unit-tests/reader-activation-sync-woocommerce.php +++ b/tests/unit-tests/reader-activation-sync-woocommerce.php @@ -120,6 +120,7 @@ public function test_payment_metadata_basic() { 'total_paid' => self::USER_DATA['meta_input']['wc_total_spent'] + $order_data['total'], 'account' => self::$user_id, 'registration_date' => $today, + 'cancellation_reason' => '', ], ], $contact_data