Skip to content

Commit

Permalink
feat(subscriptions): add cancellation reason metadata (#3568)
Browse files Browse the repository at this point in the history
This PR adds meta data to cancelled woo subscriptions and uses this new meta to sync cancellation reason to ESPs for cancelled subscriptions.
  • Loading branch information
chickenn00dle authored Dec 6, 2024
1 parent cb764e3 commit de83e02
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 14 deletions.
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,72 @@
<?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;
}

remove_action( 'woocommerce_subscription_status_updated', array( __CLASS__, 'maybe_record_cancelled_subscription_meta' ) );

$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();
}

add_action( 'woocommerce_subscription_status_updated', array( __CLASS__, 'maybe_record_cancelled_subscription_meta' ), 10, 3 );
}
}
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.' );
}
}
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

0 comments on commit de83e02

Please sign in to comment.