Skip to content

Commit

Permalink
fix(memberships-sync): handle active subs from other nodes (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
adekbadek authored Jul 25, 2024
1 parent d1e9533 commit 97f9a58
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 25 deletions.
2 changes: 2 additions & 0 deletions includes/class-initializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static function init() {
Hub\Nodes::init();
Hub\Webhook::init();
Hub\Pull_Endpoint::init();
Hub\Network_Data_Endpoint::init();
Hub\Event_Listeners::init();
Hub\Newspack_Ads_GAM::init();
Hub\Connect_Node::init();
Expand Down Expand Up @@ -55,6 +56,7 @@ public static function init() {

Woocommerce_Memberships\Admin::init();
Woocommerce_Memberships\Events::init();
Woocommerce_Memberships\Subscriptions_Integration::init();
Woocommerce_Subscriptions\Admin::init();

register_activation_hook( NEWSPACK_NETWORK_PLUGIN_FILE, [ __CLASS__, 'activation_hook' ] );
Expand Down
31 changes: 16 additions & 15 deletions includes/hub/class-network-data-endpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static function register_routes() {
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ __CLASS__, 'api_get_network_subscriptions' ],
'permission_callback' => '__return_true',
'permission_callback' => '__return_true', // Auth will be handled by \Newspack_Network\Utils\Requests::get_request_to_hub_errors.
],
]
);
Expand All @@ -46,26 +46,28 @@ public static function register_routes() {
* Get active subscription IDs from the network.
*
* @param string $email Email of the user.
* @param string $plan_network_id Network ID of the plan.
* @param string $plan_network_ids Network ID of the plan.
* @param string $site Site URL.
*/
public static function get_active_subscription_ids_from_network( $email, $plan_network_id, $site ) {
public static function get_active_subscription_ids_from_network( $email, $plan_network_ids, $site = false ) {
$active_subscriptions_ids = [];
foreach ( Nodes::get_all_nodes() as $node ) {
if ( $site === $node->get_url() ) {
// Skip the node which is making the request. It's only interested in the other nodes.
if ( $site && $site === $node->get_url() ) {
// Skip the node which is making the request.
continue;
}
$node_subscription_ids = $node->get_subscriptions_with_network_plans( $email, $plan_network_ids );
if ( is_array( $node_subscription_ids ) ) {
$active_subscriptions_ids = array_merge( $active_subscriptions_ids, $node_subscription_ids );
}
}
// Also look on the Hub itself, unless the $site was provided.
if ( $site !== false ) {
$active_subscriptions_ids = array_merge(
$active_subscriptions_ids,
$node->get_subscriptions_with_network_plan( $email, $plan_network_id )
\Newspack_Network\Utils\Users::get_users_active_subscriptions_tied_to_network_ids( $email, $plan_network_ids )
);
}
// Also look on the Hub itself.
$active_subscriptions_ids = array_merge(
$active_subscriptions_ids,
\Newspack_Network\Utils\Users::get_users_active_subscriptions_tied_to_network_id( $email, $plan_network_id )
);
return $active_subscriptions_ids;
}

Expand All @@ -80,13 +82,12 @@ public static function api_get_network_subscriptions( $request ) {
if ( \is_wp_error( $request_error ) ) {
return new WP_REST_Response( [ 'error' => $request_error->get_error_message() ], 403 );
}
if ( ! isset( $request['plan_network_id'] ) || empty( $request['plan_network_id'] ) ) {
return new WP_REST_Response( [ 'error' => __( 'Missing plan_network_id', 'newspack-network' ) ], 400 );
if ( ! isset( $request['plan_network_ids'] ) || empty( $request['plan_network_ids'] ) ) {
return new WP_REST_Response( [ 'error' => __( 'Missing plan_network_ids', 'newspack-network' ) ], 400 );
}

return new WP_REST_Response(
[
'active_subscriptions_ids' => self::get_active_subscription_ids_from_network( $request['email'], $request['plan_network_id'], $request['site'] ),
'active_subscriptions_ids' => self::get_active_subscription_ids_from_network( $request['email'], $request['plan_network_ids'], $request['site'] ),
]
);
}
Expand Down
26 changes: 25 additions & 1 deletion includes/hub/class-node.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use WP_Post;

/**
* Class to represent one Node of the netowrk
* Class to represent a Node in the network
*/
class Node {
/**
Expand Down Expand Up @@ -179,6 +179,30 @@ public function get_site_info() {
$this->get_url() . '/wp-json/newspack-network/v1/info',
[
'headers' => $this->get_authorization_headers( 'info' ),
'timeout' => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
]
);
return json_decode( wp_remote_retrieve_body( $response ) );
}

/**
* Get all subscriptions of a user, related to provided network plan IDs.
*
* @param string $email The email to get subscriptions for.
* @param string $plan_network_ids The plan network ID to get subscriptions for.
*/
public function get_subscriptions_with_network_plans( $email, $plan_network_ids ) {
$response = wp_remote_get( // phpcs:ignore
add_query_arg(
[
'email' => $email,
'plan_network_ids' => $plan_network_ids,
],
$this->get_url() . '/wp-json/newspack-network/v1/subscriptions'
),
[
'headers' => $this->get_authorization_headers( 'subscriptions' ),
'timeout' => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
]
);
return json_decode( wp_remote_retrieve_body( $response ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ public function update_membership() {
User_Update_Watcher::$enabled = false;

$user = User_Utils::get_or_create_user_by_email( $email, $this->get_site(), $this->data->user_id ?? '' );
if ( ! $user ) {
Debugger::log( 'User not found.' );
return;
}

$user_membership = wc_memberships_get_user_membership( $user->ID, $local_plan_id );

Expand Down Expand Up @@ -108,7 +112,7 @@ public function update_membership() {
)
);

Debugger::log( 'User membership updated' );
Debugger::log( 'User membership updated.' );
}

/**
Expand Down
38 changes: 35 additions & 3 deletions includes/node/class-info-endpoints.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,32 @@ public static function register_routes() {
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ __CLASS__, 'handle_info_request' ],
'permission_callback' => function( $request ) {
return \Newspack_Network\Rest_Authenticaton::verify_signature( $request, 'info', Settings::get_secret_key() );
},
'permission_callback' => [ __CLASS__, 'permission_callback' ],
],
]
);
register_rest_route(
'newspack-network/v1',
'/subscriptions',
[
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ __CLASS__, 'handle_subscriptions_request' ],
'permission_callback' => [ __CLASS__, 'permission_callback' ],
],
]
);
}

/**
* The permission callback.
*
* @param WP_REST_Request $request Full data about the request.
*/
public static function permission_callback( $request ) {
$route = $request->get_route();
$request_slug = substr( $route, strrpos( $route, '/' ) + 1 );
return \Newspack_Network\Rest_Authenticaton::verify_signature( $request, $request_slug, Settings::get_secret_key() );
}

/**
Expand All @@ -53,4 +73,16 @@ public static function handle_info_request() {
]
);
}

/**
* Handles the subscriptions request.
* Will return the active subscription IDs for the given email, when matching a membership by plan network ID.
*
* @param WP_REST_Request $request Full data about the request.
*/
public static function handle_subscriptions_request( $request ) {
return rest_ensure_response(
\Newspack_Network\Utils\Users::get_users_active_subscriptions_tied_to_network_ids( $request['email'], $request['plan_network_ids'] )
);
}
}
49 changes: 49 additions & 0 deletions includes/utils/class-users.php
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,53 @@ public static function get_not_synchronized_users_emails() {
public static function get_not_synchronized_users_count() {
return count( self::get_not_synchronized_users( [ 'id' ] ) );
}

/**
* Get user's active subscriptions tied to a network ID.
*
* @param string $email The email of the user to look for.
* @param array $plan_network_ids Network IDs of the plans.
* @return array Array of active subscription IDs.
*/
public static function get_users_active_subscriptions_tied_to_network_ids( $email, $plan_network_ids ) {
if ( ! function_exists( 'wcs_get_subscriptions' ) ) {
return [];
}
$user = get_user_by( 'email', $email );
if ( ! $user ) {
return [];
}
// If a membership is a "shadowed" membership, no subscription will be tied to it.
// The relevant subscription has to be found by matching the plan-granting products with subscriptions.
$plan_ids = get_posts(
[
'meta_key' => \Newspack_Network\Woocommerce_Memberships\Admin::NETWORK_ID_META_KEY,
'meta_value' => $plan_network_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
'meta_compare' => 'IN',
'post_type' => \Newspack_Network\Woocommerce_Memberships\Admin::MEMBERSHIPS_CPT,
'field' => 'ID',
]
);
$product_ids = [];
foreach ( $plan_ids as $plan_id ) {
// Get the products that grant membership in the plan.
$product_ids = array_merge( $product_ids, get_post_meta( $plan_id->ID, '_product_ids', true ) );
}
// Get any active subscriptions for these product IDs.
$active_subscription_ids = [];
foreach ( $product_ids as $product_id ) {
$args = [
'customer_id' => $user->ID,
'product_id' => $product_id,
'subscription_status' => 'active',
'subscriptions_per_page' => 1,
];
$subscriptions = \wcs_get_subscriptions( $args );
$subscription = reset( $subscriptions );
if ( $subscription ) {
$active_subscription_ids[] = $subscription->get_id();
}
}
return $active_subscription_ids;
}
}
6 changes: 2 additions & 4 deletions includes/woocommerce-memberships/class-admin.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
<?php
/**
* Newspack Network Admin customizations for woocommerce memberships.
* Newspack Network Admin customizations for WooCommerce Memberships.
*
* @package Newspack
*/

namespace Newspack_Network\Woocommerce_Memberships;

/**
* Handles admin tweaks for woocommerce memberships.
*
* Adds a metabox to the membership plan edit screen to allow the user to add a network id metadata to the plans
* Handles admin tweaks for WooCommerce Memberships.
*/
class Admin {

Expand Down
2 changes: 1 addition & 1 deletion includes/woocommerce-memberships/class-events.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Newspack Network Woo Membership events
* Newspack Network WooCommerce Memberships events
*
* @package Newspack
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/**
* Newspack Network WooCommerce Subscriptions integration for WooCommerce Memberships.
*
* @package Newspack
*/

namespace Newspack_Network\Woocommerce_Memberships;

/**
* Handles tweaks for WooCommerce Memberships WooCommerce Subscriptions integration.
*/
class Subscriptions_Integration {
/**
* Runs the initialization.
*
* @return void
*/
public static function init() {
// You'd think this first filter is enough, but it's not. Even if the membership cancellation via linked subscription
// is prevented, the expiry code is also executed.
add_filter( 'wc_memberships_cancel_subscription_linked_membership', [ __CLASS__, 'prevent_membership_expiration' ], 10, 2 );
add_filter( 'wc_memberships_expire_user_membership', [ __CLASS__, 'prevent_membership_expiration' ], 10, 2 );
}

/**
* Prevent membership expiration if another network site has a synced membership active.
*
* @param bool $cancel_or_expire whether to cancel/expire the membership when the subscription is cancelled (default true).
* @param \WC_Memberships_Integration_Subscriptions_User_Membership $user_membership the subscription-tied membership.
*/
public static function prevent_membership_expiration( $cancel_or_expire, $user_membership ) {
$user_email = get_userdata( $user_membership->user_id )->user_email;
$membership_plan_id = get_post_meta( $user_membership->get_plan()->get_id(), Admin::NETWORK_ID_META_KEY, true );

if ( \Newspack_Network\Site_Role::is_hub() ) {
$active_subscriptions_ids = \Newspack_Network\Hub\Network_Data_Endpoint::get_active_subscription_ids_from_network(
$user_email,
[ $membership_plan_id ]
);
} else {
$params = [
'site' => get_bloginfo( 'url' ),
'plan_network_ids' => [ $membership_plan_id ],
'email' => $user_email,
];
$response = \Newspack_Network\Utils\Requests::request_to_hub( 'wp-json/newspack-network/v1/network-subscriptions', $params, 'GET' );
if ( is_wp_error( $response ) ) {
return $cancel_or_expire;
}
$active_subscriptions_ids = json_decode( wp_remote_retrieve_body( $response ) )->active_subscriptions_ids ?? [];
}
$can_cancel = empty( $active_subscriptions_ids );
if ( ! $can_cancel ) {
$user_membership->add_note(
__( 'Membership is not cancelled, because there is at least one active subscription linked to the membership plan on the network.', 'newspack-plugin' )
);
}
return $can_cancel ? $cancel_or_expire : false;
}
}

0 comments on commit 97f9a58

Please sign in to comment.