Forum Replies Created
-
AuthorPosts
-
landreww
ParticipantThank 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/” 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
-
AuthorPosts