s2Member sending request to Cancel Subscription to Stripe

I tie it to the user by manually filling the cus_ and sub_ codes.

I manage a bunch of things manually because the plugin is broken or isn’t properly designed for customer retention, dunning routines, downgrading when payments fail and reupgrading when they happen later on etc. I would likely have less than half of the subscribers I do if I just let the system do its thing, plus people would be getting free access for far too long. I recover a lot of users sometimes after weeks or even a couple of months trying to collect. Only a few users take action to fix a subscription when a payment doesn’t go through but they surely contact me when it does and they’re not properly reupgraded. :smirk:

Oh, and shouldn’t recurring times be -1 normally, since it’s a recurring subscription towards infinity? I wonder if any other subscription without a set term is different… :thinking:

Sorry for the out of topic paragraph above. Anyways, I can send you a DM with the user name so you can see her.

:slightly_smiling_face:

Happened again.

I have a feeling it’s the anti duplicate subscription. Maybe Stripe sends two requests because the first one isn’t confirmed quickly enough by the server, maybe because it’s busy or something similar. Then it’s possible that s2Member thinks it’s a second subscription for the same product on the same customer and sends the signal to cancel the “previous” one, which is equal the last one that’s just being notified by Stripe a second time.

Makes sense?

How can we disable that feature and let Stripe create as many duplicates as necessary, at least for the time being? Or perhaps it can check both cus and sub codes and not cancel if they match.

This bug is really not nice because I am forced to operate on Stripe, assign credit, resubscribe the user, copy the values manually etc.

If possible to do something easy, of course, maybe even if it’s temporarily just not letting the anti duplicate subscription routine kick in?

Hmm… I’m not sure I’m understand it well yet…

Could you describe it again, maybe as a step by step list of what happens? Maybe with enough details to be able to try reproducing it on my end.

:slight_smile:

1 Like

Hi!

This is what seems to happen:

  1. User Subscribes / Upgrades / Resubscribes / Reupgrades (with or without existing paid subscription) via Stripe using s2Member pro form;

  2. Stripe sends notification to my web server, s2Member receives it, upgrades user as it should etc.

  3. Stripe doesn’t receive a notification back from my site (it only happens sometimes) saying 200 OK or other sort of handshake it should, maybe because the server is busy, maybe because there’s a communication issue.

  4. Stripe, then, sends the same notification (Item 2 above) to the server again, after a minute or a few, maybe once more, maybe a few more times, expecting to receive a handshake / confirmation / 200 OK or whatever is normal back from my Web Server.

  5. s2Member CANCELS the subscription because of s2Member’s own “anti duplicate subscription” functionality as it thinks the user subscribed twice for any error. THIS is the feature that needs to be fixed to perform differently. s2Member also sends a request to Stripe to cancel the user’s subscription.

  6. Final result is you get one single payment and the user is left downgraded and the recurring subscription is cancelled, meaning the system won’t collect automatically as expected, because s2Member sent Stripe a cancellation request, which Stripe fulfilled.

I understand that it’s expected that s2member will cancel an existing subscription when a user starts a new subscription to prevent the user from paying for more than a single subscription, as simultaneous subscriptions aren’t currently supported by s2member for a single user.

That routine that is designed to prevent duplicity should only act when there’s an actual duplicity and it should never cancel the subscription that it’s being notified on. Therefore there should be better testing to prevent a new subscription from ever being cancelled in that situation (if the user will have no subscription).

Also, ideally (maybe in a remote future), s2Member should use the fields that are editable by us like cus and sub fields not that long “string” that stays hidden from view. I frequently re-upgrade users that make late payments by editing both fields manually as s2Member doesn’t have dunning and recovery routines.

Of course, those things can be fixed on a later date but we need to have at least this feature that nukes subscriptions on Stripe deactivated somehow.

It’s ok to downgrade the user on your database only, we’re notified by it and we can re-upgrade the user manually, but recreating the user subscription on Stripe is a massive pain plus we lose 3D Secure status for any future payments related to that subscription.

