Skip to content

Commit

Permalink
fix: force active membership if the user has active subs (#3268)
Browse files Browse the repository at this point in the history
* fix: force active membership if the user has active subs

* fix: match memberships product ownership logic (OR, not AND)

* fix: logical error

* fix: logical error

* fix: incorrect condition

* fix: feedback from code review
  • Loading branch information
dkoo authored Aug 1, 2024
1 parent e47e422 commit aebe581
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 27 deletions.
96 changes: 77 additions & 19 deletions includes/plugins/wc-memberships/class-memberships.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace Newspack;

use Newspack\Memberships\Metering;
use Newspack\WooCommerce_Connection;

defined( 'ABSPATH' ) || exit;

Expand Down Expand Up @@ -56,6 +57,7 @@ public static function init() {
add_filter( 'newspack_popups_assess_has_disabled_popups', [ __CLASS__, 'disable_popups' ] );
add_filter( 'newspack_reader_activity_article_view', [ __CLASS__, 'suppress_article_view_activity' ], 100 );
add_filter( 'user_has_cap', [ __CLASS__, 'user_has_cap' ], 10, 3 );
add_filter( 'get_post_status', [ __CLASS__, 'check_membership_status' ], 10, 2 );
add_action( 'wp', [ __CLASS__, 'remove_unnecessary_content_restriction' ], 11 );

/** Add gate content filters to mimic 'the_content'. See 'wp-includes/default-filters.php' for reference. */
Expand All @@ -78,6 +80,13 @@ public static function init() {
include __DIR__ . '/class-metering.php';
}

/**
* Check if Memberships is available.
*/
public static function is_active() {
return class_exists( 'WC_Memberships' ) && function_exists( 'wc_memberships' );
}

/**
* Parses dynamic blocks out of `post_content` and re-renders them.
*
Expand Down Expand Up @@ -321,7 +330,7 @@ private static function get_plan_gate_id( $plan_id ) {
* @return string[] Plan names keyed by plan ID.
*/
private static function get_gate_plans( $gate_id ) {
if ( ! function_exists( 'wc_memberships_get_membership_plan' ) ) {
if ( ! self::is_active() || ! function_exists( 'wc_memberships_get_membership_plan' ) ) {
return [];
}
$ids = get_post_meta( $gate_id, 'plans', true );
Expand All @@ -344,7 +353,7 @@ private static function get_gate_plans( $gate_id ) {
* @return array
*/
public static function get_plans() {
if ( ! function_exists( 'wc_memberships_get_membership_plans' ) ) {
if ( ! self::is_active() || ! function_exists( 'wc_memberships_get_membership_plans' ) ) {
return [];
}
$membership_plans = wc_memberships_get_membership_plans();
Expand Down Expand Up @@ -415,7 +424,7 @@ private static function current_user_has_plan( $plan_id ) {
if ( ! \is_user_logged_in() ) {
return false;
}
if ( ! function_exists( 'wc_memberships_is_user_active_or_delayed_member' ) ) {
if ( ! self::is_active() || ! function_exists( 'wc_memberships_is_user_active_or_delayed_member' ) ) {
return false;
}
return \wc_memberships_is_user_active_or_delayed_member( \get_current_user_id(), $plan_id );
Expand Down Expand Up @@ -480,7 +489,7 @@ public static function is_post_restricted( $post_id = null ) {
if ( ! $post_id ) {
$post_id = get_the_ID();
}
if ( ! function_exists( 'wc_memberships_is_post_content_restricted' ) || ! \wc_memberships_is_post_content_restricted( $post_id ) ) {
if ( ! self::is_active() || ! function_exists( 'wc_memberships_is_post_content_restricted' ) || ! \wc_memberships_is_post_content_restricted( $post_id ) ) {
return false;
}
return ! is_user_logged_in() || ! current_user_can( 'wc_memberships_view_restricted_post_content', $post_id ); // phpcs:ignore WordPress.WP.Capabilities.Unknown
Expand Down Expand Up @@ -795,14 +804,26 @@ private static function can_manage_woocommerce( $caps ) {
*/
public static function user_has_cap( $all_caps, $caps, $args ) {
// Bail if Woo Memberships is not active.
if ( ! class_exists( 'WC_Memberships' ) || ! function_exists( 'wc_memberships' ) ) {
if ( ! self::is_active() ) {
return $all_caps;
}

if ( ! empty( $caps ) ) {
foreach ( $caps as $cap ) {

switch ( $cap ) {
case 'wc_memberships_access_all_restricted_content':
case 'wc_memberships_view_restricted_product':
case 'wc_memberships_purchase_restricted_product':
case 'wc_memberships_view_restricted_product_taxonomy_term':
case 'wc_memberships_view_delayed_product_taxonomy_term':
case 'wc_memberships_view_restricted_taxonomy_term':
case 'wc_memberships_view_restricted_taxonomy':
case 'wc_memberships_view_restricted_post_type':
case 'wc_memberships_view_delayed_post_type':
case 'wc_memberships_view_delayed_taxonomy':
case 'wc_memberships_view_delayed_taxonomy_term':
case 'wc_memberships_view_delayed_post_content':
case 'wc_memberships_view_restricted_post_content':
if ( self::can_manage_woocommerce( $all_caps ) ) {
$all_caps[ $cap ] = true;
Expand All @@ -815,6 +836,10 @@ public static function user_has_cap( $all_caps, $caps, $args ) {
break;
}

if ( ! isset( $args[1] ) || ! isset( $args[2] ) ) {
break;
}

$user_id = (int) $args[1];
$post_id = (int) $args[2];

Expand All @@ -828,20 +853,8 @@ public static function user_has_cap( $all_caps, $caps, $args ) {

break;

case 'wc_memberships_access_all_restricted_content':
case 'wc_memberships_view_restricted_product':
case 'wc_memberships_purchase_restricted_product':
case 'wc_memberships_view_restricted_product_taxonomy_term':
case 'wc_memberships_view_delayed_product_taxonomy_term':
case 'wc_memberships_view_restricted_taxonomy_term':
case 'wc_memberships_view_restricted_taxonomy':
case 'wc_memberships_view_restricted_post_type':
case 'wc_memberships_view_delayed_post_type':
case 'wc_memberships_view_delayed_taxonomy':
case 'wc_memberships_view_delayed_taxonomy_term':
case 'wc_memberships_view_delayed_post_content':
case 'wc_memberships_view_delayed_product':
// Allow user who can edit posts (by default: editors, authors, contributors).
// Allow users who can edit posts (by default: editors, authors, contributors).
if ( isset( $all_caps['edit_posts'] ) && true === $all_caps['edit_posts'] ) {
$all_caps[ $cap ] = true;
break;
Expand Down Expand Up @@ -876,18 +889,27 @@ private static function user_has_content_access_from_rules( $user_id, array $rul
return true;
}

$integrations = wc_memberships()->get_integrations_instance();
$integration = $integrations ? $integrations->get_subscriptions_instance() : null;
$require_all_plans = self::get_require_all_plans_setting();
$has_access = false;
$has_subscription = false;

foreach ( $rules as $rule ) {
$membership_plan_id = $rule->get_membership_plan_id();
if ( $integration && $integration->has_membership_plan_subscription( $membership_plan_id ) ) {
$subscription_plan = new \WC_Memberships_Integration_Subscriptions_Membership_Plan( $membership_plan_id );
$required_products = $subscription_plan->get_subscription_product_ids();
$has_subscription = ! empty( WooCommerce_Connection::get_active_subscriptions_for_user( $user_id, $required_products ) );
}

// If no object ID is provided, then we are looking at rules that apply to whole post types or taxonomies.
// In this case, rules that apply to specific objects should be skipped.
if ( empty( $object_id ) && $rule->has_objects() ) {
continue;
}

if ( wc_memberships_is_user_active_or_delayed_member( $user_id, $rule->get_membership_plan_id() ) ) {
if ( $has_subscription || wc_memberships_is_user_active_or_delayed_member( $user_id, $rule->get_membership_plan_id() ) ) {
$has_access = true;
if ( ! $require_all_plans ) {
break;
Expand All @@ -901,6 +923,41 @@ private static function user_has_content_access_from_rules( $user_id, array $rul
return $has_access;
}

/**
* Check if a user has an active subscription with the required products when checking membership status.
* If they have an active subscription, reset inactive memberships to active link to the active subscription.
*
* @param string $post_status Post status.
* @param WP_Post $post Post object.
*
* @return string
*/
public static function check_membership_status( $post_status, $post ) {
if ( 'wc_user_membership' !== $post->post_type || 'wcm-active' === $post->post_status || ! self::is_active() || ! function_exists( 'wc_memberships_get_user_membership' ) ) {
return $post_status;
}
$integrations = wc_memberships()->get_integrations_instance();
$integration = $integrations ? $integrations->get_subscriptions_instance() : null;
$membership = wc_memberships_get_user_membership( $post->ID );
$plan_id = $membership->get_plan_id();
if ( $integration && $integration->has_membership_plan_subscription( $plan_id ) ) {
$subscription_plan = new \WC_Memberships_Integration_Subscriptions_Membership_Plan( $plan_id );
$required_products = $subscription_plan->get_subscription_product_ids();
$active_subscriptions = WooCommerce_Connection::get_active_subscriptions_for_user( $membership->get_user_id(), $required_products );
$has_subscription = ! empty( $active_subscriptions );
if ( $has_subscription ) {
$post_status = 'wcm-active';
$membership = new \WC_Memberships_Integration_Subscriptions_User_Membership( $post->ID );
$membership->unschedule_expiration_events();
$membership->set_subscription_id( $active_subscriptions[0] );
$membership->set_end_date(); // Clear the end date.
$membership->update_status( 'active' );
}
}

return $post_status;
}

/**
* Deactivate the cron job.
*/
Expand Down Expand Up @@ -952,6 +1009,7 @@ public static function fix_expired_memberships_for_active_subscriptions() {
foreach ( $memberships as $membership ) {
// If the membership is not active and has an end date in the past, reactivate it.
if ( $membership && ! $membership->has_status( $active_membership_statuses ) && $membership->has_end_date() && $membership->get_end_date( 'timestamp' ) < time() ) {
$membership->unschedule_expiration_events();
$membership->set_end_date(); // Clear the end date.
$membership->update_status( 'active' ); // Reactivate the membership.
$reactivated_memberships++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,41 @@ public static function get_batch_of_active_subscriptions( $batch_size = 100, $of
return ! empty( $subscriptions ) ? array_values( $subscriptions ) : false;
}

/**
* Does the given user have any subscriptions with an active status?
* Can optionally pass an array of product IDs. If given, only subscriptions
* that have at least one of the given product IDs will be returned.
*
* @param int $user_id User ID.
* @param array $product_ids Optional array of product IDs to filter by.
*
* @return int[] Array of active subscription IDs.
*/
public static function get_active_subscriptions_for_user( $user_id, $product_ids = [] ) {
$subcriptions = array_reduce(
array_keys( \wcs_get_users_subscriptions( $user_id ) ),
function( $acc, $subscription_id ) use ( $product_ids ) {
$subscription = \wcs_get_subscription( $subscription_id );
if ( $subscription->has_status( self::ACTIVE_SUBSCRIPTION_STATUSES ) ) {
if ( ! empty( $product_ids ) ) {
foreach ( $product_ids as $product_id ) {
if ( $subscription->has_product( $product_id ) ) {
$acc[] = $subscription_id;
return $acc;
}
}
} else {
$acc[] = $subscription_id;
}
}
return $acc;
},
[]
);

return $subcriptions;
}

/**
* Get the most recent active subscription, or the last successful order for a given customer.
*
Expand All @@ -160,20 +195,18 @@ public static function get_last_successful_order( $customer ) {
return false;
}

$user_id = $customer->get_id();

// Prioritize any currently active subscriptions.
$user_subscriptions = \wcs_get_users_subscriptions( $customer->get_id() );
if ( ! empty( $user_subscriptions ) ) {
foreach ( $user_subscriptions as $subscription ) {
if ( $subscription->has_status( self::ACTIVE_SUBSCRIPTION_STATUSES ) ) {
return $subscription;
}
}
$active_subscriptions = self::get_active_subscriptions_for_user( $user_id );
if ( ! empty( $active_subscriptions ) ) {
return reset( $active_subscriptions );
}

// If no active subscriptions, get the most recent completed order.
// See https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query for query args.
$args = [
'customer_id' => $customer->get_id(),
'customer_id' => $user_id,
'status' => [ 'wc-completed' ],
'limit' => 1,
'order' => 'DESC',
Expand Down

0 comments on commit aebe581

Please sign in to comment.