Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save strangerstudios/6169167 to your computer and use it in GitHub Desktop.
Save strangerstudios/6169167 to your computer and use it in GitHub Desktop.
Change PMPro membership cancellation to set expiration date for next payment instead of cancelling immediately.
/*
Change cancellation to set expiration date for next payment instead of cancelling immediately.
Assumes orders are generated for each payment (i.e. your webhooks/etc are setup correctly).
Since 2015-09-21 and PMPro v1.8.5.6 contains code to look up next payment dates via Stripe and PayPal Express APIs.
*/
//before cancelling, save the next_payment_timestamp to a global for later use. (Requires PMPro 1.8.5.6 or higher.)
function my_pmpro_before_change_membership_level($level_id, $user_id) {
//are we on the cancel page?
global $pmpro_pages, $wpdb, $pmpro_stripe_event, $pmpro_next_payment_timestamp;
if($level_id == 0 && (is_page($pmpro_pages['cancel']) || (is_admin() && (empty($_REQUEST['from']) || $_REQUEST['from'] != 'profile')))) {
//get last order
$order = new MemberOrder();
$order->getLastMemberOrder($user_id, "success");
//get level to check if it already has an end date
if(!empty($order) && !empty($order->membership_id))
$level = $wpdb->get_row("SELECT * FROM $wpdb->pmpro_memberships_users WHERE membership_id = '" . $order->membership_id . "' AND user_id = '" . $user_id . "' ORDER BY id DESC LIMIT 1");
//figure out the next payment timestamp
if(empty($level) || (!empty($level->enddate) && $level->enddate != '0000-00-00 00:00:00')) {
//level already has an end date. set to false so we really cancel.
$pmpro_next_payment_timestamp = false;
} elseif(!empty($order) && $order->gateway == "stripe") {
//if stripe, try to use the API
if(!empty($pmpro_stripe_event)) {
//cancel initiated from Stripe webhook
if(!empty($pmpro_stripe_event->data->object->current_period_end)) {
$pmpro_next_payment_timestamp = $pmpro_stripe_event->data->object->current_period_end;
}
} else {
//cancel initiated from PMPro
$pmpro_next_payment_timestamp = PMProGateway_stripe::pmpro_next_payment("", $user_id, "success");
}
} elseif(!empty($order) && $order->gateway == "paypalexpress") {
//if PayPal, try to use the API
if(!empty($_POST['next_payment_date']) && $_POST['next_payment_date'] != 'N/A') {
//cancel initiated from IPN
$pmpro_next_payment_timestamp = strtotime($_POST['next_payment_date'], current_time('timestamp'));
} else {
//cancel initiated from PMPro
$pmpro_next_payment_timestamp = PMProGateway_paypalexpress::pmpro_next_payment("", $user_id, "success");
}
} else {
//use built in PMPro function to guess next payment date
$pmpro_next_payment_timestamp = pmpro_next_payment($user_id);
}
}
}
add_action('pmpro_before_change_membership_level', 'my_pmpro_before_change_membership_level', 10, 2);
//give users their level back with an expiration
function my_pmpro_after_change_membership_level($level_id, $user_id) {
global $pmpro_pages, $wpdb, $pmpro_next_payment_timestamp;
if($pmpro_next_payment_timestamp !== false && //this is false if the level already has an enddate
$level_id == 0 && //make sure we're cancelling
(is_page($pmpro_pages['cancel']) || (is_admin() && (empty($_REQUEST['from']) || $_REQUEST['from'] != 'profile')))) { //on the cancel page or in admin/adminajax/webhook and not the edit user page
/*
okay, let's give the user his old level back with an expiration based on his subscription date
*/
//get last order
$order = new MemberOrder();
$order->getLastMemberOrder($user_id, "cancelled");
//can't do this if we can't find the order
if(empty($order->id))
return false;
//get the last level they had
$level = $wpdb->get_row("SELECT * FROM $wpdb->pmpro_memberships_users WHERE membership_id = '" . $order->membership_id . "' AND user_id = '" . $user_id . "' ORDER BY id DESC LIMIT 1");
//can't do if we can't find an old level
if(empty($level))
return false;
//last payment date
$lastdate = date("Y-m-d", $order->timestamp);
/*
next payment date
*/
//if stripe or PayPal, try to use the API
if(!empty($pmpro_next_payment_timestamp)) {
$nextdate = $pmpro_next_payment_timestamp;
} else {
$nextdate = $wpdb->get_var("SELECT UNIX_TIMESTAMP('" . $lastdate . "' + INTERVAL " . $level->cycle_number . " " . $level->cycle_period . ")");
}
//if the date in the future?
if($nextdate - time() > 0) {
//give them their level back with the expiration date set
$old_level = $wpdb->get_row("SELECT * FROM $wpdb->pmpro_memberships_users WHERE membership_id = '" . $order->membership_id . "' AND user_id = '" . $user_id . "' ORDER BY id DESC LIMIT 1", ARRAY_A);
$old_level['enddate'] = date("Y-m-d H:i:s", $nextdate);
//disable this hook so we don't loop
remove_action("pmpro_after_change_membership_level", "my_pmpro_after_change_membership_level", 10, 2);
remove_filter('pmpro_cancel_previous_subscriptions', 'my_pmpro_before_change_membership_level', 10, 2);
//disable the action to set the default level on cancels
remove_action('pmpro_after_change_membership_level', 'pmpro_after_change_membership_level_default_level', 10, 2);
//change level
pmpro_changeMembershipLevel($old_level, $user_id);
//add the action back just in case
add_action("pmpro_after_change_membership_level", "my_pmpro_after_change_membership_level", 10, 2);
add_filter('pmpro_cancel_previous_subscriptions', 'my_pmpro_before_change_membership_level', 10, 2);
//add the action back to set the default level on cancels
remove_action('pmpro_after_change_membership_level', 'pmpro_after_change_membership_level_default_level', 10, 2);
//change message shown on cancel page
add_filter("gettext", "my_gettext_cancel_text", 10, 3);
}
}
//clear up this global in case we're changing many levels at once (e.g. expiration script running)
unset($pmpro_next_payment_timestamp);
}
add_action("pmpro_after_change_membership_level", "my_pmpro_after_change_membership_level", 10, 2);
//this replaces the cancellation text so people know they'll still have access for a certain amount of time
function my_gettext_cancel_text($translated_text, $text, $domain) {
if(($domain == "pmpro" || $domain == "paid-memberships-pro") && $text == "Your membership has been cancelled.") {
global $current_user;
$translated_text = "Your recurring subscription has been cancelled. Your active membership will expire on " . date(get_option("date_format"), pmpro_next_payment($current_user->ID, "cancelled")) . ".";
}
return $translated_text;
}
//want to update the cancellation email as well
function my_pmpro_email_body($body, $email) {
if($email->template == "cancel") {
global $wpdb;
$user_id = $wpdb->get_var("SELECT ID FROM $wpdb->users WHERE user_email = '" . esc_sql($email->email) . "' LIMIT 1");
if(!empty($user_id)) {
$expiration_date = pmpro_next_payment($user_id);
//if the date in the future?
if($expiration_date - time() > 0) {
$body .= "<p>Your access will expire on " . date(get_option("date_format"), $expiration_date) . ".</p>";
}
}
}
return $body;
}
add_filter("pmpro_email_body", "my_pmpro_email_body", 10, 2);
@mircobabini
Copy link

