WC_Stripe_Subs_Compat   F
last analyzed

Complexity

Total Complexity 108

Size/Duplication

Total Lines 711
Duplicated Lines 28.55 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
dl 203
loc 711
rs 1.889
c 0
b 0
f 0
wmc 108
lcom 1
cbo 5

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 30 2
A maybe_hide_save_checkbox() 0 7 2
A has_subscription() 0 3 4
A is_subs_change_payment() 0 3 2
A display_update_subs_payment_checkout() 19 19 4
A differentiate_change_payment_method_form() 0 3 1
A change_subs_payment_method() 0 20 2
A process_payment() 12 12 3
A generate_create_intent_request() 0 13 2
A scheduled_subscription_payment() 0 3 1
F process_subscription_payment() 54 150 24
B save_source_to_order() 20 20 6
A delete_resubscribe_meta() 9 9 1
A delete_renewal_meta() 0 9 1
A update_failing_payment_method() 0 4 1
A add_subscription_payment_meta() 0 28 2
B validate_subscription_payment_meta() 24 24 11
F maybe_render_subscription_payment_method() 25 78 20
A remove_order_pay_var() 0 7 2
A restore_order_pay_var() 0 6 2
A has_authentication_already_failed() 0 29 5
A redirect_after_early_renewal() 0 10 1
A handle_intent_verification_success() 8 8 2
A handle_intent_verification_failure() 8 8 2
A handle_add_payment_method_success() 24 24 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WC_Stripe_Subs_Compat often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WC_Stripe_Subs_Compat, and based on these observations, apply Extract Interface, too.

1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * Compatibility class for Subscriptions.
8
 *
9
 * @extends WC_Gateway_Stripe
10
 */
11
class WC_Stripe_Subs_Compat extends WC_Gateway_Stripe {
12
	/**
13
	 * Constructor
14
	 */
15
	public function __construct() {
16
		parent::__construct();
17
18
		if ( class_exists( 'WC_Subscriptions_Order' ) ) {
19
			add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, array( $this, 'scheduled_subscription_payment' ), 10, 2 );
20
			add_action( 'wcs_resubscribe_order_created', array( $this, 'delete_resubscribe_meta' ), 10 );
21
			add_action( 'wcs_renewal_order_created', array( $this, 'delete_renewal_meta' ), 10 );
22
			add_action( 'woocommerce_subscription_failing_payment_method_updated_stripe', array( $this, 'update_failing_payment_method' ), 10, 2 );
23
			add_action( 'wc_stripe_cards_payment_fields', array( $this, 'display_update_subs_payment_checkout' ) );
24
			add_action( 'wc_stripe_add_payment_method_' . $this->id . '_success', array( $this, 'handle_add_payment_method_success' ), 10, 2 );
25
			add_action( 'woocommerce_subscriptions_change_payment_before_submit', array( $this, 'differentiate_change_payment_method_form' ) );
26
27
			// display the credit card used for a subscription in the "My Subscriptions" table
28
			add_filter( 'woocommerce_my_subscriptions_payment_method', array( $this, 'maybe_render_subscription_payment_method' ), 10, 2 );
29
30
			// allow store managers to manually set Stripe as the payment method on a subscription
31
			add_filter( 'woocommerce_subscription_payment_meta', array( $this, 'add_subscription_payment_meta' ), 10, 2 );
32
			add_filter( 'woocommerce_subscription_validate_payment_meta', array( $this, 'validate_subscription_payment_meta' ), 10, 2 );
33
			add_filter( 'wc_stripe_display_save_payment_method_checkbox', array( $this, 'maybe_hide_save_checkbox' ) );
34
35
			/*
36
			 * WC subscriptions hooks into the "template_redirect" hook with priority 100.
37
			 * If the screen is "Pay for order" and the order is a subscription renewal, it redirects to the plain checkout.
38
			 * See: https://github.com/woocommerce/woocommerce-subscriptions/blob/99a75687e109b64cbc07af6e5518458a6305f366/includes/class-wcs-cart-renewal.php#L165
39
			 * If we are in the "You just need to authorize SCA" flow, we don't want that redirection to happen.
40
			 */
41
			add_action( 'template_redirect', array( $this, 'remove_order_pay_var' ), 99 );
42
			add_action( 'template_redirect', array( $this, 'restore_order_pay_var' ), 101 );
43
		}
44
	}
45
46
	/**
47
	 * Checks to see if we need to hide the save checkbox field.
48
	 * Because when cart contains a subs product, it will save regardless.
49
	 *
50
	 * @since 4.0.0
51
	 * @version 4.0.0
52
	 */
53
	public function maybe_hide_save_checkbox( $display_tokenization ) {
54
		if ( WC_Subscriptions_Cart::cart_contains_subscription() ) {
55
			return false;
56
		}
57
58
		return $display_tokenization;
59
	}
60
61
	/**
62
	 * Is $order_id a subscription?
63
	 * @param  int  $order_id
64
	 * @return boolean
65
	 */
66
	public function has_subscription( $order_id ) {
67
		return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) );
68
	}
69
70
	/**
71
	 * Checks if page is pay for order and change subs payment page.
72
	 *
73
	 * @since 4.0.4
74
	 * @return bool
75
	 */
76
	public function is_subs_change_payment() {
77
		return ( isset( $_GET['pay_for_order'] ) && isset( $_GET['change_payment_method'] ) );
78
	}
79
80
	/**
81
	 * Displays a checkbox to allow users to update all subs payments with new
82
	 * payment.
83
	 *
84
	 * @since 4.1.11
85
	 */
86 View Code Duplication
	public function display_update_subs_payment_checkout() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
87
		$subs_statuses = apply_filters( 'wc_stripe_update_subs_payment_method_card_statuses', array( 'active' ) );
88
		if (
89
			apply_filters( 'wc_stripe_display_update_subs_payment_method_card_checkbox', true ) &&
90
			wcs_user_has_subscription( get_current_user_id(), '', $subs_statuses ) &&
91
			is_add_payment_method_page()
92
		) {
93
			$label = esc_html( apply_filters( 'wc_stripe_save_to_subs_text', __( 'Update the Payment Method used for all of my active subscriptions.', 'woocommerce-gateway-stripe' ) ) );
94
			$id    = sprintf( 'wc-%1$s-update-subs-payment-method-card', $this->id );
95
			woocommerce_form_field(
96
				$id,
97
				array(
98
					'type'    => 'checkbox',
99
					'label'   => $label,
100
					'default' => apply_filters( 'wc_stripe_save_to_subs_checked', false ),
101
				)
102
			);
103
		}
104
	}
105
106
	/**
107
	 * Updates all active subscriptions payment method.
108
	 *
109
	 * @since 4.1.11
110
	 * @param string $source_id
111
	 * @param object $source_object
112
	 */
113 View Code Duplication
	public function handle_add_payment_method_success( $source_id, $source_object ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
114
		if ( isset( $_POST[ 'wc-' . $this->id . '-update-subs-payment-method-card' ] ) ) {
115
			$all_subs        = wcs_get_users_subscriptions();
116
			$subs_statuses   = apply_filters( 'wc_stripe_update_subs_payment_method_card_statuses', array( 'active' ) );
117
			$stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
118
119
			if ( ! empty( $all_subs ) ) {
120
				foreach ( $all_subs as $sub ) {
121
					if ( $sub->has_status( $subs_statuses ) ) {
122
						WC_Subscriptions_Change_Payment_Gateway::update_payment_method(
123
							$sub,
124
							$this->id,
125
							array(
126
								'post_meta' => array(
127
									'_stripe_source_id' => array( 'value' => $source_id ),
128
									'_stripe_customer_id' => array( 'value' => $stripe_customer->get_id() ),
129
								),
130
							)
131
						);
132
					}
133
				}
134
			}
135
		}
136
	}
137
138
	/**
139
	 * Render a dummy element in the "Change payment method" form (that does not appear in the "Pay for order" form)
140
	 * which can be checked to determine proper SCA handling to apply for each form.
141
	 *
142
	 * @since 4.6.1
143
	 */
144
	public function differentiate_change_payment_method_form() {
145
		echo '<input type="hidden" id="wc-stripe-change-payment-method" />';
146
	}
147
148
	/**
149
	 * Process the payment method change for subscriptions.
150
	 *
151
	 * @since 4.0.4
152
	 * @since 4.1.11 Remove 3DS check as it is not needed.
153
	 * @param int $order_id
154
	 */
155
	public function change_subs_payment_method( $order_id ) {
156
		try {
157
			$subscription    = wc_get_order( $order_id );
158
			$prepared_source = $this->prepare_source( get_current_user_id(), true );
159
160
			$this->maybe_disallow_prepaid_card( $prepared_source );
161
			$this->check_source( $prepared_source );
162
			$this->save_source_to_order( $subscription, $prepared_source );
163
164
			do_action( 'wc_stripe_change_subs_payment_method_success', $prepared_source->source, $prepared_source );
165
166
			return array(
167
				'result'   => 'success',
168
				'redirect' => $this->get_return_url( $subscription ),
169
			);
170
		} catch ( WC_Stripe_Exception $e ) {
171
			wc_add_notice( $e->getLocalizedMessage(), 'error' );
172
			WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
173
		}
174
	}
175
176
	/**
177
	 * Process the payment based on type.
178
	 * @param  int $order_id
179
	 * @return array
180
	 */
181 View Code Duplication
	public function process_payment( $order_id, $retry = true, $force_save_source = false, $previous_error = false, $use_order_source = false ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
182
		if ( $this->has_subscription( $order_id ) ) {
183
			if ( $this->is_subs_change_payment() ) {
184
				return $this->change_subs_payment_method( $order_id );
185
			}
186
187
			// Regular payment with force customer enabled
188
			return parent::process_payment( $order_id, $retry, true, $previous_error, $use_order_source );
189
		} else {
190
			return parent::process_payment( $order_id, $retry, $force_save_source, $previous_error, $use_order_source );
191
		}
192
	}
193
194
	/**
195
	 * Overloads WC_Stripe_Payment_Gateway::generate_create_intent_request() in order to
196
	 * include additional flags, used when setting payment intents up for off-session usage.
197
	 *
198
	 * @param WC_Order $order           The order that is being paid for.
199
	 * @param object   $prepared_source The source that is used for the payment.
200
	 * @return array                    The arguments for the request.
201
	 */
202
	public function generate_create_intent_request( $order, $prepared_source ) {
203
		$request = parent::generate_create_intent_request( $order, $prepared_source );
204
205
		// Non-subscription orders do not need any additional parameters.
206
		if ( ! $this->has_subscription( $order ) ) {
207
			return $request;
208
		}
209
210
		// Let Stripe know that the payment should be prepared for future usage.
211
		$request['setup_future_usage'] = 'off_session';
212
213
		return $request;
214
	}
215
216
	/**
217
	 * Scheduled_subscription_payment function.
218
	 *
219
	 * @param $amount_to_charge float The amount to charge.
220
	 * @param $renewal_order WC_Order A WC_Order object created to record the renewal payment.
221
	 */
222
	public function scheduled_subscription_payment( $amount_to_charge, $renewal_order ) {
223
		$this->process_subscription_payment( $amount_to_charge, $renewal_order, true, false );
224
	}
225
226
	/**
227
	 * Process_subscription_payment function.
228
	 *
229
	 * @since 3.0
230
	 * @since 4.0.4 Add third parameter flag to retry.
231
	 * @since 4.1.0 Add fourth parameter to log previous errors.
232
	 * @param float $amount
233
	 * @param mixed $renewal_order
234
	 * @param bool $retry Should we retry the process?
235
	 * @param object $previous_error
236
	 */
237
	public function process_subscription_payment( $amount, $renewal_order, $retry = true, $previous_error = false ) {
238
		try {
239 View Code Duplication
			if ( $amount * 100 < WC_Stripe_Helper::get_minimum_amount() ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
240
				/* translators: minimum amount */
241
				$message = sprintf( __( 'Sorry, the minimum allowed order total is %1$s to use this payment method.', 'woocommerce-gateway-stripe' ), wc_price( WC_Stripe_Helper::get_minimum_amount() / 100 ) );
242
				throw new WC_Stripe_Exception(
243
					'Error while processing renewal order ' . $renewal_order->get_id() . ' : ' . $message,
244
					$message
245
				);
246
			}
247
248
			$order_id = $renewal_order->get_id();
249
250
			$this->ensure_subscription_has_customer_id( $order_id );
251
252
			// Unlike regular off-session subscription payments, early renewals are treated as on-session payments, involving the customer.
253
			if ( isset( $_REQUEST['process_early_renewal'] ) ) { // wpcs: csrf ok.
254
				$response = parent::process_payment( $order_id, true, false, $previous_error, true );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (process_payment() instead of process_subscription_payment()). Are you sure this is correct? If so, you might want to change this to $this->process_payment().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
255
256
				if( 'success' === $response['result'] && isset( $response['payment_intent_secret'] ) ) {
257
					$verification_url = add_query_arg(
258
						array(
259
							'order'         => $order_id,
260
							'nonce'         => wp_create_nonce( 'wc_stripe_confirm_pi' ),
261
							'redirect_to'   => remove_query_arg( array( 'process_early_renewal', 'subscription_id', 'wcs_nonce' ) ),
262
							'early_renewal' => true,
263
						),
264
						WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
265
					);
266
267
					echo wp_json_encode( array(
268
						'stripe_sca_required' => true,
269
						'intent_secret'       => $response['payment_intent_secret'],
270
						'redirect_url'        => $verification_url,
271
					) );
272
273
					exit;
274
				}
275
276
				// Hijack all other redirects in order to do the redirection in JavaScript.
277
				add_action( 'wp_redirect', array( $this, 'redirect_after_early_renewal' ), 100 );
278
279
				return;
280
			}
281
282
			// Check for an existing intent, which is associated with the order.
283
			if ( $this->has_authentication_already_failed( $renewal_order ) ) {
284
				return;
285
			}
286
287
			// Get source from order
288
			$prepared_source = $this->prepare_order_source( $renewal_order );
289
			$source_object   = $prepared_source->source_object;
290
291 View Code Duplication
			if ( ! $prepared_source->customer ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
292
				throw new WC_Stripe_Exception(
293
					'Failed to process renewal for order ' . $renewal_order->get_id() . '. Stripe customer id is missing in the order',
294
					__( 'Customer not found', 'woocommerce-gateway-stripe' )
295
				);
296
			}
297
298
			WC_Stripe_Logger::log( "Info: Begin processing subscription payment for order {$order_id} for the amount of {$amount}" );
299
300
			/* If we're doing a retry and source is chargeable, we need to pass
301
			 * a different idempotency key and retry for success.
302
			 */
303 View Code Duplication
			if ( is_object( $source_object ) && empty( $source_object->error ) && $this->need_update_idempotency_key( $source_object, $previous_error ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
304
				add_filter( 'wc_stripe_idempotency_key', array( $this, 'change_idempotency_key' ), 10, 2 );
305
			}
306
307 View Code Duplication
			if ( ( $this->is_no_such_source_error( $previous_error ) || $this->is_no_linked_source_error( $previous_error ) ) && apply_filters( 'wc_stripe_use_default_customer_source', true ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
308
				// Passing empty source will charge customer default.
309
				$prepared_source->source = '';
310
			}
311
312
			$this->lock_order_payment( $renewal_order );
313
314
			$response                   = $this->create_and_confirm_intent_for_off_session( $renewal_order, $prepared_source, $amount );
315
			$is_authentication_required = $this->is_authentication_required_for_payment( $response );
316
317
			// It's only a failed payment if it's an error and it's not of the type 'authentication_required'.
318
			// If it's 'authentication_required', then we should email the user and ask them to authenticate.
319 View Code Duplication
			if ( ! empty( $response->error ) && ! $is_authentication_required ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
320
				// We want to retry.
321
				if ( $this->is_retryable_error( $response->error ) ) {
322
					if ( $retry ) {
323
						// Don't do anymore retries after this.
324
						if ( 5 <= $this->retry_interval ) {
325
							return $this->process_subscription_payment( $amount, $renewal_order, false, $response->error );
326
						}
327
328
						sleep( $this->retry_interval );
329
330
						$this->retry_interval++;
331
332
						return $this->process_subscription_payment( $amount, $renewal_order, true, $response->error );
333
					} else {
334
						$localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
335
						$renewal_order->add_order_note( $localized_message );
336
						throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
337
					}
338
				}
339
340
				$localized_messages = WC_Stripe_Helper::get_localized_messages();
341
342
				if ( 'card_error' === $response->error->type ) {
343
					$localized_message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
344
				} else {
345
					$localized_message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
346
				}
347
348
				$renewal_order->add_order_note( $localized_message );
349
350
				throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
351
			}
352
353
			// Either the charge was successfully captured, or it requires further authentication.
354
355
			if ( $is_authentication_required ) {
356
				do_action( 'wc_gateway_stripe_process_payment_authentication_required', $renewal_order, $response );
357
358
				$error_message = __( 'This transaction requires authentication.', 'woocommerce-gateway-stripe' );
359
				$renewal_order->add_order_note( $error_message );
360
361
				$charge = end( $response->error->payment_intent->charges->data );
362
				$id = $charge->id;
363
				$order_id = $renewal_order->get_id();
0 ignored issues
show
Unused Code introduced by
$order_id is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
364
365
				$renewal_order->set_transaction_id( $id );
366
				$renewal_order->update_status( 'failed', sprintf( __( 'Stripe charge awaiting authentication by user: %s.', 'woocommerce-gateway-stripe' ), $id ) );
367
				if ( is_callable( array( $renewal_order, 'save' ) ) ) {
368
					$renewal_order->save();
369
				}
370
			} else {
371
				// The charge was successfully captured
372
				do_action( 'wc_gateway_stripe_process_payment', $response, $renewal_order );
373
374
				$this->process_response( end( $response->charges->data ), $renewal_order );
375
			}
376
377
			$this->unlock_order_payment( $renewal_order );
378
		} catch ( WC_Stripe_Exception $e ) {
379
			WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
380
381
			do_action( 'wc_gateway_stripe_process_payment_error', $e, $renewal_order );
382
383
			/* translators: error message */
384
			$renewal_order->update_status( 'failed' );
385
		}
386
	}
387
388
	/**
389
	 * Updates other subscription sources.
390
	 *
391
	 * @since 3.1.0
392
	 * @version 4.0.0
393
	 */
394 View Code Duplication
	public function save_source_to_order( $order, $source ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
395
		parent::save_source_to_order( $order, $source );
396
397
		$order_id = $order->get_id();
398
399
		// Also store it on the subscriptions being purchased or paid for in the order
400
		if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id ) ) {
401
			$subscriptions = wcs_get_subscriptions_for_order( $order_id );
402
		} elseif ( function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order_id ) ) {
403
			$subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id );
404
		} else {
405
			$subscriptions = array();
406
		}
407
408
		foreach ( $subscriptions as $subscription ) {
409
			$subscription_id = $subscription->get_id();
410
			update_post_meta( $subscription_id, '_stripe_customer_id', $source->customer );
411
			update_post_meta( $subscription_id, '_stripe_source_id', $source->source );
412
		}
413
	}
414
415
	/**
416
	 * Don't transfer Stripe customer/token meta to resubscribe orders.
417
	 * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
418
	 */
419 View Code Duplication
	public function delete_resubscribe_meta( $resubscribe_order ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
420
		delete_post_meta( $resubscribe_order->get_id(), '_stripe_customer_id' );
0 ignored issues
show
Bug introduced by
The method get_id cannot be called on $resubscribe_order (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
421
		delete_post_meta( $resubscribe_order->get_id(), '_stripe_source_id' );
0 ignored issues
show
Bug introduced by
The method get_id cannot be called on $resubscribe_order (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
422
		// For BW compat will remove in future
423
		delete_post_meta( $resubscribe_order->get_id(), '_stripe_card_id' );
0 ignored issues
show
Bug introduced by
The method get_id cannot be called on $resubscribe_order (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
424
		// delete payment intent ID
425
		delete_post_meta( $resubscribe_order->get_id(), '_stripe_intent_id' );
0 ignored issues
show
Bug introduced by
The method get_id cannot be called on $resubscribe_order (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
426
		$this->delete_renewal_meta( $resubscribe_order );
427
	}
428
429
	/**
430
	 * Don't transfer Stripe fee/ID meta to renewal orders.
431
	 * @param int $resubscribe_order The order created for the customer to resubscribe to the old expired/cancelled subscription
0 ignored issues
show
Bug introduced by
There is no parameter named $resubscribe_order. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
432
	 */
433
	public function delete_renewal_meta( $renewal_order ) {
434
		WC_Stripe_Helper::delete_stripe_fee( $renewal_order );
435
		WC_Stripe_Helper::delete_stripe_net( $renewal_order );
436
437
		// delete payment intent ID
438
		delete_post_meta( $renewal_order->get_id(), '_stripe_intent_id' );
439
440
		return $renewal_order;
441
	}
442
443
	/**
444
	 * Update the customer_id for a subscription after using Stripe to complete a payment to make up for
445
	 * an automatic renewal payment which previously failed.
446
	 *
447
	 * @access public
448
	 * @param WC_Subscription $subscription The subscription for which the failing payment method relates.
449
	 * @param WC_Order $renewal_order The order which recorded the successful payment (to make up for the failed automatic payment).
450
	 * @return void
451
	 */
452
	public function update_failing_payment_method( $subscription, $renewal_order ) {
453
		update_post_meta( $subscription->get_id(), '_stripe_customer_id', $renewal_order->get_meta( '_stripe_customer_id', true ) );
454
		update_post_meta( $subscription->get_id(), '_stripe_source_id', $renewal_order->get_meta( '_stripe_source_id', true ) );
455
	}
456
457
	/**
458
	 * Include the payment meta data required to process automatic recurring payments so that store managers can
459
	 * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
460
	 *
461
	 * @since 2.5
462
	 * @param array $payment_meta associative array of meta data required for automatic payments
463
	 * @param WC_Subscription $subscription An instance of a subscription object
464
	 * @return array
465
	 */
466
	public function add_subscription_payment_meta( $payment_meta, $subscription ) {
467
		$subscription_id = $subscription->get_id();
468
		$source_id       = get_post_meta( $subscription_id, '_stripe_source_id', true );
469
470
		// For BW compat will remove in future.
471
		if ( empty( $source_id ) ) {
472
			$source_id = get_post_meta( $subscription_id, '_stripe_card_id', true );
473
474
			// Take this opportunity to update the key name.
475
			update_post_meta( $subscription_id, '_stripe_source_id', $source_id );
476
			delete_post_meta( $subscription_id, '_stripe_card_id', $source_id );
477
		}
478
479
		$payment_meta[ $this->id ] = array(
480
			'post_meta' => array(
481
				'_stripe_customer_id' => array(
482
					'value' => get_post_meta( $subscription_id, '_stripe_customer_id', true ),
483
					'label' => 'Stripe Customer ID',
484
				),
485
				'_stripe_source_id'   => array(
486
					'value' => $source_id,
487
					'label' => 'Stripe Source ID',
488
				),
489
			),
490
		);
491
492
		return $payment_meta;
493
	}
494
495
	/**
496
	 * Validate the payment meta data required to process automatic recurring payments so that store managers can
497
	 * manually set up automatic recurring payments for a customer via the Edit Subscriptions screen in 2.0+.
498
	 *
499
	 * @since 2.5
500
	 * @since 4.0.4 Stripe sourd id field no longer needs to be required.
501
	 * @param string $payment_method_id The ID of the payment method to validate
502
	 * @param array $payment_meta associative array of meta data required for automatic payments
503
	 * @return array
504
	 */
505 View Code Duplication
	public function validate_subscription_payment_meta( $payment_method_id, $payment_meta ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
506
		if ( $this->id === $payment_method_id ) {
507
508
			if ( ! isset( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) || empty( $payment_meta['post_meta']['_stripe_customer_id']['value'] ) ) {
509
510
				// Allow empty stripe customer id during subscription renewal. It will be added when processing payment if required.
511
				if ( ! isset( $_POST['wc_order_action'] ) || 'wcs_process_renewal' !== $_POST['wc_order_action'] ) {
512
					throw new Exception( __( 'A "Stripe Customer ID" value is required.', 'woocommerce-gateway-stripe' ) );
513
				}
514
			} elseif ( 0 !== strpos( $payment_meta['post_meta']['_stripe_customer_id']['value'], 'cus_' ) ) {
515
				throw new Exception( __( 'Invalid customer ID. A valid "Stripe Customer ID" must begin with "cus_".', 'woocommerce-gateway-stripe' ) );
516
			}
517
518
			if (
519
				! empty( $payment_meta['post_meta']['_stripe_source_id']['value'] ) && (
520
					0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'card_' )
521
					&& 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'src_' )
522
					&& 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'pm_' )
523
				)
524
			) {
525
				throw new Exception( __( 'Invalid source ID. A valid source "Stripe Source ID" must begin with "src_", "pm_", or "card_".', 'woocommerce-gateway-stripe' ) );
526
			}
527
		}
528
	}
529
530
	/**
531
	 * Render the payment method used for a subscription in the "My Subscriptions" table
532
	 *
533
	 * @since 1.7.5
534
	 * @param string $payment_method_to_display the default payment method text to display
535
	 * @param WC_Subscription $subscription the subscription details
536
	 * @return string the subscription payment method
537
	 */
538
	public function maybe_render_subscription_payment_method( $payment_method_to_display, $subscription ) {
539
		$customer_user = $subscription->get_customer_id();
540
541
		// bail for other payment methods
542
		if ( $subscription->get_payment_method() !== $this->id || ! $customer_user ) {
543
			return $payment_method_to_display;
544
		}
545
546
		$stripe_source_id = get_post_meta( $subscription->get_id(), '_stripe_source_id', true );
547
548
		// For BW compat will remove in future.
549
		if ( empty( $stripe_source_id ) ) {
550
			$stripe_source_id = get_post_meta( $subscription->get_id(), '_stripe_card_id', true );
551
552
			// Take this opportunity to update the key name.
553
			update_post_meta( $subscription->get_id(), '_stripe_source_id', $stripe_source_id );
554
		}
555
556
		$stripe_customer    = new WC_Stripe_Customer();
557
		$stripe_customer_id = get_post_meta( $subscription->get_id(), '_stripe_customer_id', true );
558
559
		// If we couldn't find a Stripe customer linked to the subscription, fallback to the user meta data.
560 View Code Duplication
		if ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
561
			$user_id            = $customer_user;
562
			$stripe_customer_id = get_user_option( '_stripe_customer_id', $user_id );
563
			$stripe_source_id   = get_user_option( '_stripe_source_id', $user_id );
564
565
			// For BW compat will remove in future.
566
			if ( empty( $stripe_source_id ) ) {
567
				$stripe_source_id = get_user_option( '_stripe_card_id', $user_id );
568
569
				// Take this opportunity to update the key name.
570
				update_user_option( $user_id, '_stripe_source_id', $stripe_source_id, false );
571
			}
572
		}
573
574
		// If we couldn't find a Stripe customer linked to the account, fallback to the order meta data.
575 View Code Duplication
		if ( ( ! $stripe_customer_id || ! is_string( $stripe_customer_id ) ) && false !== $subscription->order ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
576
			$stripe_customer_id = get_post_meta( $subscription->get_parent_id(), '_stripe_customer_id', true );
577
			$stripe_source_id   = get_post_meta( $subscription->get_parent_id(), '_stripe_source_id', true );
578
579
			// For BW compat will remove in future.
580
			if ( empty( $stripe_source_id ) ) {
581
				$stripe_source_id = get_post_meta( $subscription->get_parent_id(), '_stripe_card_id', true );
582
583
				// Take this opportunity to update the key name.
584
				update_post_meta( $subscription->get_parent_id(), '_stripe_source_id', $stripe_source_id );
585
			}
586
		}
587
588
		$stripe_customer->set_id( $stripe_customer_id );
589
590
		$sources                   = $stripe_customer->get_sources();
591
		$payment_method_to_display = __( 'N/A', 'woocommerce-gateway-stripe' );
592
593
		if ( $sources ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sources of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
594
			$card = false;
595
596
			foreach ( $sources as $source ) {
597
				if ( isset( $source->type ) && 'card' === $source->type ) {
598
					$card = $source->card;
599
				} elseif ( isset( $source->object ) && 'card' === $source->object ) {
600
					$card = $source;
601
				}
602
603
				if ( $source->id === $stripe_source_id ) {
604
					if ( $card ) {
605
						/* translators: 1) card brand 2) last 4 digits */
606
						$payment_method_to_display = sprintf( __( 'Via %1$s card ending in %2$s', 'woocommerce-gateway-stripe' ), ( isset( $card->brand ) ? $card->brand : __( 'N/A', 'woocommerce-gateway-stripe' ) ), $card->last4 );
607
					}
608
609
					break;
610
				}
611
			}
612
		}
613
614
		return $payment_method_to_display;
615
	}
616
617
	/**
618
	 * If this is the "Pass the SCA challenge" flow, remove a variable that is checked by WC Subscriptions
619
	 * so WC Subscriptions doesn't redirect to the checkout
620
	 */
621
	public function remove_order_pay_var() {
622
		global $wp;
623
		if ( isset( $_GET['wc-stripe-confirmation'] ) ) {
624
			$this->order_pay_var = $wp->query_vars['order-pay'];
625
			$wp->query_vars['order-pay'] = null;
626
		}
627
	}
628
629
	/**
630
	 * Restore the variable that was removed in remove_order_pay_var()
631
	 */
632
	public function restore_order_pay_var() {
633
		global $wp;
634
		if ( isset( $this->order_pay_var ) ) {
635
			$wp->query_vars['order-pay'] = $this->order_pay_var;
636
		}
637
	}
638
639
	/**
640
	 * Checks if a renewal already failed because a manual authentication is required.
641
	 *
642
	 * @param WC_Order $renewal_order The renewal order.
643
	 * @return boolean
644
	 */
645
	public function has_authentication_already_failed( $renewal_order ) {
646
		$existing_intent = $this->get_intent_from_order( $renewal_order );
647
648
		if (
649
			! $existing_intent
650
			|| 'requires_payment_method' !== $existing_intent->status
651
			|| empty( $existing_intent->last_payment_error )
652
			|| 'authentication_required' !== $existing_intent->last_payment_error->code
653
		) {
654
			return false;
655
		}
656
657
		// Make sure all emails are instantiated.
658
		WC_Emails::instance();
659
660
		/**
661
		 * A payment attempt failed because SCA authentication is required.
662
		 *
663
		 * @param WC_Order $renewal_order The order that is being renewed.
664
		 */
665
		do_action( 'wc_gateway_stripe_process_payment_authentication_required', $renewal_order );
666
667
		// Fail the payment attempt (order would be currently pending because of retry rules).
668
		$charge    = end( $existing_intent->charges->data );
669
		$charge_id = $charge->id;
670
		$renewal_order->update_status( 'failed', sprintf( __( 'Stripe charge awaiting authentication by user: %s.', 'woocommerce-gateway-stripe' ), $charge_id ) );
671
672
		return true;
673
	}
674
675
	/**
676
	 * Hijacks `wp_redirect` in order to generate a JS-friendly object with the URL.
677
	 *
678
	 * @param string $url The URL that Subscriptions attempts a redirect to.
679
	 * @return void
680
	 */
681
	public function redirect_after_early_renewal( $url ) {
682
		echo wp_json_encode(
683
			array(
684
				'stripe_sca_required' => false,
685
				'redirect_url'        => $url,
686
			)
687
		);
688
689
		exit;
690
	}
691
692
	/**
693
	 * Once an intent has been verified, perform some final actions for early renewals.
694
	 *
695
	 * @param WC_Order $order The renewal order.
696
	 * @param stdClass $intent The Payment Intent object.
697
	 */
698 View Code Duplication
	protected function handle_intent_verification_success( $order, $intent ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
699
		parent::handle_intent_verification_success( $order, $intent );
700
701
		if ( isset( $_GET['early_renewal'] ) ) { // wpcs: csrf ok.
702
			wcs_update_dates_after_early_renewal( wcs_get_subscription( $order->get_meta( '_subscription_renewal' ) ), $order );
703
			wc_add_notice( __( 'Your early renewal order was successful.', 'woocommerce-gateway-stripe' ), 'success' );
704
		}
705
	}
706
707
	/**
708
	 * During early renewals, instead of failing the renewal order, delete it and let Subs redirect to the checkout.
709
	 *
710
	 * @param WC_Order $order The renewal order.
711
	 * @param stdClass $intent The Payment Intent object (unused).
712
	 */
713 View Code Duplication
	protected function handle_intent_verification_failure( $order, $intent ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
714
		if ( isset( $_GET['early_renewal'] ) ) {
715
			$order->delete( true );
716
			wc_add_notice( __( 'Payment authorization for the renewal order was unsuccessful, please try again.', 'woocommerce-gateway-stripe' ), 'error' );
717
			$renewal_url = wcs_get_early_renewal_url( wcs_get_subscription( $order->get_meta( '_subscription_renewal' ) ) );
718
			wp_redirect( $renewal_url ); exit;
719
		}
720
	}
721
}
722