Skip to content

Commit

Permalink
feat(cli): add command to synchronize premium lists (#1699)
Browse files Browse the repository at this point in the history
Co-authored-by: leogermani <[email protected]>
  • Loading branch information
adekbadek and leogermani authored Nov 15, 2024
1 parent a6dc0c6 commit 21d15a7
Show file tree
Hide file tree
Showing 10 changed files with 592 additions and 20 deletions.
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
],
"post-update-cmd": [
"vendor/bin/cghooks update"
],
"test-with-coverage": [
"XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html codecov"
]
},
"extra": {
Expand All @@ -48,4 +51,4 @@
"includes"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
<?php
/**
* Newspack Newsletters Membership-tied Subscribers CLI.
*
* @package Newspack
*/

namespace Newspack_Newsletters\CLI;

defined( 'ABSPATH' ) || exit;

/**
* Manages Settings page.
*/
class Sync_Membership_Tied_Subscribers_CLI {
/**
* Initialize the class
*
* @codeCoverageIgnore
*/
public static function init() {
add_action( 'init', [ __CLASS__, 'initialize_cli_commands' ] );
}

/**
* Initialize CLI commands.
*
* @codeCoverageIgnore
*/
public static function initialize_cli_commands() {
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
return;
}

\WP_CLI::add_command(
'newspack-newsletters sync-membership-tied-subscribers',
[ __CLASS__, 'cli_sync_membership_tied_subscribers' ],
[
'shortdesc' => 'Synchronizes the membership-tied newsletter lists with the memberships.
For each Membership Plan set to restrict Subscription Lists, all members\' ESP subscription statuses will be
realigned with the membership status.
Note that if a member has unsubscribed from a list, but has an active membership, they will be re-subscribed.',
]
);
}

/**
* Can a memberships be considered active?
*
* @param \WC_Memberships_User_Membership $user_membership User membership.
*/
public static function is_membership_active( $user_membership ): bool {
$active_statuses = wc_memberships()->get_user_memberships_instance()->get_active_access_membership_statuses();
return in_array( $user_membership->get_status(), $active_statuses );
}

/**
* CLI handler for membership-tied newsletter lists synchronization.
*
* ## OPTIONS
*
* [--live]
* : Run the command in live mode, updating the data.
*
* [--verbose]
* : More output.
*
* ## EXAMPLES
*
* wp newspack-newsletters sync-membership-tied-subscribers
*
* @param array $args Positional arguments.
* @param array $assoc_args Assoc arguments.
* @return void
*/
public static function cli_sync_membership_tied_subscribers( $args, $assoc_args ) {
\WP_CLI::log( '' );

if ( ! function_exists( 'wc_memberships_get_membership_plans' ) ) {
\WP_CLI::error( 'The woocommerce-memberships plugin must be active.' );
}

$live = isset( $assoc_args['live'] ) ? true : false;
$verbose = isset( $assoc_args['verbose'] ) ? true : false;
if ( $live ) {
\WP_CLI::log(
'Live mode.
Note that if a member has unsubscribed from a list, but has an active membership, they will be re-subscribed.'
);
} else {
\WP_CLI::log( 'Dry run. Use --live flag to run in live mode.' );
}
\WP_CLI::log( '' );

$provider = \Newspack_Newsletters::get_service_provider();
if ( ! $provider ) {
\WP_CLI::error( 'No ESP provider set.' );
}

foreach ( wc_memberships_get_membership_plans() as $plan ) {
foreach ( $plan->get_content_restriction_rules() as $rule ) {
if ( \Newspack\Newsletters\Subscription_Lists::CPT === $rule->get_content_type_name() ) {
if ( $verbose ) {
\WP_CLI::log( sprintf( 'Processing WCM plan "%s"', $plan->get_name() ) );
}

$restricted_lists = [];
foreach ( $rule->get_object_ids() as $list_id ) {
try {
$list = new \Newspack\Newsletters\Subscription_List( $list_id );
$restricted_lists[] = $list;
} catch ( \Throwable $th ) {
\WP_CLI::warning( sprintf( 'Could not get subscription list for ID %d: %s', $list_id, $th->getMessage() ) );
continue;
}
}

if ( empty( $restricted_lists ) ) {
\WP_CLI::warning( 'No subscription lists to process for the plan, skipping.' );
continue;
}

$plan_memberships = $plan->get_memberships();
foreach ( $restricted_lists as $list ) {
$list_public_id = $list->get_public_id();
\WP_CLI::log( sprintf( ' - Synchronizing list "%s" (#%d, public ID: %s)', $list->get_title(), $list->get_id(), $list_public_id ) );
foreach ( $plan_memberships as $user_membership ) {
$user = $user_membership->get_user();
if ( ! $user ) {
continue;
}
$email = $user->user_email;
if ( ! $email ) {
\WP_CLI::warning( sprintf( 'No email for user #%d, skipping.', $user->ID ) );
continue;
}
$membership_id = $user_membership->get_id();
$membership_status = $user_membership->get_status();
if ( $verbose ) {
\WP_CLI::log( '' );
\WP_CLI::log( sprintf( ' - Processing user %s with membership #%d of status %s.', $email, $membership_id, $membership_status ) );
}

$contact_lists = \Newspack_Newsletters_Subscription::get_contact_lists( $email );
$currently_subscribed = is_array( $contact_lists ) && in_array( $list_public_id, $contact_lists, true );

// Determine which lists to update.
$lists_to_add = [];
$lists_to_remove = [];
if ( self::is_membership_active( $user_membership ) ) {
if ( ! $currently_subscribed ) {
$lists_to_add = [ $list_public_id ];
}
} elseif ( $currently_subscribed ) {
$lists_to_remove = [ $list_public_id ];
}

if ( empty( $lists_to_add ) && empty( $lists_to_remove ) ) {
if ( $verbose ) {
\WP_CLI::log( ' - No changes needed, skipping.' );
}
continue;
}

$result = null;

// Check user status in the ESP.
$contact_data = \Newspack_Newsletters_Subscription::get_contact_data( $email );
$should_create_contact = \is_wp_error( $contact_data );
if ( $should_create_contact ) {

// allow subscription to restricted lists.
remove_filter( 'newspack_newsletters_contact_lists', [ 'Newspack_Newsletters\Plugins\Woocommerce_Memberships', 'filter_lists' ] );

if ( empty( $lists_to_add ) ) {
if ( $verbose ) {
\WP_CLI::log( ' - Contact not found in ESP, but there are no lists to add, skipping.' );
}
continue;
}
if ( $verbose ) {
\WP_CLI::log(
$live ? ' - Contact not found - adding the contact in the ESP…' : ' - Contact not found - would add the contact to the ESP.'
);
}
if ( $live ) {
$result = \Newspack_Newsletters_Contacts::subscribe(
[
'email' => $email,
'name' => $user->display_name,
],
$lists_to_add,
false,
'Adding contact when running the sync-membership-tied-subscribers CLI sync script.'
);
}
} else {
if ( $verbose ) {
\WP_CLI::log(
$live ? ' - Updating the contact in the ESP…' : ' - Would update the contact in the ESP.'
);
}
if ( $live ) {
$result = \Newspack_Newsletters_Contacts::add_and_remove_lists(
$email,
$lists_to_add,
$lists_to_remove,
'Updating contact when running the sync-membership-tied-subscribers CLI sync script.'
);
}
}

if ( \is_wp_error( $result ) ) {
\WP_CLI::warning( sprintf( 'Error when updating lists: %s', $result->get_error_message() ) );
} elseif ( $result !== null ) {
\WP_CLI::success( sprintf( 'User %s processed successfully!', $email ) );
}
}
}
}
}
}

\WP_CLI::log( '' );
}
}
Sync_Membership_Tied_Subscribers_CLI::init();
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

defined( 'ABSPATH' ) || exit;

require_once NEWSPACK_NEWSLETTERS_PLUGIN_FILE . '/includes/plugins/woocommerce-memberships/class-sync-membership-tied-subscribers-cli.php';

/**
* Manages Settings page.
*/
Expand Down Expand Up @@ -169,7 +171,7 @@ public static function handle_membership_status_change( $user_membership, $old_s
}

/**
* Removes user from premium lists associated with a membership plan
* Removes user from membership-tied lists associated with a membership plan
*
* @param \WC_Memberships_User_Membership $user_membership The User Membership object.
* @return void
Expand Down Expand Up @@ -229,7 +231,7 @@ public static function remove_user_from_lists( $user_membership ) {
}

/**
* Adds user to premium lists when a membership is granted
* Adds user to membership-tied lists when a membership is granted
*
* @param \WC_Memberships_Membership_Plan $plan the plan that user was granted access to.
* @param array $args {
Expand Down Expand Up @@ -266,7 +268,7 @@ public static function add_user_to_lists( $plan, $args ) {
return;
}

// If post-checkout newsletter signup is enabled, we only want to add the reader to premium lists if:
// If post-checkout newsletter signup is enabled, we only want to add the reader to membership-tied lists if:
// - The membership is going from `paused` to `active` status (when a prior subscription is renewed).
// - The reader was already subscribed to the list(s).
$post_checkout_newsletter_signup_enabled = defined( 'NEWSPACK_ENABLE_POST_CHECKOUT_NEWSLETTER_SIGNUP' ) && NEWSPACK_ENABLE_POST_CHECKOUT_NEWSLETTER_SIGNUP;
Expand Down
2 changes: 1 addition & 1 deletion newspack-newsletters.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
require_once NEWSPACK_NEWSLETTERS_PLUGIN_FILE . '/includes/tracking/class-data-events.php';
require_once NEWSPACK_NEWSLETTERS_PLUGIN_FILE . '/includes/tracking/class-admin.php';
require_once NEWSPACK_NEWSLETTERS_PLUGIN_FILE . '/includes/class-newspack-newsletters.php';
require_once NEWSPACK_NEWSLETTERS_PLUGIN_FILE . '/includes/plugins/class-woocommerce-memberships.php';
require_once NEWSPACK_NEWSLETTERS_PLUGIN_FILE . '/includes/plugins/woocommerce-memberships/class-woocommerce-memberships.php';

// This MUST be initialized after Newspack_Newsletter class.
\Newspack\Newsletters\Subscription_Lists::init();
Expand Down
6 changes: 6 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,18 @@ function _manually_load_plugin() {
// Trait used to test Send Lists.
require_once 'trait-send-lists-setup.php';

// Trait used to test WC Memberships.
require_once 'trait-wc-memberships-setup.php';

// MailChimp mock.
require_once 'mocks/class-mailchimp-mock.php';

// WC Memberships mock.
require_once 'mocks/wc-memberships.php';

// WC CLI mock.
require_once 'mocks/wp-cli.php';

// Abstract ESP tests.
require_once 'abstract-esp-tests.php';

Expand Down
65 changes: 62 additions & 3 deletions tests/mocks/wc-memberships.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
<?php // phpcs:ignoreFile
<?php // phpcs:disable Squiz.Commenting, Universal.Files, Generic.Files, WordPress.DB

class WC_Memberships_User_Membership {
public function __construct( $id, $user_id ) {
$this->id = $id;
$this->user_id = $user_id;
}
public function get_user() {
return get_user_by( 'id', $this->user_id );
}
public function get_id() {
return $this->id;
}
public function get_status() {
return str_replace( 'wcm-', '', get_post( $this->id )->post_status );
}
}

class WC_Memberships_Membership_Plan {
private $id;
private $name;
Expand All @@ -8,11 +25,33 @@ public function __construct( $id ) {
$this->id = $id;
$this->name = 'Test Membership';
}

public function get_content_restriction_rules() {
return $this->rules;
}

public function get_memberships() {
$args = [
'post_type' => 'wc_user_membership',
'post_status' => 'any',
'meta_query' => [
[
'key' => '_membership_plan_id',
'value' => $this->id,
],
],
];
$query = new WP_Query( $args );
$memberships = [];
foreach ( $query->posts as $post ) {
$memberships[] = new WC_Memberships_User_Membership( $post->ID, $post->post_author );
}
return $memberships;
}
public function get_id() {
return $this->id;
}
public function get_name() {
return $this->name;
}
public function set_content_restriction_rules( $rules ) {
$this->rules = $rules;
}
Expand All @@ -37,3 +76,23 @@ public function get_object_ids() {
return $this->object_id_rules;
}
}

function wc_memberships_get_membership_plans() {
global $test_wc_memberships;
if ( empty( $test_wc_memberships ) ) {
return [];
}
return $test_wc_memberships;
}

function wc_memberships() {
return new class() {
public function get_user_memberships_instance() {
return new class() {
public function get_active_access_membership_statuses() {
return [ 'active', 'complimentary', 'free_trial', 'pending' ];
}
};
}
};
}
Loading

0 comments on commit 21d15a7

Please sign in to comment.