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

feat(subscriptions): limit network-sync'd subscriptions buying #112

Closed
wants to merge 1 commit into from
Closed
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 @@ -26,6 +26,7 @@ public static function init() {
Hub\Pull_Endpoint::init();
Hub\Event_Listeners::init();
Hub\Newspack_Ads_GAM::init();
Hub\Network_Data_Endpoint::init();
Hub\Connect_Node::init();
}

Expand Down Expand Up @@ -56,6 +57,7 @@ public static function init() {
Woocommerce_Memberships\Admin::init();
Woocommerce_Memberships\Events::init();
Woocommerce_Subscriptions\Admin::init();
Woocommerce_Subscriptions\Limiter::init();

register_activation_hook( NEWSPACK_NETWORK_PLUGIN_FILE, [ __CLASS__, 'activation_hook' ] );
}
Expand Down
23 changes: 10 additions & 13 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,25 +46,23 @@ 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 ) {
$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.
// Skip the node which is making the request.
continue;
}
$active_subscriptions_ids = array_merge(
$active_subscriptions_ids,
$node->get_subscriptions_with_network_plan( $email, $plan_network_id )
);
$node_subscription_ids = $node->get_subscriptions_with_network_plans( $email, $plan_network_ids );
$active_subscriptions_ids = array_merge( $active_subscriptions_ids, $node_subscription_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 )
\Newspack_Network\Utils\Users::get_users_active_subscriptions_tied_to_network_ids( $email, $plan_network_ids )
);
return $active_subscriptions_ids;
}
Expand All @@ -80,13 +78,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
23 changes: 23 additions & 0 deletions includes/hub/class-node.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,27 @@ public function get_site_info() {
);
return json_decode( wp_remote_retrieve_body( $response ) );
}

/**
* Get all subscriptions.
*
* @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 ) );
}
}
36 changes: 35 additions & 1 deletion includes/node/class-info-endpoints.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,32 @@ public static function register_routes() {
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ __CLASS__, 'handle_info_request' ],
'permission_callback' => '__return_true',
'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 @@ -54,4 +76,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'] )
);
}
}
39 changes: 39 additions & 0 deletions includes/utils/class-users.php
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,43 @@ 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( 'wc_memberships_get_user_memberships' ) || ! function_exists( 'wcs_get_subscription' ) ) {
return [];
}
$active_subscription_ids = [];
$user = get_user_by( 'email', $email );
if ( ! $user ) {
return [];
}
$memberships = wc_memberships_get_user_memberships( $user->ID );
foreach ( $memberships as $membership ) {
$membership_plan_network_id = get_post_meta( $membership->get_plan()->get_id(), \Newspack_Network\Woocommerce_Memberships\Admin::NETWORK_ID_META_KEY, true );
if ( ! in_array( $membership_plan_network_id, $plan_network_ids ) ) {
continue;
}
$wcm_wcs_integration = wc_memberships()->get_integrations_instance()->get_subscriptions_instance();
if ( ! $wcm_wcs_integration ) {
continue;
}
$subscription_id = $wcm_wcs_integration->get_user_membership_subscription_id( $membership->get_id() );
if ( ! $subscription_id ) {
continue;
}
$subscription = wcs_get_subscription( $subscription_id );
$subscription_status = $subscription ? $subscription->get_status() : null;
if ( $subscription_status === 'active' ) {
$active_subscription_ids[] = $subscription_id;
}
}
return $active_subscription_ids;
}
}
4 changes: 2 additions & 2 deletions includes/woocommerce-subscriptions/class-admin.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<?php
/**
* Newspack Network Admin customizations for woocommerce subscriptions.
* Newspack Network Admin customizations for WooCommerce Subscriptions.
*
* @package Newspack
*/

namespace Newspack_Network\Woocommerce_Subscriptions;

/**
* Handles admin tweaks for woocommerce subscriptions.
* Handles admin tweaks for WooCommerce Subscriptions.
*
* Adds a metabox to the membership plan edit screen to allow the user to add a network id metadata to the plans
*/
Expand Down
124 changes: 124 additions & 0 deletions includes/woocommerce-subscriptions/class-limiter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php
/**
* Newspack Network limiter for WooCommerce Subscriptions.
*
* @package Newspack
*/

namespace Newspack_Network\Woocommerce_Subscriptions;

/**
* Handles limiting for WooCommerce Subscriptions - only one
*/
class Limiter {
/**
* Cache for restriction checking results.
*
* @var array
*/
private static $cache = [];

/**
* Initializer.
*/
public static function init() {
add_filter( 'woocommerce_subscription_is_purchasable', [ __CLASS__, 'restrict_network_subscriptions' ], 10, 2 );
add_filter( 'woocommerce_cart_product_cannot_be_purchased_message', [ __CLASS__, 'woocommerce_cart_product_cannot_be_purchased_message' ], 10, 2 );
}

/**
* Restricts subscription purchasing from a network-synchronized plan to one.
*
* @param bool $purchasable Whether the subscription product is purchasable.
* @param \WC_Product_Subscription|\WC_Product_Subscription_Variation $subscription_product The subscription product.
* @return bool
*/
public static function restrict_network_subscriptions( $purchasable, $subscription_product ) {
return self::can_buy_subscription( $subscription_product ) ? $purchasable : false;
}

/**
* Verify if this subscription can be bought.
*
* @param \WC_Product_Subscription|\WC_Product_Subscription_Variation $subscription_product The subscription product.
*/
public static function can_buy_subscription( $subscription_product ) {
$cache_key = $subscription_product->get_id();
if ( isset( self::$cache[ $cache_key ] ) ) {
return self::$cache[ $cache_key ];
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
self::$cache[ $cache_key ] = true;
return self::$cache[ $cache_key ];
}

// Get the membership plan related to the subscription.
$plans = self::get_plans_from_subscription_product( $subscription_product );
if ( empty( $plans ) ) {
self::$cache[ $cache_key ] = true;
return self::$cache[ $cache_key ];
}
$user_email = get_userdata( $user_id )->user_email;
$params = [
'email' => $user_email,
'plan_network_ids' => array_column( $plans, 'network_pass_id' ),
'site' => get_bloginfo( 'url' ),
];
$response = \Newspack_Network\Utils\Requests::request_to_hub( 'wp-json/newspack-network/v1/network-subscriptions', $params, 'GET' );
$response_code = wp_remote_retrieve_response_code( $response );
if ( $response_code !== 200 ) {
self::$cache[ $cache_key ] = true;
return self::$cache[ $cache_key ];
}
$response_data = json_decode( wp_remote_retrieve_body( $response ) );
if ( isset( $response_data->active_subscriptions_ids ) && count( $response_data->active_subscriptions_ids ) > 0 ) {
self::$cache[ $cache_key ] = false;
return self::$cache[ $cache_key ];
}
self::$cache[ $cache_key ] = true;
return self::$cache[ $cache_key ];
}

/**
* Get the plan related to the subscription product.
*
* @param WC_Product $product Product data.
*/
public static function get_plans_from_subscription_product( $product ) {
$membership_plans = [];
if ( ! function_exists( 'wc_memberships_get_membership_plans' ) ) {
return [];
}
$plans = array_filter(
wc_memberships_get_membership_plans(),
function( $plan ) use ( $product ) {
return in_array( $product->get_id(), $plan->get_product_ids() );
}
);
return array_map(
function( $plan ) {
return [
'id' => $plan->get_id(),
'network_pass_id' => get_post_meta( $plan->post->ID, \Newspack_Network\Woocommerce_Memberships\Admin::NETWORK_ID_META_KEY, true ),
];
},
$plans
);
}

/**
* Filters the error message shown when a product can't be added to the cart.
*
* @param string $message Message.
* @param WC_Product $product_data Product data.
*
* @return string
*/
public static function woocommerce_cart_product_cannot_be_purchased_message( $message, $product_data ) {
if ( ! self::can_buy_subscription( $product_data ) ) {
return __( 'You can only purchase one subscription in this network at a time.', 'newspack-network' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Instead of just saying they can't buy the subscription, we could say that they already have that subscription, because they bought it in site X

}
return $message;
}
}