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

handle active subs from other nodes - as a hotfix #118

Merged
merged 1 commit into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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;
}
}