-
-
Save strangerstudios/6169167 to your computer and use it in GitHub Desktop.
/* | |
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); |
FWIW, I don't think the empty($order)
test on line 25 could ever give the expected result since you instantiate the variable with a new class (no longer empty), then assign a variable in the class with the $order->getLastMemberOrder()
function?
@eighty20results Thanks. Updated to check empty($order->id) now.
where does this code go? in functions.php?
Some one said to make it a plugin. aka add on?
I made this into a plugin and now since the upgrade it is not showing up in the plugin list. is this the right way to set it up?
/**
-
Plugin Name: Membership expires on next billing
-
Description: will cancle a membership on next billing date
-
Plugin URI: https://gist.github.com/strangerstudios/6169167
-
Author: Strangerstudios
-
Author URI: http://wordpress.stackexchange.com/users/9942/william
*/
/*
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).
*/
//give users their level back with an expiration
Errors on my last post odd
I made this into a plugin is this the right way to set it up?
/**
-
Plugin Name: Membership expires on next billing
-
Description: will cancel a membership on next billing date
-
Plugin URI: https://gist.github.com/strangerstudios/6169167
-
Author: Strangerstudios
-
Author URI: http://wordpress.stackexchange.com/users/9942/william
*/
I made this into a plugin is this the right way to set it up?
That looks pretty good to me. Does it work well? Also note that I made an important update to the code here just now to add better checks to make sure the level change is a cancel and that the code has the data it needs to do the extension.
Just to let you know: I added this to my existing installation, and worked well BUT I observed that new memberships got cancelled right away...I don't know why, but I deleted this gist from my setup and everything was working ok. Maybe it was only coincidence, but I hope you can look into it.
Hey All. We've been using this gist for a 6 months or so with no issue. Recently however the customer gets and error on the screen when they try to cancel. The cancel goes through correctly in Stripe and the new expiration date is set in PMPro but they see an error like this on the screen:
Error getting subscription with Stripe: Customer cus_xxxxxx does not have a subscription with ID sub_xxxxx
I'm assuming this is because it may be firing the code too late after the Stripe subscription is cancelled. Maybe the error is coming from this line:
$pmpro_next_payment_timestamp = $pmpro_stripe_event->data->object->current_period_end;
Any help?
Hi @strangerstudios does this code work with the newest version of the Paid Membership Pro? I've noticed that:
- If a customer has only been billed once and they cancel, the cancellation is processed correctly and the date in WP is populated to expire at the end of the billing term (1 month, 3 months or 1 year)
- If a customer has been with us for awhile and the subscription has auto-renewed and customer has been billed more than 1 time, the cancellation takes effect immediately.
We're using braintree integration.
I just updated this gist with a few things. Can you guys who were having issues with it test and let me know if it's working for you again? I think my changes may have been unrelated, but just in case...
Our members have a week's trial, then a quarterly subscription can be taken. If the payment gateway suspends a member for non-payment after the trial period, will the system still only cancel at the end of the quarter of immediately?
Hey @strangerstudios so we've been having issues with Stripe users who's membership cancels after 4 failed payments (we use the Failed payment plugin) still get extended time on their account with this code even though they haven't paid for the current month or worse the current year! I'm wondering if that is because Stripe first creates an invoice and then tries to charge it so when this checks for the "current_period_end" it is seeing the unpaid invoice rather than the last paid invoice.
Perhaps that section should be changed to this and use the last successful payment in PMPro:
if(!empty($pmpro_stripe_event)) { //cancel initiated from Stripe webhook if(!empty($pmpro_stripe_event->data->object->current_period_end)) { $pmpro_next_payment_timestamp = PMProGateway_stripe::pmpro_next_payment("", $user_id, "success"); } } else { //cancel initiated from PMPro $pmpro_next_payment_timestamp = PMProGateway_stripe::pmpro_next_payment("", $user_id, "success"); }
is my thinking off here?
Also @strangerstudios we seem to still be getting the Stripe error occasionally (reported back in January) when people cancel and I'm thinking it is this Gist. I even added a loading screen that fires on the Cancel button so they can't accidentally press it twice.
This extra bit will stop Stripe from extended the membership of users who have their subscription deleted through the webhook (aka had a failed payment).
function my_pmpro_stripe_subscription_deleted() { // Remove "cancel on next payment" hooks. remove_action('pmpro_before_change_membership_level', 'my_pmpro_before_change_membership_level', 10, 2); remove_action("pmpro_after_change_membership_level", "my_pmpro_after_change_membership_level", 10, 2); remove_filter("pmpro_email_body", "my_pmpro_email_body", 10, 2); } add_action( 'pmpro_stripe_subscription_deleted', 'my_pmpro_stripe_subscription_deleted' );
Something similar could be setup for PayPal and other gateways.
We could instead check the start date and length of the membership and try to not extend past the next payment date.
This gist in general needs an update.
Hi ideadude, et al
appreciated. It would be awesome if we could update this gist to include PayPal bit too and any other bits that need updating. It is super crucial to NOT Cancel membership instantly as opposed to cancelling it at the next subscription cycle (monthly, annual etc.). Given that subscription payments almost always are taken in advance, it makes little to no sense to torpedo membership i.e. content access immediately upon subscription cancellation - what am I talking about? I think PMPro core should consider this change to be the New default (as opposed to lumping membership and subscription cancellations together).
@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 );
@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 :)
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é
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.
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).
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.
NEW: PMPro Addon. https://www.paidmembershipspro.com/add-ons/cancel-on-next-payment-date/
Hi, Can anyone help me with the pmpro_stripe_subscription_deleted hook of PmPro, I have a custom functionality on the pmpro_after_checkout hook, but pmpro_stripe_subscription_deleted() is calling explicitly on payment success. and I am unable to find the pmpro_stripe_subscription_deleted hook in the PmPro documentation.
How i get this code work? to your plugin?