I don’t use s2Member cancellation routines towards payment processors (I only take email requests for cancellations, I don’t trust cancellation buttons for many reasons). I cancel everything manually upon request or when I find redundancy. Is there an easy way to add a couple of php lines on my generic must use plugin to prevent any cancellation request of any sort to be ever sent from s2Member to payment processors as a temporary workaround until you fix this bug?

Thanks in advance! :slightly_smiling_face:

Hi, i was having a look at your website, https://thesimarchitect.com and I noticed that most of the images don’t load. If I check the code and try to download the image from the direct link, I get an error. Maybe, somehow it’s related.

1 Like

Hi! Really? My site looks normal here :grimacing:

Looks normal to me, too.

1 Like

Thank you, that made it very clear. I’ll look into this.

Also, ideally (maybe in a remote future), s2Member should use the fields that are editable by us like cus and sub fields not that long “string” that stays hidden from view. I frequently re-upgrade users that make late payments by editing both fields manually as s2Member doesn’t have dunning and recovery routines.

You mean the signup vars, right?

Is there an easy way to add a couple of php lines on my generic must use plugin to prevent any cancellation request of any sort to be ever sent from s2Member to payment processors as a temporary workaround until you fix this bug?

I’ll tell you if I see a way.

:slight_smile:

1 Like

Awesome!

Signup vars might be their name, I didn’t want to risk giving you wrong information? That long string that has a bunch of sub elements bundled together instead of proper separate fields that we can edit (and see) using the admin view from the back end. :slightly_smiling_face:

When I edit a PayPal subscriber I just need to add it to the sub and cus fields and the integration sees and interacts with it. Stripe only interacts with the complex one that’s always hidden from us and that shows as a single string with subparts when exported. :crazy_face:

I hope that I can at least neuter this behavior temporarily with a hack or two.

Thanks and welcome back again! :slightly_smiling_face:

1 Like

Just had a similar but different case.

User had a pending invoice.

Instead of paying it, they started a new upgraded subscription via Stripe

s2Member recognized the operation as a modification, upgraded the account.

Right after doing it, s2Member demoted the user and sent an EOT message.

Thankfully the subscription is still active on Stripe and I reupgraded the user manually at Wordpress, as usual.

Sadly I know there’s a high chance s2Member sends a cancellation request to Stripe later, I hope not. :pray:t2:

Thanks for the update. Any related entries in the s2 logs about it? Could I see them? (edit any sensitive information, please)

:slight_smile:

1 Like

I thought you had seen my private message with the logs.

I notice there was an update to the plugin today. Do you want me to re-edit that part for testing manually again or should I just keep the code as is?

I thought you had seen my private message with the logs.

Thank you! I saw them now.

That’s from s2’s stripe-api.log, right? Could you show me the related entries from the log over at Stripe? I’m trying to see what part of the webhooks is causing s2 to end up firing the EOT. I have a suspicion, but trying to confirm it.

I notice there was an update to the plugin today. Do you want me to re-edit that part for testing manually again or should I just keep the code as is?

Do you mean this? Ipn_signup_vars

Sure, you can reapply it if you want to keep testing that.

:slight_smile:

1 Like

Problem happened again today. User tried to subscribe, payment failed. They did it again a minute later successfully.

s2Member cancelled the successful subscription.

How can we disable any signal from s2member cancelling subscriptions, so this does not happen? I don’t use Cancel buttons on my site as I process cancellations always manually to avoid issues like users saying they clicked on a button when they didn’t etc.

Can you help me with a snippet of code or a portion of the plugin to modify so s2member NEVER sends a request for Stripe to cancel a subscription under any circumstance? :pray:t2:

Same problem just happened again. Another new subscription cancelled by the plugin 2 hours after the new subscription started.

I don’t have a cancel button anywhere on my site and I didn’t process any cancellation request, so it’s likely the same glitch as always.

I had to create a new subscription manually, as usual. I hope I don’t face problems for doing that with my recurring charges as there’s likely no 3D Secure etc. It’s usually better that a subscription started by the user recurrs naturally, plus it’s a pain to update everything manually everytime s2member gets confused, usually because there is another subscription for the user (or there was one).

