Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(subscriptions): add cancellation reason metadata #3568

Merged
merged 17 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 16 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
Expand Up @@ -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 );
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/**
* WooCommerce Subscriptions meta class.
*
* @package Newspack
*/

namespace Newspack;

defined( 'ABSPATH' ) || exit;

/**
* Main class.
*/
class Subscriptions_Meta {
const CANCELLATION_REASON_META_KEY = 'newspack_subscriptions_cancellation_reason';
const CANCELLATION_REASON_ADMIN_CANCELLED = 'manually-cancelled';
const CANCELLATION_REASON_ADMIN_PENDING_CANCEL = 'manually-pending-cancel';
const CANCELLATION_REASON_USER_CANCELLED = 'user-cancelled';
const CANCELLATION_REASON_USER_PENDING_CANCEL = 'user-pending-cancel';

/**
* Initialize hooks and filters.
*/
public static function init() {
if ( ! WooCommerce_Subscriptions::is_enabled() ) {
return;
}

add_action( 'woocommerce_subscription_status_updated', array( __CLASS__, 'maybe_record_cancelled_subscription_meta' ), 10, 3 );
}

/**
* Record woo custom field for cancelled subscriptions.
*
* @param WC_Subscription $subscription The subscription object.
* @param string $to_status The status the subscription is changing to.
* @param string $from_status The status the subscription is changing from.
*/
public static function maybe_record_cancelled_subscription_meta( $subscription, $to_status, $from_status ) {
// We only care about active, cancelled, and pending statuses.
if ( ! in_array( $to_status, [ 'active', 'cancelled', 'pending-cancel' ], true ) || in_array( $from_status, [ 'cancelled', 'expired' ], true ) ) {
return;
}
$meta_value = $subscription->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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}


Expand All @@ -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
*/
Expand Down
1 change: 1 addition & 0 deletions includes/reader-activation/sync/class-metadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 11 additions & 4 deletions includes/reader-activation/sync/class-woocommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Newspack\Donations;
use Newspack\WooCommerce_Connection;
use Newspack\WooCommerce_Order_UTM;
use Newspack\Subscriptions_Meta;

defined( 'ABSPATH' ) || exit;

Expand Down Expand Up @@ -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 );

Expand All @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions tests/mocks/wc-mocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
Expand Down Expand Up @@ -226,6 +232,9 @@ public function save() {
}
}

class WC_Subscriptions {
}

function wc_create_order( $data ) {
return new WC_Order( $data );
}
Expand All @@ -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 [];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/**
* Tests the WooCommerce Subscriptions integration class.
*
* @package Newspack\Tests
*/

use Newspack\Subscriptions_Meta;
use Newspack\WooCommerce_Subscriptions;

/**
* Test WooCommerce Subscriptions integration functionality.
*
* @group WooCommerce_Subscriptions_Integration
*/
class Newspack_Test_Subscriptions_Meta extends WP_UnitTestCase {
/**
* Setup for the tests.
*/
public static function set_up_before_class() {
if ( ! defined( 'NEWSPACK_SUBSCRIPTIONS_EXPIRATION' ) ) {
define( 'NEWSPACK_SUBSCRIPTIONS_EXPIRATION', true );
}
}

/**
* Test Subscriptions_Meta::maybe_record_cancelled_subscription_meta.
*/
public function test_maybe_record_cancelled_subscription_meta() {
$subscription = wcs_create_subscription();
$this->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.'
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/**
* Tests the WooCommerce Subscriptions integration class.
*
* @package Newspack\Tests
*/

use Newspack\WooCommerce_Subscriptions;
use Newspack\Reader_Activation;

/**
* Test WooCommerce Subscriptions integration functionality.
*
* @group WooCommerce_Subscriptions_Integration
*/
class Newspack_Test_WooCommerce_Subscriptions extends WP_UnitTestCase {
/**
* Setup for the tests.
*/
public static function set_up_before_class() {
if ( ! defined( 'NEWSPACK_SUBSCRIPTIONS_EXPIRATION' ) ) {
define( 'NEWSPACK_SUBSCRIPTIONS_EXPIRATION', true );
}
}

/**
* Test WooCommerce_Subscriptions::is_active.
*/
public function test_is_active() {
$is_active = WooCommerce_Subscriptions::is_active();
$this->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.' );
Copy link
Contributor Author

@chickenn00dle chickenn00dle Dec 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to test the case where FF is not defined, as well as when Reader Activation is not enabled, but could not find a way to modify globally defined constants between test runs. I tried @preserveGlobalState paired with @runInSeparateProcess, but this ended up removing other globals the tested method relied on.

Would love any advice on how to approach this if possible

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no good way to test constants... The best we could do would be to change the approach we use for Feature Flags and use something else to handle these things

}
}
1 change: 1 addition & 0 deletions tests/unit-tests/reader-activation-sync-woocommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading