diff --git a/includes/class-rest-authenticaton.php b/includes/class-rest-authenticaton.php
index f16148f3..269c3ec7 100644
--- a/includes/class-rest-authenticaton.php
+++ b/includes/class-rest-authenticaton.php
@@ -33,6 +33,10 @@ class Rest_Authenticaton {
'endpoint' => '|^/wc/v2/memberships/plans|',
'callback' => [ __CLASS__, 'add_filter_for_woo_read_endpoints' ],
],
+ 'get-woo-memberships' => [
+ 'endpoint' => '|^/wc/v2/memberships|',
+ 'callback' => [ __CLASS__, 'add_filter_for_woo_read_endpoints' ],
+ ],
];
/**
diff --git a/includes/cli/backfillers/class-reader-registered.php b/includes/cli/backfillers/class-reader-registered.php
index b801e7d7..4e291d30 100644
--- a/includes/cli/backfillers/class-reader-registered.php
+++ b/includes/cli/backfillers/class-reader-registered.php
@@ -56,7 +56,9 @@ public function get_events() {
]
);
+ WP_CLI::line( '' );
WP_CLI::line( sprintf( 'Found %s user(s) eligible for sync.', count( $users ) ) );
+ WP_CLI::line( '' );
$this->maybe_initialize_progress_bar( 'Processing users', count( $users ) );
diff --git a/includes/cli/backfillers/class-woocommerce-membership-updated.php b/includes/cli/backfillers/class-woocommerce-membership-updated.php
index 6a30c52f..5b3d08c6 100644
--- a/includes/cli/backfillers/class-woocommerce-membership-updated.php
+++ b/includes/cli/backfillers/class-woocommerce-membership-updated.php
@@ -58,6 +58,9 @@ public function get_events() {
$this->maybe_initialize_progress_bar( 'Processing memberships', count( $membership_posts_ids ) );
$events = [];
+ WP_CLI::line( '' );
+ WP_CLI::line( sprintf( 'Found %s membership(s) eligible for sync.', count( $membership_posts_ids ) ) );
+ WP_CLI::line( '' );
foreach ( $membership_posts_ids as $post_id ) {
$membership = new \WC_Memberships_User_Membership( $post_id );
@@ -77,6 +80,7 @@ public function get_events() {
'membership_id' => $membership->get_id(),
'new_status' => $status,
];
+ $timestamp = null;
switch ( $status ) {
case 'paused':
$timestamp = strtotime( $membership->get_paused_date() );
diff --git a/includes/hub/admin/class-membership-plans-table.php b/includes/hub/admin/class-membership-plans-table.php
index 29770d01..f2293cfe 100644
--- a/includes/hub/admin/class-membership-plans-table.php
+++ b/includes/hub/admin/class-membership-plans-table.php
@@ -22,16 +22,28 @@ class Membership_Plans_Table extends \WP_List_Table {
*/
public function get_columns() {
$columns = [
- 'id' => __( 'ID', 'newspack-network' ),
'name' => __( 'Name', 'newspack-network' ),
];
$columns['site_url'] = __( 'Site URL', 'newspack-network' );
$columns['network_pass_id'] = __( 'Network ID', 'newspack-network' );
if ( \Newspack_Network\Admin::use_experimental_auditing_features() ) {
- $columns['active_members_count'] = __( 'Active Members', 'newspack-network' );
- $columns['network_pass_discrepancies'] = __( 'Discrepancies', 'newspack-network' );
+ $columns['active_memberships_count'] = __( 'Active Memberships', 'newspack-network' );
+ $columns['network_pass_discrepancies'] = __( 'Membership Discrepancies', 'newspack-network' );
+
+ $active_subscriptions_sum = array_reduce(
+ $this->items,
+ function( $carry, $item ) {
+ return $carry + ( is_numeric( $item['active_subscriptions_count'] ) ? $item['active_subscriptions_count'] : 0 );
+ },
+ 0
+ );
+ $subs_info = sprintf(
+ ' ',
+ __( 'Active Subscriptions tied to this membership plan', 'newspack-network' )
+ );
+ // translators: %d is the sum of active subscriptions.
+ $columns['active_subscriptions_count'] = sprintf( __( 'Active Subscriptions (%d)', 'newspack-network' ), $active_subscriptions_sum ) . $subs_info;
}
- $columns['links'] = __( 'Links', 'newspack-network' );
return $columns;
}
@@ -39,8 +51,25 @@ public function get_columns() {
* Prepare items to be displayed
*/
public function prepare_items() {
- $this->_column_headers = [ $this->get_columns(), [], [], 'id' ];
- $this->items = Membership_Plans::get_membershp_plans_from_network();
+ $membership_plans_from_network_data = Membership_Plans::get_membership_plans_from_network();
+
+ // Handle table sorting.
+ $order = isset( $_REQUEST['order'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
+ $orderby = isset( $_REQUEST['orderby'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
+ if ( $order && $orderby ) {
+ usort(
+ $membership_plans_from_network_data['plans'],
+ function( $a, $b ) use ( $orderby, $order ) {
+ if ( $order === 'asc' ) {
+ return $a[ $orderby ] <=> $b[ $orderby ];
+ }
+ return $b[ $orderby ] <=> $a[ $orderby ];
+ }
+ );
+ }
+
+ $this->items = $membership_plans_from_network_data['plans'];
+ $this->_column_headers = [ $this->get_columns(), [], $this->get_sortable_columns(), 'id' ];
}
/**
@@ -53,6 +82,10 @@ public function prepare_items() {
public function column_default( $item, $column_name ) {
$memberships_list_url = sprintf( '%s/wp-admin/edit.php?s&post_status=wcm-active&post_type=wc_user_membership&post_parent=%d', $item['site_url'], $item['id'] );
+ if ( $column_name === 'name' ) {
+ $edit_url = sprintf( '%s/wp-admin/post.php?post=%d&action=edit', $item['site_url'], $item['id'] );
+ return sprintf( '%s', esc_url( $edit_url ), $item[ $column_name ] . ' (#' . $item['id'] . ')' );
+ }
if ( $column_name === 'network_pass_id' && $item[ $column_name ] ) {
return sprintf( '%s', $item[ $column_name ] );
}
@@ -65,7 +98,15 @@ public function column_default( $item, $column_name ) {
$memberships_list_url_with_emails_url = add_query_arg(
\Newspack_Network\Woocommerce_Memberships\Admin::MEMBERSHIPS_TABLE_EMAILS_QUERY_PARAM,
- implode( ',', $discrepancies ),
+ implode(
+ ',',
+ array_map(
+ function( $email_address ) {
+ return urlencode( $email_address );
+ },
+ $discrepancies
+ )
+ ),
$memberships_list_url
);
$message = sprintf(
@@ -80,13 +121,18 @@ public function column_default( $item, $column_name ) {
);
return sprintf( '%s', esc_url( $memberships_list_url_with_emails_url ), esc_html( $message ) );
}
- if ( $column_name === 'links' ) {
- $edit_url = sprintf( '%s/wp-admin/post.php?post=%d&action=edit', $item['site_url'], $item['id'] );
- return sprintf( '%s', esc_url( $edit_url ), esc_html__( 'Edit', 'newspack-network' ) );
- }
- if ( $column_name === 'active_members_count' && $item[ $column_name ] ) {
+ if ( $column_name === 'active_memberships_count' && isset( $item[ $column_name ] ) ) {
return sprintf( '%s', esc_url( $memberships_list_url ), $item[ $column_name ] );
}
return isset( $item[ $column_name ] ) ? $item[ $column_name ] : '';
}
+
+ /**
+ * Get sortable columns.
+ */
+ public function get_sortable_columns() {
+ return [
+ 'network_pass_id' => [ 'network_pass_id', false, __( 'Network Pass ID' ), __( 'Table ordered by Network Pass ID.' ) ],
+ ];
+ }
}
diff --git a/includes/hub/admin/class-membership-plans.php b/includes/hub/admin/class-membership-plans.php
index 6f4bc570..da252be3 100644
--- a/includes/hub/admin/class-membership-plans.php
+++ b/includes/hub/admin/class-membership-plans.php
@@ -55,7 +55,7 @@ public static function render() {
@@ -75,21 +75,20 @@ public static function render() {
* @param \Newspack_Network\Node\Node $node The node.
* @param string $collection_endpoint The collection endpoint.
* @param string $collection_endpoint_id The collection endpoint ID.
+ * @param array $query_args The query args.
*/
- public static function fetch_collection_from_api( $node, $collection_endpoint, $collection_endpoint_id ) {
- $endpoint = sprintf( '%s/wp-json/%s', $node->get_url(), $collection_endpoint );
- if ( Network_Admin::use_experimental_auditing_features() ) {
- $endpoint = add_query_arg( 'include_active_members_emails', 1, $endpoint );
- }
+ public static function fetch_collection_from_api( $node, $collection_endpoint, $collection_endpoint_id, $query_args = [] ) {
+ $endpoint = add_query_arg( $query_args, sprintf( '%s/wp-json/%s', $node->get_url(), $collection_endpoint ) );
$response = wp_remote_get( // phpcs:ignore
$endpoint,
[
'headers' => $node->get_authorization_headers( 'get-woo-' . $collection_endpoint_id ),
+ 'timeout' => 60, // phpcs:ignore
]
);
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
Debugger::log( 'API request for node\'s memberships failed' );
- return;
+ return null;
}
return json_decode( wp_remote_retrieve_body( $response ) );
}
@@ -104,27 +103,32 @@ private static function get_membership_plans_from_cache() {
/**
* Get membership plans from all nodes.
*/
- public static function get_membershp_plans_from_network() {
+ public static function get_membership_plans_from_network() {
$plans_cache = self::get_membership_plans_from_cache();
if ( $plans_cache && isset( $plans_cache['plans'] ) ) {
- return $plans_cache['plans'];
+ return $plans_cache;
}
$by_network_pass_id = [];
$membership_plans = [];
- if ( Network_Admin::use_experimental_auditing_features() ) {
- $local_membership_plans = self::get_local_membership_plans();
- foreach ( $local_membership_plans as $local_plan ) {
- if ( $local_plan['network_pass_id'] ) {
- $by_network_pass_id[ $local_plan['network_pass_id'] ][ $local_plan['site_url'] ] = $local_plan['active_members_emails'];
- }
+ $local_membership_plans = self::get_local_membership_plans();
+ foreach ( $local_membership_plans as $local_plan ) {
+ if ( $local_plan['network_pass_id'] ) {
+ $by_network_pass_id[ $local_plan['network_pass_id'] ][ $local_plan['site_url'] ] = $local_plan['active_members_emails'];
}
- $membership_plans = array_merge( $local_membership_plans, $membership_plans );
}
+ $membership_plans = array_merge( $local_membership_plans, $membership_plans );
$nodes = \Newspack_Network\Hub\Nodes::get_all_nodes();
foreach ( $nodes as $node ) {
- $node_plans = self::fetch_collection_from_api( $node, 'wc/v2/memberships/plans', 'membership-plans' );
+ $query_args = [];
+ if ( \Newspack_Network\Admin::use_experimental_auditing_features() ) {
+ $query_args['include_active_members_emails'] = 1;
+ }
+ $node_plans = self::fetch_collection_from_api( $node, 'wc/v2/memberships/plans', 'membership-plans', $query_args );
+ if ( $node_plans === null ) {
+ continue;
+ }
foreach ( $node_plans as $plan ) {
$network_pass_id = null;
foreach ( $plan->meta_data as $meta ) {
@@ -139,15 +143,17 @@ public static function get_membershp_plans_from_network() {
$by_network_pass_id[ $network_pass_id ][ $node->get_url() ] = $plan->active_members_emails;
}
$membership_plans[] = [
- 'id' => $plan->id,
- 'site_url' => $node->get_url(),
- 'name' => $plan->name,
- 'network_pass_id' => $network_pass_id,
- 'active_members_count' => $plan->active_members_count,
+ 'id' => $plan->id,
+ 'site_url' => $node->get_url(),
+ 'name' => $plan->name,
+ 'network_pass_id' => $network_pass_id,
+ 'active_memberships_count' => $plan->active_memberships_count,
+ 'active_subscriptions_count' => $plan->active_subscriptions_count,
];
}
}
+ $discrepancies_emails = [];
if ( Network_Admin::use_experimental_auditing_features() ) {
$discrepancies = [];
foreach ( $by_network_pass_id as $plan_network_pass_id => $by_site ) {
@@ -156,6 +162,15 @@ public static function get_membershp_plans_from_network() {
$discrepancies[ $plan_network_pass_id ][ $site_url ] = array_diff( $emails, $shared_emails );
}
}
+
+ // Get all emails which are discrepant across all sites.
+ foreach ( $discrepancies as $plan_network_id => $plan_discrepancies ) {
+ foreach ( $plan_discrepancies as $site_url => $plan_site_discrepancies ) {
+ $discrepancies_emails = array_merge( $discrepancies_emails, $plan_site_discrepancies );
+ }
+ }
+ $discrepancies_emails = array_unique( $discrepancies_emails );
+
$membership_plans = array_map(
function( $plan ) use ( $discrepancies ) {
if ( isset(
@@ -170,12 +185,13 @@ function( $plan ) use ( $discrepancies ) {
$membership_plans
);
}
- $plans_to_save = [
- 'plans' => $membership_plans,
- 'last_updated' => time(),
+ $memberships_data = [
+ 'plans' => $membership_plans,
+ 'discrepancies_emails' => $discrepancies_emails,
+ 'last_updated' => time(),
];
- update_option( self::OPTIONS_CACHE_KEY_PLANS, $plans_to_save );
- return $membership_plans;
+ update_option( self::OPTIONS_CACHE_KEY_PLANS, $memberships_data );
+ return $memberships_data;
}
/**
@@ -187,15 +203,21 @@ public static function get_local_membership_plans() {
return [];
}
foreach ( wc_memberships_get_membership_plans() as $plan ) {
+ $network_pass_id = get_post_meta( $plan->post->ID, \Newspack_Network\Woocommerce_Memberships\Admin::NETWORK_ID_META_KEY, true );
$plan_data = [
- 'id' => $plan->post->ID,
- 'site_url' => get_site_url(),
- 'name' => $plan->post->post_title,
- 'network_pass_id' => get_post_meta( $plan->post->ID, \Newspack_Network\Woocommerce_Memberships\Admin::NETWORK_ID_META_KEY, true ),
- 'active_members_count' => $plan->get_memberships_count( 'active' ),
+ 'id' => $plan->post->ID,
+ 'site_url' => get_site_url(),
+ 'name' => $plan->post->post_title,
+ 'network_pass_id' => $network_pass_id,
+ 'active_memberships_count' => $plan->get_memberships_count( 'active' ),
];
if ( Network_Admin::use_experimental_auditing_features() ) {
$plan_data['active_members_emails'] = \Newspack_Network\Woocommerce_Memberships\Admin::get_active_members_emails( $plan );
+ if ( $network_pass_id ) {
+ $plan_data['active_subscriptions_count'] = \Newspack_Network\Woocommerce_Memberships\Admin::get_plan_related_active_subscriptions( $plan );
+ } else {
+ $plan_data['active_subscriptions_count'] = __( 'Only displayed for plans with a Network ID.', 'newspack-network' );
+ }
}
$membership_plans[] = $plan_data;
}
diff --git a/includes/woocommerce-memberships/class-admin.php b/includes/woocommerce-memberships/class-admin.php
index 29178114..4ea5cf16 100644
--- a/includes/woocommerce-memberships/class-admin.php
+++ b/includes/woocommerce-memberships/class-admin.php
@@ -66,6 +66,7 @@ public static function init() {
add_filter( 'post_row_actions', array( __CLASS__, 'post_row_actions' ), 99, 2 ); // After the Memberships plugin.
add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_cap' ), 20, 4 );
add_filter( 'wc_memberships_rest_api_membership_plan_data', [ __CLASS__, 'add_data_to_membership_plan_response' ], 2, 3 );
+ add_filter( 'woocommerce_rest_prepare_wc_user_membership', [ __CLASS__, 'add_data_to_wc_user_membership_response' ], 2, 3 );
add_filter( 'request', [ __CLASS__, 'request_query' ] );
add_action( 'pre_user_query', [ __CLASS__, 'pre_user_query' ] );
add_action( 'admin_notices', [ __CLASS__, 'admin_notices' ] );
@@ -100,14 +101,47 @@ function ( $membership ) {
*/
public static function add_data_to_membership_plan_response( $data, $plan, $request ) {
if ( $request && isset( $request->get_headers()['x_np_network_signature'] ) ) {
- $data['active_members_count'] = $plan->get_memberships_count( 'active' );
- if ( $request->get_param( 'include_active_members_emails' ) ) {
- $data['active_members_emails'] = self::get_active_members_emails( $plan );
+ $data['active_memberships_count'] = $plan->get_memberships_count( 'active' );
+ $network_pass_id = get_post_meta( $plan->id, self::NETWORK_ID_META_KEY, true );
+ if ( $network_pass_id && $request->get_param( 'include_active_members_emails' ) ) {
+ $data['active_subscriptions_count'] = self::get_plan_related_active_subscriptions( $plan );
+ $data['active_members_emails'] = array_values( array_unique( self::get_active_members_emails( $plan ) ) );
+ } else {
+ $data['active_subscriptions_count'] = __( 'Only displayed for plans with a Network ID.', 'newspack-network' );
}
}
return $data;
}
+ /**
+ * Get the active subscriptions related to a membership plan.
+ *
+ * @param \WC_Memberships_Membership_Plan $plan The membership plan.
+ */
+ public static function get_plan_related_active_subscriptions( $plan ) {
+ $product_ids = $plan->get_product_ids();
+ $subscriptions = wcs_get_subscriptions_for_product( $product_ids, 'ids', [ 'subscription_status' => 'active' ] );
+ return count( $subscriptions );
+ }
+
+ /**
+ * Filter user membership data from REST API.
+ *
+ * @param \WP_REST_Response $response the response object.
+ * @param null|\WP_Post $user the user membership post object.
+ * @param \WP_REST_Request $request the request object.
+ */
+ public static function add_data_to_wc_user_membership_response( $response, $user, $request ) {
+ if ( $request && isset( $request->get_headers()['x_np_network_signature'] ) ) {
+ // Add network plan ID to the response.
+ $plan = wc_memberships_get_membership_plan( $response->data['plan_id'] );
+ if ( $plan !== false ) {
+ $response->data['plan_network_id'] = get_post_meta( $plan->id, self::NETWORK_ID_META_KEY, true );
+ }
+ }
+ return $response;
+ }
+
/**
* Adds a meta box to the membership plan edit screen.
*/