:grimacing:

Just bumping because the problem persists. :cry:

@clavaque doesn’t even show up to clear all the spam that’s taking over this forum, so I wouldn’t expect any solutions.

1 Like

He’s back :partying_face:

I’ll also keep an eye from now on. :innocent:

So, I am unsure if it will work, but this is my test, where I intentionally neuter Stripe’s Cancellation routines:

Inside the folder /wp-content/plugins/s2member-pro/src/includes/classes/gateways/stripe I modified a few files…

stripe-cancellation-in.inc.php

										if(is_array($ipn_signup_vars = c_ws_plugin__s2member_utils_users::get_user_ipn_signup_vars()))
										{
											/* Deactivated Section Below and changed the Log Return to Reflect That
											$ipn['txn_type']   = 'subscr_cancel';
											$ipn['subscr_cid'] = $ipn_signup_vars['subscr_cid'];
											$ipn['subscr_id']  = $ipn_signup_vars['subscr_id'];
											$ipn['custom']     = $ipn_signup_vars['custom'];

											$ipn['period1'] = $ipn_signup_vars['period1'];
											$ipn['period3'] = $ipn_signup_vars['period3'];

											$ipn['payer_email'] = $ipn_signup_vars['payer_email'];
											$ipn['first_name']  = $ipn_signup_vars['first_name'];
											$ipn['last_name']   = $ipn_signup_vars['last_name'];

											$ipn['option_name1']      = $ipn_signup_vars['option_name1'];
											$ipn['option_selection1'] = $ipn_signup_vars['option_selection1'];

											$ipn['option_name2']      = $ipn_signup_vars['option_name2'];
											$ipn['option_selection2'] = $ipn_signup_vars['option_selection2'];

											$ipn['item_name']   = $ipn_signup_vars['item_name'];
											$ipn['item_number'] = $ipn_signup_vars['item_number'];

											$ipn['s2member_paypal_proxy']              = 'stripe';
											$ipn['s2member_paypal_proxy_use']          = 'pro-emails';
											$ipn['s2member_paypal_proxy_verification'] = c_ws_plugin__s2member_paypal_utilities::paypal_proxy_key_gen();

											c_ws_plugin__s2member_utils_urls::remote(home_url('/?s2member_paypal_notify=1'), $ipn, array('timeout' => 20));
											*/
										}

Commented out the segment above, first line of the excerpt is number 89.

stripe-cancellation.inc.php

	public static function stripe_cancellation()
	{
		/* Disabled the Function Below
		if(!empty($_POST['s2member_pro_stripe_cancellation']))
			c_ws_plugin__s2member_pro_stripe_cancellation_in::stripe_cancellation();
		*/
	}

I added the return false and I commented out the original. Not sure what this does but hopefully it cripples one item necessary.

stripe-utilities.inc.php

		try // Attempt to cancel the subscription for this customer.
		{
			/* Deactivated Routines Below
			// Check for draft/open invoice and void.
			$subscription = \Stripe\Subscription::retrieve($subscription_id);
			if (!empty($subscription->latest_invoice)) {
				$latest_invoice = \Stripe\Invoice::retrieve($subscription->latest_invoice);
				// If draft, finalize to change status to "open".
				if ($latest_invoice->status == 'draft') {
					$latest_invoice = $latest_invoice->finalizeInvoice([
						'auto_advance' => false
					]);
				}
				if ($latest_invoice->status == 'open') {
					$latest_invoice = $latest_invoice->voidInvoice();
				}
			}

			// Delete subscription if cancel now, update if at period end.
			if ($cancel_at_period_end) {
				$subscription = \Stripe\Subscription::update(
					$subscription_id, 
					array('cancel_at_period_end' => true)
				);
			} else {
				$subscription = \Stripe\Subscription::retrieve($subscription_id);
				$subscription = $subscription->delete();
			}

			self::log_entry(__FUNCTION__, $input_time, $input_vars, time(), $subscription);

			return $subscription; // Stripe subscription object.
			*/
		}

