swpm-stripe-subscription-ipn.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <?php
  2. require SIMPLE_WP_MEMBERSHIP_PATH . 'ipn/swpm_handle_subsc_ipn.php';
  3. class SwpmStripeSubscriptionIpnHandler {
  4. public function __construct() {
  5. $this->handle_stripe_ipn();
  6. }
  7. public function handle_stripe_ipn() {
  8. /*
  9. * [Imp] This comment explains how this script handles both the first time HTTP Post after payment and the webhooks.
  10. * If the "hook" query arg is set then that means it is a webhook notification. It will be used for certain actions like (update, cancel, refund, etc). Others will be ignored.
  11. * The first time payment in browser is handled via HTTP POST (when the "hook" query arg is not set).
  12. */
  13. if ( isset( $_GET['hook'] ) ) {
  14. // This is Webhook notification from Stripe.
  15. // This webhook is used for all recurring payment notification (Legacy and SCA ones).
  16. // TODO: add Webhook Signing Secret verification
  17. // To do this, we need to get customer ID, retreive its details from Stripe, get button_id from metadata
  18. // and see if the button has Signing Secret option set. If it is - we need to check signatures
  19. // More details here: https://stripe.com/docs/webhooks#signatures
  20. $input = @file_get_contents( 'php://input' );
  21. if ( empty( $input ) ) {
  22. SwpmLog::log_simple_debug( 'Stripe subscription webhook sent empty data or page was accessed directly. Aborting.', false );
  23. echo 'Empty Webhook data received.';
  24. die;
  25. }
  26. // SwpmLog::log_simple_debug($input, true);
  27. $event_json = json_decode( $input );
  28. $type = $event_json->type;
  29. SwpmLog::log_simple_debug( sprintf( 'Stripe subscription webhook received: %s. Checking if we need to handle this webhook.', $type ), true );
  30. if ( 'customer.subscription.deleted' === $type || 'charge.refunded' === $type ) {
  31. // Subscription expired or refunded event
  32. //SwpmLog::log_simple_debug( sprintf( 'Stripe Subscription Webhook %s received. Processing request...', $type ), true );
  33. // Let's form minimal ipn_data array for swpm_handle_subsc_cancel_stand_alone
  34. $customer = $event_json->data->object->customer;
  35. $subscr_id = $event_json->data->object->id;
  36. $ipn_data = array();
  37. $ipn_data['subscr_id'] = $subscr_id;
  38. $ipn_data['parent_txn_id'] = $customer;
  39. swpm_handle_subsc_cancel_stand_alone( $ipn_data );
  40. }
  41. if ( $type == 'customer.subscription.updated' ) {
  42. // Subscription updated webhook
  43. // Let's form minimal ipn_data array
  44. $customer = $event_json->data->object->customer;
  45. $subscr_id = $event_json->data->object->id;
  46. $ipn_data = array();
  47. $ipn_data['subscr_id'] = $subscr_id;
  48. $ipn_data['parent_txn_id'] = $customer;
  49. swpm_update_member_subscription_start_date_if_applicable( $ipn_data );
  50. }
  51. if ( $type === 'invoice.payment_succeeded' ) {
  52. $billing_reason = isset( $event_json->data->object->billing_reason ) ? $event_json->data->object->billing_reason : '';
  53. if ( $billing_reason == 'subscription_cycle' ) {
  54. //This is recurring/subscription payment invoice
  55. SwpmLog::log_simple_debug( sprintf( 'Stripe invoice.payment_succeeded webhook for subscription_cycle. This is a successful subscription charge. Capturing payment data.' ), true );
  56. $sub_id = $event_json->data->object->subscription;
  57. //$cust_id = $event_json->data->object->billing_reason;
  58. //$date = $event_json->data->object->date;
  59. $price_in_cents = $event_json->data->object->amount_paid; //amount in cents
  60. $currency_code = $event_json->data->object->currency;
  61. $zero_cents = unserialize( SIMPLE_WP_MEMBERSHIP_STRIPE_ZERO_CENTS );
  62. if ( in_array( $currency_code, $zero_cents, true ) ) {
  63. $payment_amount = $price_in_cents;
  64. } else {
  65. $payment_amount = $price_in_cents / 100;// The amount (in cents). This value is used in Stripe API.
  66. }
  67. $payment_amount = floatval( $payment_amount );
  68. // Let's try to get first_name and last_name from full name
  69. $full_name = $event_json->data->object->customer_name;
  70. $name_pieces = explode( ' ', $full_name, 2 );
  71. $first_name = $name_pieces[0];
  72. if ( ! empty( $name_pieces[1] ) ) {
  73. $last_name = $name_pieces[1];
  74. }
  75. //Retrieve the member record for this subscription
  76. $member_record = SwpmMemberUtils::get_user_by_subsriber_id( $sub_id );
  77. if ( $member_record ) {
  78. // Found a member record
  79. $member_id = $member_record->member_id;
  80. $membership_level_id = $member_record->membership_level;
  81. if ( empty( $first_name ) ) {
  82. $first_name = $member_record->first_name;
  83. }
  84. if ( empty( $last_name ) ) {
  85. $last_name = $member_record->last_name;
  86. }
  87. } else {
  88. SwpmLog::log_simple_debug( 'Could not find an existing member record for the given subscriber ID: ' . $sub_id . '. This user profile may have been deleted.', false );
  89. $member_id = '';
  90. $membership_level_id = '';
  91. }
  92. //Create the custom field
  93. $custom_field_value = 'subsc_ref=' . $membership_level_id;
  94. $custom_field_value .= '&swpm_id=' . $member_id;
  95. // Create the $ipn_data array.
  96. $ipn_data = array();
  97. $ipn_data['mc_gross'] = $payment_amount;
  98. $ipn_data['first_name'] = $first_name;
  99. $ipn_data['last_name'] = $last_name;
  100. $ipn_data['payer_email'] = $event_json->data->object->customer_email;
  101. $ipn_data['membership_level'] = $membership_level_id;
  102. $ipn_data['txn_id'] = $event_json->data->object->charge;
  103. $ipn_data['subscr_id'] = $sub_id;
  104. $ipn_data['swpm_id'] = $member_id;
  105. $ipn_data['ip'] = '';
  106. $ipn_data['custom'] = $custom_field_value;
  107. $ipn_data['gateway'] = 'stripe-sca-subs';
  108. $ipn_data['status'] = 'subscription';
  109. //TODO - Maybe handle the user access start date updating here (instead of "customer.subscription.updated" hook).
  110. //swpm_update_member_subscription_start_date_if_applicable( $ipn_data );
  111. // Save the transaction record
  112. SwpmTransactions::save_txn_record( $ipn_data );
  113. SwpmLog::log_simple_debug( 'Transaction data saved for Stripe subscription notification.', true );
  114. }
  115. }
  116. //End of the webhook notification execution.
  117. //Give 200 status then exit out.
  118. http_response_code( 200 ); // Tells Stripe we received this notification
  119. return;
  120. }
  121. //The following will get executed only for DIRECT post (not webhooks). So it is executed at the time of payment in the browser (via HTTP POST). When the "hook" query arg is not set.
  122. SwpmLog::log_simple_debug( 'Stripe subscription IPN received. Processing request...', true );
  123. // SwpmLog::log_simple_debug(print_r($_REQUEST, true), true);//Useful for debugging purpose
  124. // Include the Stripe library.
  125. SwpmMiscUtils::load_stripe_lib();
  126. // Read and sanitize the request parameters.
  127. $button_id = sanitize_text_field( $_REQUEST['item_number'] );
  128. $button_id = absint( $button_id );
  129. $button_title = sanitize_text_field( $_REQUEST['item_name'] );
  130. $stripe_token = filter_input( INPUT_POST, 'stripeToken', FILTER_SANITIZE_STRING );
  131. $stripe_token_type = filter_input( INPUT_POST, 'stripeTokenType', FILTER_SANITIZE_STRING );
  132. $stripe_email = filter_input( INPUT_POST, 'stripeEmail', FILTER_SANITIZE_EMAIL );
  133. // Retrieve the CPT for this button
  134. $button_cpt = get_post( $button_id );
  135. if ( ! $button_cpt ) {
  136. // Fatal error. Could not find this payment button post object.
  137. SwpmLog::log_simple_debug( 'Fatal Error! Failed to retrieve the payment button post object for the given button ID: ' . $button_id, false );
  138. wp_die( esc_html( sprintf( 'Fatal Error! Payment button (ID: %d) does not exist. This request will fail.', $button_id ) ) );
  139. }
  140. $plan_id = get_post_meta( $button_id, 'stripe_plan_id', true );
  141. $descr = 'Subscription to "' . $plan_id . '" plan';
  142. $membership_level_id = get_post_meta( $button_id, 'membership_level_id', true );
  143. // Validate and verify some of the main values.
  144. // Validation passed. Go ahead with the charge.
  145. // Sandbox and other settings
  146. $settings = SwpmSettings::get_instance();
  147. $sandbox_enabled = $settings->get_value( 'enable-sandbox-testing' );
  148. //API keys
  149. $api_keys = SwpmMiscUtils::get_stripe_api_keys_from_payment_button( $button_id, ! $sandbox_enabled );
  150. // Set secret API key in the Stripe library
  151. \Stripe\Stripe::setApiKey( $api_keys['secret'] );
  152. // Get the credit card details submitted by the form
  153. $token = $stripe_token;
  154. // Create the charge on Stripe's servers - this will charge the user's card
  155. try {
  156. $customer = \Stripe\Customer::create(
  157. array(
  158. 'description' => $descr,
  159. 'email' => $stripe_email,
  160. 'source' => $token,
  161. 'plan' => $plan_id,
  162. 'trial_from_plan' => 'true',
  163. )
  164. );
  165. } catch ( Exception $e ) {
  166. SwpmLog::log_simple_debug( 'Error occurred during Stripe Subscribe. ' . $e->getMessage(), false );
  167. $body = $e->getJsonBody();
  168. $error = $body['error'];
  169. $error_string = wp_json_encode( $error );
  170. SwpmLog::log_simple_debug( 'Error details: ' . $error_string, false );
  171. wp_die( esc_html( 'Stripe subscription Error! ' . $e->getMessage() . $error_string ) );
  172. }
  173. // Everything went ahead smoothly with the charge.
  174. SwpmLog::log_simple_debug( 'Stripe subscription successful.', true );
  175. // let's add button_id to metadata
  176. $customer->metadata = array( 'button_id' => $button_id );
  177. try {
  178. $customer->save();
  179. } catch ( Exception $e ) {
  180. SwpmLog::log_simple_debug( 'Error occurred during Stripe customer metadata update. ' . $e->getMessage(), false );
  181. $body = $e->getJsonBody();
  182. SwpmLog::log_simple_debug( 'Error details: ' . $error_string, false );
  183. }
  184. // Grab customer ID and set it as the transaction ID.
  185. $txn_id = $customer->id; // $charge->balance_transaction;
  186. // Grab subscription ID
  187. $subscr_id = $customer->subscriptions->data[0]->id;
  188. $custom = sanitize_text_field( $_REQUEST['custom'] );
  189. $custom_var = SwpmTransactions::parse_custom_var( $custom );
  190. $swpm_id = isset( $custom_var['swpm_id'] ) ? $custom_var['swpm_id'] : '';
  191. $payment_amount = $customer->subscriptions->data[0]->plan->amount / 100;
  192. // Create the $ipn_data array.
  193. $ipn_data = array();
  194. $ipn_data['mc_gross'] = $payment_amount;
  195. $ipn_data['first_name'] = '';
  196. $ipn_data['last_name'] = '';
  197. $ipn_data['payer_email'] = $stripe_email;
  198. $ipn_data['membership_level'] = $membership_level_id;
  199. $ipn_data['txn_id'] = $txn_id;
  200. $ipn_data['subscr_id'] = $subscr_id;
  201. $ipn_data['swpm_id'] = $swpm_id;
  202. $ipn_data['ip'] = $custom_var['user_ip'];
  203. $ipn_data['custom'] = $custom;
  204. $ipn_data['gateway'] = 'stripe';
  205. $ipn_data['status'] = 'completed';
  206. $ipn_data['address_street'] = '';
  207. $ipn_data['address_city'] = '';
  208. $ipn_data['address_state'] = '';
  209. $ipn_data['address_zipcode'] = '';
  210. $ipn_data['country'] = '';
  211. $ipn_data['payment_button_id'] = $button_id;
  212. $ipn_data['is_live'] = ! $sandbox_enabled;
  213. // Handle the membership signup related tasks.
  214. swpm_handle_subsc_signup_stand_alone( $ipn_data, $membership_level_id, $txn_id, $swpm_id );
  215. // Save the transaction record
  216. SwpmTransactions::save_txn_record( $ipn_data );
  217. SwpmLog::log_simple_debug( 'Transaction data saved.', true );
  218. // Trigger the stripe IPN processed action hook (so other plugins can can listen for this event).
  219. do_action( 'swpm_stripe_ipn_processed', $ipn_data );
  220. do_action( 'swpm_payment_ipn_processed', $ipn_data );
  221. // Redirect the user to the return URL (or to the homepage if a return URL is not specified for this payment button).
  222. $return_url = get_post_meta( $button_id, 'return_url', true );
  223. if ( empty( $return_url ) ) {
  224. $return_url = SIMPLE_WP_MEMBERSHIP_SITE_HOME_URL;
  225. }
  226. SwpmLog::log_simple_debug( 'Redirecting customer to: ' . $return_url, true );
  227. SwpmLog::log_simple_debug( 'End of Stripe subscription IPN processing.', true, true );
  228. SwpmMiscUtils::redirect_to_url( $return_url );
  229. }
  230. }
  231. $swpm_stripe_subscription_ipn = new SwpmStripeSubscriptionIpnHandler();