mircobabini commented Apr 14, 2020

@ideadude also, this one is incompatible with pmpro-reason-for-cancelling, maybe because of:

$order->getlastMemberOrder( $user_id, array("", "success") ) since the last order is not in a success status, but it's already set as cancelled.

Notice: Undefined property: MemberOrder::$notes in /home/scimmia/public_html/lascimmiayoga.com/wp-content/plugins/pmpro-reason-for-cancelling/pmpro-reason-for-cancelling.php on line 41

Notice: Undefined property: MemberOrder::$membership_id in /home/scimmia/public_html/lascimmiayoga.com/wp-content/plugins/paid-memberships-pro/classes/class.memberorder.php on line 699

Adding some details here. I temporary disabled it, not a solution of course. Should be fixed in reason-for-cancelling addon, I think.

/**
 * Must disable the addition of the note about the reason for cancelling into the order.
 * This is because the last user order is already in the cancelled status, not success anymore.
 * Then, if a user have just one order, it will not find it and gibes the errors above.
 * Instead if the user have another order for some reason, it will take the wrong order.
 *
 * It's just the wrong way of handling it.
 */
add_action( 'pmpro_before_change_membership_level', function( $level_id, $user_id ){
    remove_action( 'pmpro_after_change_membership_level', 'pmpror4c_save_reason_to_last_order', 10, 3 );
}, 10, 2 );

@mircobabini
Copy link

mircobabini commented Apr 27, 2020

@ideadude just found a bug. Line 99 and 109. You are manipulating the pmpro_cancel_previous_subscriptions, but you are not using that filter anymore. The correct filter is pmpro_before_change_membership_level.

This issue causes looping. You don't want it to happen as you said at line 97 :)

@dretazzz
Copy link

Hi there,

 I am learning pHp.  Do I put this code in "as is", or am I suppose to alter anything?  Also, where should I put it?  I created a pHp file in the adminPages folder of PMPro.  

Please advise, thank you.
André

@mircobabini
Copy link

I am learning pHp. Do I put this code in "as is", or am I suppose to alter anything? Also, where should I put it? I created a pHp file in the adminPages folder of PMPro.

Nice to meed you @dretazzz; the best way is you DON'T use this script (which is obsolete), instead try this plugin from @eighty20results https://github.com/eighty20results/e20r-membership-expiration-choice (just download and upload the plugin as is). Read the plugin notes to understand how it works.

@dretazzz
Copy link

dretazzz commented Sep 14, 2020 via email

@DanJBishop
Copy link

I am learning pHp. Do I put this code in "as is", or am I suppose to alter anything? Also, where should I put it? I created a pHp file in the adminPages folder of PMPro.

Nice to meed you @dretazzz; the best way is you DON'T use this script (which is obsolete), instead try this plugin from @eighty20results https://github.com/eighty20results/e20r-membership-expiration-choice (just download and upload the plugin as is). Read the plugin notes to understand how it works.

Thanks for this, this plugin looks perfect. Can I ask how it handles those subscriptions where payment has failed after multiple retries and is then cancelled automatically by the payment provider? By default, will their membership expire immediately or at end of next payment period? (Ideally of course we want those that have paid to cancel at end of their current subscription period and those that have not to be cancelled straight away).

@mircobabini
Copy link

Can I ask how it handles those subscriptions where payment has failed after multiple retries and is then cancelled automatically by the payment provider?

It does not depend on this at all. This depends on the specific Gateway implementation of PMPro, so I suggest you to contact the PMPro support directly on this. For instance, PayPal Express has an issue where the Gateway does not cancel automatically the payment even if was not possible to collect the payment. By the way... out of scope here.

@mircobabini
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment