PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.9.6
Subscriptions for WooCommerce with Stripe Recurring Payments v1.9.6
2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
subscription / includes / Illuminate / Gateways / Stripe / Stripe.php

Stripe.php in Subscriptions for WooCommerce with Stripe Recurring Payments 1.9.6, at includes/Illuminate/Gateways/Stripe/Stripe.php

462 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Stripe integration helpers for subscription auto-renewals.
4 *
5 * Ensures payment methods are saved with mandates (SEPA, etc.) so that
6 * off-session renewals can be charged automatically by Stripe.
7 *
8 * @package SpringDevs\Subscription
9 */
10
11 namespace SpringDevs\Subscription\Illuminate\Gateways\Stripe;
12
13 use SpringDevs\Subscription\Illuminate\Helper;
14
15 /**
16 * Class Stripe
17 *
18 * @package SpringDevs\SubscriptionPro\Illuminate
19 */
20 class Stripe extends \WC_Stripe_Payment_Gateway {
21
22 /**
23 * Subscriptions supported Stripe payment methods.
24 */
25 public const WPSUBS_SUPPORTED_METHODS = [ 'stripe', 'stripe_ideal', 'stripe_sepa', 'sepa_debit', 'stripe_bancontact' ];
26
27 /**
28 * Mandate needed methods.
29 */
30 public const WPSUBS_MANDATE_NEEDED_METHODS = [ 'stripe_ideal', 'stripe_sepa', 'sepa_debit', 'stripe_bancontact' ];
31
32 /**
33 * Initialize the class
34 */
35 public function __construct() {
36 // Hook into WPSubscription renewal events
37 add_action( 'subscrpt_after_create_renew_order', array( $this, 'after_create_renew_order' ), 10, 3 );
38 add_filter( 'subscrpt_before_saving_renewal_order', array( $this, 'copy_stripe_metadata' ), 10, 3 );
39
40 add_filter( 'wc_stripe_payment_metadata', array( $this, 'add_payment_metadata' ), 10, 2 );
41
42 // Ensure a reusable payment method is stored for subscription checkouts (needed for iDEAL/SEPA auto-renewals).
43 add_filter( 'wc_stripe_force_save_payment_method', array( $this, 'force_save_payment_method_for_subscriptions' ), 10, 2 );
44
45 // Modify create intent request to add setup_future_usage and customer when needed.
46 add_filter( 'wc_stripe_generate_create_intent_request', [ $this, 'modify_create_intent_request_for_subscriptions' ], 20, 3 );
47 }
48
49 /**
50 * Process stripe auto renewal process.
51 *
52 * @param \WC_Order $new_order New Order.
53 * @param \WC_Order $old_order Old Order.
54 * @param int $subscription_id Subscription ID.
55 */
56 public function after_create_renew_order( $new_order, $old_order, $subscription_id ) {
57 $is_auto_renew = get_post_meta( $subscription_id, '_subscrpt_auto_renew', true );
58 $is_auto_renew = in_array( $is_auto_renew, [ 1,'1' ], true );
59
60 $is_global_auto_renew = get_option( 'wp_subscription_stripe_auto_renew', '1' );
61 $is_global_auto_renew = in_array( $is_global_auto_renew, [ 1,'1' ], true );
62
63 $stripe_supported_methods = self::WPSUBS_SUPPORTED_METHODS;
64 $old_method = $old_order->get_payment_method();
65 $is_stripe_pm = ! empty( $old_method ) && in_array( $old_method, $stripe_supported_methods, true );
66
67 $has_stripe_meta = ! empty( $old_order->get_meta( '_stripe_customer_id' ) ) || ! empty( $old_order->get_meta( '_stripe_source_id' ) );
68
69 $stripe_enabled = ( ( $is_stripe_pm || $has_stripe_meta ) && $is_auto_renew && $is_global_auto_renew && subscrpt_is_auto_renew_enabled() );
70
71 if ( ! $stripe_enabled ) {
72 $log_message = "Stripe auto renewal not enabled. [ Subscription: {$subscription_id}, Order #{$new_order->get_id()} ]";
73 subscrpt_write_log( $log_message );
74
75 // Log details for debugging.
76 subscrpt_write_debug_log( $log_message );
77 subscrpt_write_debug_log( 'is_auto_renew: ' . ( $is_auto_renew ? 'true' : 'false' ) );
78 subscrpt_write_debug_log( 'is_global_auto_renew: ' . ( $is_global_auto_renew ? 'true' : 'false' ) );
79 subscrpt_write_debug_log( 'is_stripe_payment_method: ' . ( $is_stripe_pm ? 'true' : 'false' ) . ' (old method: ' . $old_method . ')' );
80 subscrpt_write_debug_log( 'has_stripe_meta: ' . ( $has_stripe_meta ? 'true' : 'false' ) );
81 return;
82 }
83
84 $this->pay_renew_order( $new_order );
85 }
86
87 /**
88 * Copy Stripe metadata from old order to renewal order
89 *
90 * @param \WC_Order $new_order Renewal order.
91 * @param \WC_Order $old_order Parent order.
92 * @param int $subscription_id Subscription ID.
93 */
94 public function copy_stripe_metadata( $new_order, $old_order, $subscription_id ) {
95 $stripe_supported_methods = self::WPSUBS_SUPPORTED_METHODS;
96 $old_method = $old_order->get_payment_method();
97 $is_stripe_pm = ! empty( $old_method ) && in_array( $old_method, $stripe_supported_methods, true );
98
99 if ( ! $is_stripe_pm ) {
100 return $new_order;
101 }
102
103 Helper::clone_stripe_metadata_for_renewal( $subscription_id, $old_order, $new_order );
104
105 // Store Stripe subscription ID if available
106 $stripe_subscription_id = $old_order->get_meta( '_stripe_subscription_id' );
107 if ( $stripe_subscription_id ) {
108 $new_order->update_meta_data( '_stripe_subscription_id', $stripe_subscription_id );
109 }
110
111 return $new_order;
112 }
113
114 /**
115 * Pay renewal Order
116 *
117 * @param \WC_Order $renewal_order Renewal order.
118 * @throws \WC_Stripe_Exception $e exception.
119 */
120 public function pay_renew_order( $renewal_order ) {
121 subscrpt_write_log( "Processing renewal order #{$renewal_order->get_id()} for payment." );
122 subscrpt_write_debug_log( "Processing renewal order #{$renewal_order->get_id()} for payment." );
123
124 try {
125 $stripe_order_helper = new \WC_Stripe_Order_Helper();
126 $stripe_order_helper->validate_minimum_order_amount( $renewal_order );
127
128 $amount = $renewal_order->get_total();
129 $order_id = $renewal_order->get_id();
130
131 // Get source from order.
132 $prepared_source = $this->prepare_order_source( $renewal_order );
133 if ( ! $prepared_source->customer ) {
134 subscrpt_write_log( "Customer not found for renewal order #{$renewal_order->get_id()}. Skipping payment." );
135 return new \WP_Error( 'stripe_error', __( 'Customer not found', 'subscription' ) );
136 }
137
138 \WC_Stripe_Logger::info( "Begin processing subscription payment for order {$order_id} for the amount of {$amount}" );
139
140 $intent = $this->create_intent( $renewal_order, $prepared_source );
141
142 if ( empty( $intent->error ) ) {
143 $stripe_order_helper->lock_order_payment( $renewal_order, $intent );
144 // Only confirm if Stripe still requires confirmation.
145 if ( \WC_Stripe_Intent_Status::REQUIRES_CONFIRMATION === $intent->status ) {
146 $intent = $this->confirm_intent( $intent, $renewal_order, $prepared_source );
147 }
148 }
149
150 if ( ! empty( $intent->error ) ) {
151 $this->maybe_remove_non_existent_customer( $intent->error, $renewal_order );
152
153 $stripe_order_helper->unlock_order_payment( $renewal_order );
154 $this->throw_localized_message( $intent, $renewal_order );
155 }
156
157 if ( ! empty( $intent ) ) {
158 // Use the last charge within the intent to proceed.
159 $response = $this->get_latest_charge_from_intent( $intent );
160 $this->process_response( $response, $renewal_order );
161 }
162 $stripe_order_helper->unlock_order_payment( $renewal_order );
163
164 } catch ( \WC_Stripe_Exception $e ) {
165 \WC_Stripe_Logger::error( 'Error: ' . $e->getMessage() );
166
167 $log_message = "Error processing renewal order #{$renewal_order->get_id()}: " . $e->getMessage();
168 subscrpt_write_log( $log_message );
169 subscrpt_write_debug_log( $log_message );
170
171 do_action( 'wc_gateway_stripe_process_payment_error', $e, $renewal_order );
172
173 // Get subscription ID.
174 $subscription = Helper::get_subscriptions_from_order( $renewal_order->get_id() ?? 0 );
175 $subscription = reset( $subscription );
176 $subscription_id = $subscription->subscription_id ?? 0;
177
178 // Trigger failed payment mail.
179 do_action( 'subscrpt_payment_failure_email_notification', $subscription_id );
180 }
181 }
182
183 /**
184 * Confirms an intent if it is the `requires_confirmation` state with SEPA mandate support.
185 *
186 * @param object $intent The intent to confirm.
187 * @param \WC_Order $order The order that the intent is associated with.
188 * @param object $prepared_source The source that is being charged.
189 * @return object Either an error or the updated intent.
190 */
191 public function confirm_intent( $intent, $order, $prepared_source ) {
192 if ( \WC_Stripe_Intent_Status::REQUIRES_CONFIRMATION !== $intent->status ) {
193 return $intent;
194 }
195
196 // Build confirm request and include SEPA mandate_data when needed.
197 $confirm_request = \WC_Stripe_Helper::add_payment_method_to_request_array( $prepared_source->source, array() );
198
199 $payment_method_types = array();
200 if ( isset( $intent->payment_method_types ) && is_array( $intent->payment_method_types ) ) {
201 $payment_method_types = $intent->payment_method_types;
202 } elseif ( isset( $prepared_source->source_object->type ) ) {
203 $payment_method_types = array( $prepared_source->source_object->type );
204 }
205
206 if ( in_array( 'sepa_debit', $payment_method_types, true ) ) {
207 $confirm_request['mandate_data'] = array(
208 'customer_acceptance' => array(
209 'type' => 'offline',
210 ),
211 );
212 }
213
214 $level3_data = $this->get_level3_data_from_order( $order );
215 $confirmed_intent = \WC_Stripe_API::request_with_level3_data(
216 $confirm_request,
217 "payment_intents/$intent->id/confirm",
218 $level3_data,
219 $order
220 );
221
222 if ( ! empty( $confirmed_intent->error ) ) {
223 return $confirmed_intent;
224 }
225
226 // Save a note about the status of the intent.
227 $order_id = $order->get_id();
228 if ( \WC_Stripe_Intent_Status::SUCCEEDED === $confirmed_intent->status ) {
229 \WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
230 } elseif ( \WC_Stripe_Intent_Status::REQUIRES_ACTION === $confirmed_intent->status ) {
231 \WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id requires authentication for order $order_id" );
232 }
233
234 return $confirmed_intent;
235 }
236
237 /**
238 * Generates the request when creating a new payment intent.
239 *
240 * @param \WC_Order $order The order that is being paid for.
241 * @param object $prepared_source The source that is used for the payment.
242 * @return array The arguments for the request.
243 */
244 public function generate_create_intent_request( $order, $prepared_source ) {
245 // The request for a charge contains metadata for the intent.
246 $full_request = $this->generate_payment_request( $order, $prepared_source );
247
248 $payment_method_types = array( 'card' );
249 if ( isset( $prepared_source->source_object->type ) ) {
250 $payment_method_types = array( $prepared_source->source_object->type );
251 }
252
253 // Determine capture method safely; default to 'automatic'.
254 $requires_automatic_capture = in_array( 'sepa_debit', $payment_method_types, true );
255 $capture_method = 'automatic';
256 if ( ! $requires_automatic_capture && isset( $full_request['capture'] ) ) {
257 $capture_method = ( 'true' === $full_request['capture'] ) ? 'automatic' : 'manual';
258 }
259
260 $currency = strtolower( $order->get_currency() );
261
262 $request = array(
263 'amount' => \WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency ),
264 'currency' => $currency,
265 'description' => $full_request['description'],
266 'metadata' => $full_request['metadata'],
267 'capture_method' => $capture_method,
268 'payment_method_types' => $payment_method_types,
269 );
270
271 $request = \WC_Stripe_Helper::add_payment_method_to_request_array( $prepared_source->source, $request );
272
273 $force_save_source = apply_filters( 'wc_stripe_force_save_payment_method', false, $order->get_id() );
274
275 // Only ask Stripe to set up future usage when we actually have a Stripe customer
276 // (logged-in user or a customer created for this order). For guest + iDEAL, this can
277 // leave orders pending if webhooks are not completing the flow.
278 $has_stripe_customer = ! empty( $prepared_source->customer );
279
280 if ( $has_stripe_customer && ( $this->save_payment_method_requested() || $this->has_subscription( $order->get_id() ) || $force_save_source ) ) {
281 $request['setup_future_usage'] = 'off_session';
282 $request['metadata']['save_payment_method'] = 'true';
283 }
284
285 // For renewal orders, do not set setup_future_usage to avoid mandate_data requirement on confirmation.
286 if ( $this->is_subscription_renewal_order( $order->get_id() ) && isset( $request['setup_future_usage'] ) ) {
287 unset( $request['setup_future_usage'] );
288 }
289
290 if ( $prepared_source->customer ) {
291 $request['customer'] = $prepared_source->customer;
292 }
293
294 if ( isset( $full_request['statement_descriptor_suffix'] ) ) {
295 $request['statement_descriptor_suffix'] = $full_request['statement_descriptor_suffix'];
296 }
297
298 if ( isset( $full_request['shipping'] ) ) {
299 $request['shipping'] = $full_request['shipping'];
300 }
301
302 if ( isset( $full_request['receipt_email'] ) ) {
303 $request['receipt_email'] = $full_request['receipt_email'];
304 }
305
306 /**
307 * Filter the return value of the WC_Payment_Gateway_CC::generate_create_intent_request.
308 *
309 * @since 3.1.0
310 * @param array $request
311 * @param WC_Order $order
312 * @param object $source
313 */
314 return apply_filters( 'wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
315 }
316
317 /**
318 * Add metadata to stripe payment.
319 *
320 * @param array $metadata Metadata.
321 * @param \WC_Order $order Order.
322 *
323 * @return array
324 */
325 public function add_payment_metadata( array $metadata, \WC_Order $order ): array {
326
327 if ( ! subscrpt_is_auto_renew_enabled() ) {
328 return $metadata;
329 }
330
331 global $wpdb;
332 $recurring = false;
333 $renewal_limit = null;
334 foreach ( $order->get_items() as $order_item ) {
335 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
336 // @phpcs:ignore
337 $relation = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE order_id=%d AND order_item_id=%d', array( $table_name, $order->get_id(), $order_item->get_id() ) ) );
338
339 if ( 0 < count( $relation ) ) {
340 $relation = $relation[0];
341 $is_auto_renew = get_post_meta( (int) $relation->subscription_id, '_subscrpt_auto_renew', true );
342
343 // Get renewal limit from product meta (handles variations)
344 $max_payments = subscrpt_get_max_payments( (int) $relation->subscription_id );
345 $renewal_limit = $max_payments ? $max_payments : 0;
346
347 if ( in_array( $is_auto_renew, array( 1, '1' ), true ) && in_array( $relation->type, array( 'early-renew', 'renew' ), true ) ) {
348 $recurring = true;
349 break;
350 }
351 }
352 }
353
354 if ( $recurring ) {
355 $metadata += array(
356 'payment_type' => 'recurring',
357 );
358 if ( null !== $renewal_limit ) {
359 $metadata['renewal_limit'] = $renewal_limit;
360 }
361 }
362
363 return $metadata;
364 }
365
366 /**
367 * Mirror of the above for gateways using wc_stripe_force_save_payment_method filter.
368 *
369 * @param bool $force Whether to force save the payment method.
370 * @param int $order_id Order ID if available during confirmation.
371 * @return bool
372 */
373 public function force_save_payment_method_for_subscriptions( $force, $order_id = 0 ) {
374 if ( $this->cart_has_subscription_items() ) {
375 return true;
376 }
377 if ( $order_id && $this->order_has_subscription_relation( (int) $order_id ) ) {
378 return true;
379 }
380 return $force;
381 }
382
383 /**
384 * Check if current cart contains subscription items added by this plugin.
385 *
386 * @return bool
387 */
388 private function cart_has_subscription_items(): bool {
389 if ( function_exists( 'WC' ) && WC()->cart ) {
390 $cart_items = WC()->cart->get_cart_contents() ?? [];
391 $recurs = Helper::get_recurrs_from_cart( $cart_items );
392
393 if ( ! empty( $recurs ) ) {
394 return true;
395 }
396 }
397 return false;
398 }
399
400 /**
401 * Determine if a given order has subscription relation rows in our mapping table.
402 *
403 * @param int $order_id The WooCommerce order ID to check.
404 * @return bool
405 */
406 private function order_has_subscription_relation( int $order_id ): bool {
407 $histories = Helper::get_subscriptions_from_order( $order_id );
408 return ! empty( $histories );
409 }
410
411 /**
412 * Detect if given order id is a renewal order created by this plugin.
413 *
414 * @param int $order_id Order ID.
415 * @return bool
416 */
417 private function is_subscription_renewal_order( $order_id ) {
418 global $wpdb;
419 $table_name = $wpdb->prefix . 'subscrpt_order_relation';
420 // @phpcs:ignore
421 $row = $wpdb->get_row( $wpdb->prepare( 'SELECT type FROM %i WHERE order_id=%d ORDER BY id DESC', array( $table_name, $order_id ) ) );
422 return ( $row && isset( $row->type ) && 'renew' === $row->type );
423 }
424
425 /**
426 * Modify create intent request to add setup_future_usage and customer when needed.
427 *
428 * @param array $request The arguments for the request.
429 * @param \WC_Order $order The order that is being paid for.
430 * @param object $prepared_source The source that is used for the payment.
431 */
432 public function modify_create_intent_request_for_subscriptions( $request, $order, $prepared_source ) {
433 $is_subscription_order = $this->order_has_subscription_relation( $order->get_id() );
434 $is_renewal_order = $this->is_subscription_renewal_order( $order->get_id() );
435
436 // Don't add setup_future_usage for renewal orders (payment method already saved)
437 if ( ! $is_subscription_order || $is_renewal_order ) {
438 return $request;
439 }
440
441 $request['setup_future_usage'] = 'off_session';
442 $request['metadata']['save_payment_method'] = 'true';
443
444 // Ensure we have a customer for future payments
445 if ( ! empty( $prepared_source->customer ) ) {
446 $request['customer'] = $prepared_source->customer;
447 }
448
449 if ( isset( $request['confirm'] ) && true === $request['confirm'] ) {
450 if ( in_array( $order->get_payment_method(), self::WPSUBS_MANDATE_NEEDED_METHODS, true ) ) {
451 $request['mandate_data'] = [
452 'customer_acceptance' => [
453 'type' => 'offline',
454 ],
455 ];
456 }
457 }
458
459 return $request;
460 }
461 }
462