Membership Plugin

WordPress Membership Plugin

  • Home
  • Documentation
  • Addons
  • Support
    • Quick Setup
    • Documentation
    • Premium Addon Support
    • Paid Support
    • Support Forum
    • Support Forum Search
    • Forum Login
    • Forum Registration
  • Contact

[Support request] Stripe Refund Webhook Does Not Have Subscriber ID

Simple Membership Plugin › Forums › Simple Membership Plugin › Stripe Refund Webhook Does Not Have Subscriber ID

Tagged: cancellation, IPN, Refund, stripe, webhook

  • This topic has 4 replies, 3 voices, and was last updated 4 years, 2 months ago by admin.
Viewing 5 posts - 1 through 5 (of 5 total)
  • Author
    Posts
  • January 16, 2022 at 10:42 pm #23483
    landreww
    Participant

    Hello,

    If a refund is initiated at Stripe.com, the Webhook that is sent

      does not contain a subscriber_id

    . ‘swpm-stripe-subscription-ipn.php’ assigns $subscr_id =$event_json->data->object->id; The object id is NOT the subsriber id but a charge id. The $ipn_data['subscr_id'] is assigned an inaccurate value.

    When swpm_handle_subsc_cancel_stand_alone( $ipn_data ) is called, it does not find the member record in swpm_members_tbl. Yes, you have a test to use the $ipn_data['parent_txn_id'] but that is never executed as the previous test runs against the incorrect $ipn_data['subscr_id'].

    Here are the logs from the plugin using the incorrect value as a subscriber id:
    [2022/01/10 15:39:27] -SUCCESS: Stripe subscription webhook received: charge.refunded. Checking if we need to handle this webhook.

    [2022/01/10 15:39:27] - SUCCESS: Refund/Cancellation check - lets see if a member account needs to be deactivated.
    [2022/01/10 15:39:27] - SUCCESS: Subscriber ID is present. Retrieving member account from the database. Subscr_id: <strong>ch_3KFjMnEi4YLfb3js1hAeREIe</strong>
    [2022/01/10 15:39:27] - FAILURE: No associated active member record found for this notification.

    You can see it used the “charge id” as if it was the subscribers ID (the subscriber id starts with “sub_”).

    Unfortunately, the Customer ID which is assigned to $ipn_data['parent_txn_id'] is not in the members table swpm_members_tbl, but it is in the payments table. swpm_payments_tbl.

    Here is a simple fix:
    Change the if-then in swpm_handle_subsc_ipn.php to correctly get the members record from swpm_members_tbl
    1. In swpm-stripe-subscription-ipn.php, set $ipn_data['subscr_id'] to NULL if not in the Stripe Webhook (IPN) (see if-then below).

    // Let's form minimal ipn_data array for swpm_handle_subsc_cancel_stand_alone
    $customer                  = $event_json->data->object->customer;
    $subscr_id                 = $event_json->data->object->id;
    $ipn_data                  = array();
    $ipn_data['subscr_id']     = <strong>(substr($subscr_id, 0, 4) == "sub_" ? $subscr_id : null);</strong>
    $ipn_data['parent_txn_id'] = $customer;
    <strong>$ipn_data['transaction']   = $event_json;</strong>
    
    swpm_handle_subsc_cancel_stand_alone( $ipn_data );

    2. Change the If-Then-Else in swpm_handle_subsc_ipn.php swpm_handle_subsc_cancel_stand_alone function as follows:

    } else {
      // Refund for a one time transaction. Use the parent transaction ID to retrieve the profile.
      $txn_id = $ipn_data['parent_txn_id'];
      swpm_debug_log_subsc( 'Transaction ID is present. Retrieving member account from the database. parent_txn_id: ' . $txn_id, true );
      $resultset = $wpdb->get_row(
        $wpdb->prepare(
          "SELECT t.* FROM {$wpdb->prefix}swpm_members_tbl t JOIN {$wpdb->prefix}swpm_payments_tbl p USING(subscr_id) where p.txn_id = %s",
          $txn_id
        ),
        OBJECT
      );
    }

    The above code will use the Customer ID ($ipn_data['parent_txn_id']) in a JOIN to the payments table to get the members table row.

    The result will be correct processing of the refund Webhook. Here is the log:

    [2022/01/16 14:45:14] - SUCCESS: Refund/Cancellation check - lets see if a member account needs to be deactivated.
    [2022/01/16 14:45:14] - SUCCESS: Transaction ID is present. Retrieving member account from the database. parent_txn_id: cus_KydEeWA9jyCX8V
    [2022/01/16 14:45:14] - SUCCESS: Membership level ID of the member is: 2
    [2022/01/16 14:45:14] - SUCCESS: Subscription duration type: 4
    [2022/01/16 14:45:14] - SUCCESS: This is a level with "duration" type expiry. Duration period: 1, Unit: Years
    [2022/01/16 14:45:14] - SUCCESS: Nothing to do here. The account will expire after the duration time is over.

    These changes should be made to the plugin. When you make the changes, please add the Webhook object to the $ipn_data arrray ($ipn_data['transaction'] = $event_json;). It is needed to fully use the do_action( 'swpm_subscription_payment_cancelled', $ipn_data ); // Hook for recurring payment received. We use the hook for a custom plugin and it would not interfere with existing code. We would rather not need to modify the plugin for every update.

    Thank you for your assistance in correcting this issue.

    January 18, 2022 at 1:19 pm #23488
    mbrsolution
    Moderator

    Thank you for reaching out to us and sharing this information. I have submitted a message to the developers to investigate further your findings.

    Kind regards.

    January 19, 2022 at 6:21 am #23490
    admin
    Keymaster

    Thank you. Some points:

    – You are looking at the “swpm-stripe-subscription-ipn.php” file which has been deprecated by Stripe. Stripe is forcing everyone to use the SCA compatible option. So you need to make sure that you are using the following documentation:
    https://simple-membership-plugin.com/sca-compliant-stripe-subscription-button/

    Then you can look at the “swpm-stripe-sca-subscription-ipn.php” file.

    The old stripe buttons are no longer supported by Stripe. Stripe made this decision. So the code for it will be deleted in a future version.

    – There is a history for how things evolved with the subscription payments. Originally the subscription payment wasn’t available so we later made changes to incorporate it. There is legacy stuff there for backwards compatibility. Those are needed to make sure thousands of other sites doesn’t break suddenly after an upgrade.

    – In certain cases the charge ID is used as subscriber_id (due to the history of the plugin’s evolution). So in certain cases, it’s okay.

    – Please tell us the use case (as advertised by our plugin) and which steps are breaking. For example: how we can reproduce the issue. That way, we can look at the code (keeping in mind our own evolution of the code) and we can investigate it better in terms of what can be changed without impacting thousands of other users.

    – Our code is definitely not perfect. We do have limitation of resources so for certain projects, our simple plugin is not a good fit for the project in question. Please read the following post which has some explanation:
    https://simple-membership-plugin.com/message-from-the-simple-membership-team/

    – I totally agree that we can add more details to the $ipn_data array. This shouldn’t impact anything else. So we can do that for sure. But first, check the “swpm-stripe-sca-subscription-ipn.php” file which does contain the subscriber ID, and then you can see if the additional data is still needed.

    January 26, 2022 at 10:58 pm #23526
    landreww
    Participant

    Thank you for the reply.

    We are using the new Stripe SCA Buttons. We want to capture the “cancel.payment” Webhook which uses “swpm-stripe-subscription-ipn.php”, not swpm-stripe-sca-subscription-ipn.php.

    Our issue is that when a refund (full or partial) is made on the Stripe.com site, the payments table in SWPM is NOT updated to reflect the refund. Also, since you are not finding the member account (because you are looking for the subscriber ID which is not present in the “cancel.payment” Webhook) you cannot possibly deactivate the account (should that be the appropriate action as per your documentation).

    In “https://simple-membership-plugin.com/how-can-i-cancel-a-stripe-subscription-as-a-merchant/&#8221; you state the following:
    “Step 11) If you have configured the stripe webhooks properly when creating the Stripe subscription button, then our plugin will receive this cancellation notification and it will deactivate the member’s account automatically.“. This is not possible if you cannot locate the Member’s account. Frankly, I prefer that the system NOT deactivate the Member Account as that should be done manually so the admin is in full charge of the transaction. Best case, you could add an option as to whether or not to deactivate the Member Account.

    Keep in mind that there could be times you would to give a “partial refund” in Stripe. In any case, there should be some record of a refund. Right now, the payments table only records the original payment. This is poor accounting practice. Best practice would be to add a row to the payments table (swpm_payments_tbl) showing the refund with a status of “refund” or “partial refund”. The Stripe Webhook tells you if it is full or partial as it provides the original purchase amount and the total of all refunds (should there be more than one). To keep things simple, we just applied the total refunded to the original payment and change the “status” to either “refunded” or “partial refund”. So if the original payment was $100 and $30 in refunds were given the amount in the payments list screen would show $70 and the status would be “partial refund”. Best practice would be to add columns on the list for refunded amount and a total paid, but I understand there are space limitations on the display.

    I opened this ticket to let you know that:

    1. You are not properly processing Stripe “canceled.payment” Webhooks as you are looking for a subscriber ID that is not present (even if it was before, it does not matter as it is not there now). This should be addressed as it is a bug.
    Below is a proposed fix for “function swpm_handle_subsc_cancel_stand_alone()”.

    /*
     * All in one function that can handle notification for refund, cancellation, end of term
     */
    
     function swpm_handle_subsc_cancel_stand_alone( $ipn_data, $refund = false ) {
    
     	global $wpdb;
    
             $swpm_id = '';
             if ( isset( $ipn_data['custom'] ) ){
                 $customvariables = SwpmTransactions::parse_custom_var( $ipn_data['custom'] );
                 $swpm_id         = $customvariables['swpm_id'];
             }
    
     	swpm_debug_log_subsc( 'Refund/Cancellation check - lets see if a member account needs to be deactivated.', true );
     	// swpm_debug_log_subsc("Parent txn id: " . $ipn_data['parent_txn_id'] . ", Subscr ID: " . $ipn_data['subscr_id'] . ", SWPM ID: " . $swpm_id, true);.
    
     	if ( ! empty( $swpm_id ) ) {
     		// This IPN has the SWPM ID. Retrieve the member record using member ID.
     		swpm_debug_log_subsc( 'Member ID is present. Retrieving member account from the database. Member ID: ' . $swpm_id, true );
     		$resultset = SwpmMemberUtils::get_user_by_id( $swpm_id );
     	} elseif ( isset( $ipn_data['subscr_id'] ) && substr( $ipn_data['subscr_id'], 0, 4 ) == "sub_") {
                    //APW 2022-01-26 CHECK TO SEE IF subscr_id is an actual subscriber not an event id from Stripe.
     		// This IPN has the subscriber ID. Retrieve the member record using subscr_id.
     		$subscr_id = $ipn_data['subscr_id'];
     		swpm_debug_log_subsc( 'Subscriber ID is present. Retrieving member account from the database. Subscr_id: ' . $subscr_id, true );
     		$resultset = $wpdb->get_row(
     			$wpdb->prepare(
     				"SELECT * FROM {$wpdb->prefix}swpm_members_tbl where subscr_id LIKE %s",
     				'%' . $wpdb->esc_like( $subscr_id ) . '%'
     			),
     			OBJECT
     		);
     	} elseif ( isset( $ipn_data['parent_txn_id'] ) && substr( $ipn_data['parent_txn_id'], 0, 4 ) == "cus_" )  {
     		// Refund for a one time transaction. Use the parent transaction ID (customer) to retrieve the profile.
     		//APW 2022-01-16  Added to use payments table customer ID to find Member ID.
     		// Stripe Customer ID begins with 'cus_'
     		$txn_id = $ipn_data['parent_txn_id'];
     		$test = substr( $ipn_data['parent_txn_id'], 0, 4 );
     		swpm_debug_log_subsc( 'Transaction ID is present. Retrieving member account from the database basrd on parent_txn_id (cutomer): ' . $txn_id, true );
     		$resultset = $wpdb->get_row(
     			$wpdb->prepare(
     				"SELECT t.* FROM {$wpdb->prefix}swpm_members_tbl t join {$wpdb->prefix}swpm_payments_tbl p USING(subscr_id) where p.txn_id = %s",
     				$txn_id
     			),
     			OBJECT
     		);
     	} else {
     		// Refund for a one time transaction. Use the parent transaction ID to retrieve the profile.
     		$subscr_id = $ipn_data['parent_txn_id'];
     		$resultset = $wpdb->get_row(
     			$wpdb->prepare(
     				"SELECT * FROM {$wpdb->prefix}swpm_members_tbl where subscr_id LIKE %s",
     				'%' . $wpdb->esc_like( $subscr_id ) . '%'
     			),
     			OBJECT
     		);
     	}
    
     	if ( $resultset ) {
     		// We have found a member profile for this notification.
    
     		$member_id = $resultset->member_id;
    
     		// First, check if this is a refund notification.
     		if ( $refund ) {
     			// This is a refund (not just a subscription cancellation or end). So deactivate the account regardless and bail.
     			SwpmMemberUtils::update_account_state( $member_id, 'inactive' ); // Set the account status to inactive.
     			swpm_debug_log_subsc( 'Subscription refund notification received! Member account deactivated.', true );
     			return;
     		}
    
     		// This is a cancellation or end of subscription term (no refund).
     		// Lets retrieve the membership level and details.
     		$level_id = $resultset->membership_level;
     		swpm_debug_log_subsc( 'Membership level ID of the member is: ' . $level_id, true );
     		$level_row          = SwpmUtils::get_membership_level_row_by_id( $level_id );
     		$subs_duration_type = $level_row->subscription_duration_type;
    
     		swpm_debug_log_subsc( 'Subscription duration type: ' . $subs_duration_type, true );
    
     		if ( SwpmMembershipLevel::NO_EXPIRY == $subs_duration_type ) {
     			// This is a level with "no expiry" or "until cancelled" duration.
     			swpm_debug_log_subsc( 'This is a level with "no expiry" or "until cancelled" duration', true );
    
     			// Deactivate this account as the membership level is "no expiry" or "until cancelled".
     			$account_state = 'inactive';
     			SwpmMemberUtils::update_account_state( $member_id, $account_state );
     			swpm_debug_log_subsc( 'Subscription cancellation or end of term received! Member account deactivated. Member ID: ' . $member_id, true );
     		} elseif ( SwpmMembershipLevel::FIXED_DATE == $subs_duration_type ) {
     			// This is a level with a "fixed expiry date" duration.
     			swpm_debug_log_subsc( 'This is a level with a "fixed expiry date" duration.', true );
     			swpm_debug_log_subsc( 'Nothing to do here. The account will expire on the fixed set date.', true );
     		} else {
     			// This is a level with "duration" type expiry (example: 30 days, 1 year etc). subscription_period has the duration/period.
     			$subs_period      = $level_row->subscription_period;
     			$subs_period_unit = SwpmMembershipLevel::get_level_duration_type_string( $level_row->subscription_duration_type );
    
     			swpm_debug_log_subsc( 'This is a level with "duration" type expiry. Duration period: ' . $subs_period . ', Unit: ' . $subs_period_unit, true );
     			swpm_debug_log_subsc( 'Nothing to do here. The account will expire after the duration time is over.', true );
    
     			// TODO Later as an improvement. If you wanted to segment the members who have unsubscribed, you can set the account status to "unsubscribed" here.
     			// Make sure the cronjob to do expiry check and deactivate the member accounts treat this status as if it is "active".
     		}
    
     		$ipn_data['member_id'] = $member_id;
     		do_action( 'swpm_subscription_payment_cancelled', $ipn_data ); // Hook for recurring payment received.
     	} else {
     		swpm_debug_log_subsc( 'No associated active member record found for this notification.', false );
     		return;
     	}
     }

    2. Since you provide hooks, you should pass the entire Stripe object in $ipn_data.

    Thank you

    January 27, 2022 at 7:37 am #23530
    admin
    Keymaster

    The following function is also used by PayPal and Braintree gateways. Don’t modify it with Stripe specific things (then our PayPal users will start to complain):

    
    swpm_handle_subsc_cancel_stand_alone
    

    Things are done in our plugin keeping in mind that we want to keep this plugin as lightweight and simple as possible. We don’t want to capture too much data that is already captured and available in the merchant’s Stripe or the PayPal account. A user can search within the Stripe or PayPal account to find additional details.

    Please tell me the following only so I can reproduce the issue on my site. Then I will know if our plugin can handle that or not. If I cannot reproduce the issue, I can’t get the full picture.

    – What is the use case?
    – What action I need to do with my subscription payment (in stripe account) to generate the event?
    – What parameters are being used in the payment button?
    – What version of Stripe API are you using? Depending on the API version, Stripe will send different data to the webhook.
    – When a subscription is cancelled, it will cancel the corresponding account. I have tested this just now and it works. On your site it can fail due to many other factors.
    – When a refund is issued (partial or full), the charge.refunded webhook should be triggered (which the plugin will handle).
    – Stripes webhook sends many events. We only handle a limited number of them. That’s intentional (in the context of keeping the plugin’s operation simple to make sure we can maintain it in the future). This is why the exact use case is very important. When I can reproduce the exact condition on my site, I will be able to assess things better.

  • Author
    Posts
Viewing 5 posts - 1 through 5 (of 5 total)
  • You must be logged in to reply to this topic.
Log In

Please read this message before using our plugin.

Search

Featured Addons and Extensions

  • Membership Form Builder Addon
  • Member Directory Listing Addon
  • WooCommerce Payment Integration
  • Member Data Exporter Addon

Documentation

  • Documentation Index Page

Copyright © 2026 | Simple Membership Plugin | Privacy Policy