Skip to content
Open
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
87 changes: 86 additions & 1 deletion adminpages/scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,18 @@
<?php esc_html_e( 'Level ID:', 'pmpro-toolkit' ); ?>
<input type="number" name="cancel_level_id" value="" size="5">
</p>
<p>
<label>
<input type="radio" name="cancel_level_type" value="immediate" checked>
<?php esc_html_e( 'Cancel membership immediately.', 'pmpro-toolkit' ); ?>
</label>
</p>
<p>
<label>
<input type="radio" name="cancel_level_type" value="next_payment_date">
<?php esc_html_e( 'Set expiration date to the next payment date (if set), otherwise cancel membership immediately.', 'pmpro-toolkit' ); ?>
</label>
</p>
<p class="description"><?php echo esc_html( $level_actions['pmprodev_cancel_level']['description'] ); ?></p>
</div>
</td>
Expand Down Expand Up @@ -665,6 +677,12 @@ function pmprodev_give_level( $message ) {
/**
* Cancel all users with a specific membership level.
*
* Supports two modes via the `cancel_level_type` request param:
* - 'immediate' (default): cancel the membership and any recurring subscription right away.
* - 'next_payment_date': stop recurring billing but keep the membership active until the
* subscription's next payment date. Members without an active subscription (no future
* payment date) are cancelled immediately.
*
* @param string $message The message to display after the process is complete.
* @since 1.0
* @return void
Expand All @@ -687,15 +705,82 @@ function pmprodev_cancel_level( $message ) {
pmprodev_expand_actions( 'pmprodev_cancel_level' );
}

// Determine whether to cancel immediately or expire on the next payment date.
$cancel_type = isset( $_REQUEST['cancel_level_type'] ) ? sanitize_text_field( $_REQUEST['cancel_level_type'] ) : 'immediate';
if ( 'next_payment_date' !== $cancel_type ) {
$cancel_type = 'immediate';
}

$message = sprintf( $message, count( $user_ids ) );
pmprodev_output_message( $message );
foreach ( $user_ids as $user_id ) {
pmpro_cancelMembershipLevel( $cancel_level_id, $user_id );
if ( 'next_payment_date' === $cancel_type ) {
pmprodev_cancel_level_on_next_payment_date( $cancel_level_id, $user_id );
} else {
pmpro_cancelMembershipLevel( $cancel_level_id, $user_id );
}
}

pmprodev_process_complete();
}

/**
* Cancel a user's recurring billing but keep their membership active until the
* subscription's next payment date.
*
* Cancelling the subscription at the gateway marks it cancelled in the database first,
* so the resulting webhook will not strip the membership. We then set the membership's
* end date to the latest upcoming payment date so PMPro's expiration cron removes the
* level when that date passes. Members without a future payment date are cancelled
* immediately, since there is no term to keep active.
*
* @param int $level_id The membership level ID being cancelled.
* @param int $user_id The user whose membership is being cancelled.
* @since 1.x
* @return void
*/
function pmprodev_cancel_level_on_next_payment_date( $level_id, $user_id ) {
global $wpdb;

// Find the latest upcoming payment date across the user's active subscriptions for this level.
$next_payment_date = '';
if ( class_exists( 'PMPro_Subscription' ) ) {
$subscriptions = PMPro_Subscription::get_subscriptions_for_user( $user_id, $level_id );
foreach ( $subscriptions as $subscription ) {
$date = $subscription->get_next_payment_date( 'Y-m-d H:i:s' );
if ( ! empty( $date ) && ( empty( $next_payment_date ) || $date > $next_payment_date ) ) {
$next_payment_date = $date;
}
}
}

// No future payment date to keep the membership active until, so cancel immediately.
if ( empty( $next_payment_date ) || strtotime( $next_payment_date ) <= current_time( 'timestamp' ) ) {
pmpro_cancelMembershipLevel( $level_id, $user_id );
return;
}

// Stop recurring billing at the gateway (keeps the membership active).
foreach ( $subscriptions as $subscription ) {
$subscription->cancel_at_gateway();
}

// Keep the membership active but set it to expire on the next payment date.
$wpdb->update(
$wpdb->pmpro_memberships_users,
array( 'enddate' => $next_payment_date ),
array(
'user_id' => $user_id,
'membership_id' => $level_id,
'status' => 'active',
),
array( '%s' ),
array( '%d', '%d', '%s' )
);

pmpro_clear_level_cache_for_user( $user_id );
}

/**
* Copy content restrictions from one membership level to another.
*
Expand Down
22 changes: 19 additions & 3 deletions cli/Toolkit_Commands.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,16 @@ public function give_level( $args, $assoc_args ) {
*
* ## OPTIONS
* --level=<id> : Level ID to cancel.
* [--when=<when>] : When to cancel. 'immediate' (default) cancels memberships and
* recurring subscriptions right away. 'next-payment-date' stops recurring billing
* but keeps each membership active until the subscription's next payment date
* (members without an active subscription are cancelled immediately).
* ---
* default: immediate
* options:
* - immediate
* - next-payment-date
* ---
* [--dry-run]
*/
public function cancel_level( $args, $assoc_args ) {
Expand All @@ -266,12 +276,18 @@ public function cancel_level( $args, $assoc_args ) {
if ( $level < 1 ) {
WP_CLI::error( __( 'Please supply --level.', 'pmpro-toolkit' ) );
}
$when = isset( $assoc_args['when'] ) && 'next-payment-date' === $assoc_args['when'] ? 'next_payment_date' : 'immediate';
if ( $dry ) {
$this->dry_run_note( true, sprintf( __( 'Would cancel all users with level %d.', 'pmpro-toolkit' ), $level ) );
$this->dry_run_note( true, 'next_payment_date' === $when
? sprintf( __( 'Would set memberships with level %d to expire on their next payment date.', 'pmpro-toolkit' ), $level )
: sprintf( __( 'Would cancel all users with level %d.', 'pmpro-toolkit' ), $level ) );
return;
}
$this->confirm_or_continue( $dry, sprintf( __( 'Cancel all active memberships for level %d (including recurring subscriptions)?', 'pmpro-toolkit' ), $level ) );
$_REQUEST['cancel_level_id'] = $level;
$this->confirm_or_continue( $dry, 'next_payment_date' === $when
? sprintf( __( 'Set all active memberships for level %d to expire on their next payment date (and stop recurring billing)?', 'pmpro-toolkit' ), $level )
: sprintf( __( 'Cancel all active memberships for level %d (including recurring subscriptions)?', 'pmpro-toolkit' ), $level ) );
$_REQUEST['cancel_level_id'] = $level;
$_REQUEST['cancel_level_type'] = $when;
\pmprodev_cancel_level( __( 'Cancelling users...', 'pmpro-toolkit' ) );
WP_CLI::success( __( 'Done.', 'pmpro-toolkit' ) );
}
Expand Down