Again, commented out that segment to disable it.

I am pretty sure what I am doing is extremely inelegant and might not work. Hopefully it won’t cause issues. Maybe @clavaque gives some feedback and tells me the same could be done with half line of code :crazy_face:

I’ll keep you all posted.

:slightly_smiling_face:

Just in case I did the same to paypal-cancellation.inc.php

if (!class_exists ("c_ws_plugin__s2member_pro_paypal_cancellation"))
	{
		/**
		* PayPal Cancellation Form processing.
		*
		* @package s2Member\PayPal
		* @since 1.5
		*/
		class c_ws_plugin__s2member_pro_paypal_cancellation
			{
				/**
				* Handles processing of Pro-Form cancellations.
				*
				* @package s2Member\PayPal
				* @since 1.5
				*
				* @attaches-to ``add_action("init");``
				*
				* @return null|inner Return-value of inner routine.
				*/
				public static function paypal_cancellation ()
					{
						if (!empty($_POST["s2member_pro_paypal_cancellation"]))
							{
								/* Disabled Routine Below to Prevent Cancellations
								if($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["paypal_payflow_api_username"])
									return c_ws_plugin__s2member_pro_paypal_cancellation_pf_in::paypal_cancellation();

								return c_ws_plugin__s2member_pro_paypal_cancellation_in::paypal_cancellation ();
								*/
							}
					}
			}
	}

You can see the part I commented out near the end.

Also edited paypal-utilities.inc.php

		/**
		 * Cancels a Payflow recurring profile.
		 *
		 * @package s2Member\PayPal
		 * @since 110531
		 *
		 * @param string $subscr_id A paid subscription ID (aka: Recurring Profile ID).
		 * @param string $baid A Billing Agreement ID (aka: BAID).
		 *
		 * @return boolean True if the profile was cancelled, else false.
		 */
		public static function payflow_cancel_profile($subscr_id = '', $baid = '')
		{
			/* Disabled Routine Below to Prevent Cancellations
			
			$payflow['TRXTYPE']       = 'R';
			$payflow['ACTION']        = 'C';
			$payflow['TENDER']        = 'C';
			$payflow['ORIGPROFILEID'] = $subscr_id;

			if(($cancellation = c_ws_plugin__s2member_paypal_utilities::paypal_payflow_api_response($payflow)) && empty($cancellation['__error']))
				if(!$baid || c_ws_plugin__s2member_pro_paypal_utilities::payflow_cancel_billing_agreement($baid))
					return TRUE;

			$payflow['TENDER'] = 'P';
			if(($cancellation = c_ws_plugin__s2member_paypal_utilities::paypal_payflow_api_response($payflow)) && empty($cancellation['__error']))
				if(!$baid || c_ws_plugin__s2member_pro_paypal_utilities::payflow_cancel_billing_agreement($baid))
					return TRUE;
			*/

			return FALSE;
		}

		/**
		 * Cancels a Payflow Billing Agreement.
		 *
		 * @package s2Member\PayPal
		 * @since 130510
		 *
		 * @param string $baid A Billing Agreement ID (aka: BAID).
		 *
		 * @return boolean True if the agreement was cancelled, else false.
		 */
		public static function payflow_cancel_billing_agreement($baid = '')
		{
			/* Disabled Routine Below to Prevent Cancellations
			
			$payflow['ACTION']    = 'U';
			$payflow['TENDER']    = 'P';
			$payflow['BAID']      = $baid;
			$payflow['BA_STATUS'] = 'cancel';

			if(($cancellation = c_ws_plugin__s2member_paypal_utilities::paypal_payflow_api_response($payflow)) && empty($cancellation['__error']))
				return TRUE;
			*/

			return FALSE;
		}

Those are just segments of the files, not their entirety! Make any modifications at your own risk!

I attached a zip with the three modified files.stripe.zip (15.5 KB)

Stay tuned for news! :slightly_smiling_face: