Completed
Push — master ( d8dd8c...25eaad )
by Radoslav
02:49
created

WC_Stripe_Payment_Gateway::add_payment_method()   C

Complexity

Conditions 12
Paths 64

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
nc 64
nop 0
dl 0
loc 44
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
// phpcs:disable WordPress.Files.FileName
7
8
/**
9
 * Abstract class that will be inherited by all payment methods.
10
 *
11
 * @extends WC_Payment_Gateway_CC
12
 *
13
 * @since 4.0.0
14
 */
15
abstract class WC_Stripe_Payment_Gateway extends WC_Payment_Gateway_CC {
16
	/**
17
	 * Displays the admin settings webhook description.
18
	 *
19
	 * @since 4.1.0
20
	 * @return mixed
21
	 */
22
	public function display_admin_settings_webhook_description() {
23
		/* translators: 1) webhook url */
24
		return sprintf( __( 'You must add the following webhook endpoint <strong style="background-color:#ddd;">&nbsp;%s&nbsp;</strong> to your <a href="https://dashboard.stripe.com/account/webhooks" target="_blank">Stripe account settings</a>. This will enable you to receive notifications on the charge statuses.', 'woocommerce-gateway-stripe' ), WC_Stripe_Helper::get_webhook_url() );
25
	}
26
27
	/**
28
	 * Displays the save to account checkbox.
29
	 *
30
	 * @since 4.1.0
31
	 */
32
	public function save_payment_method_checkbox() {
33
		printf(
34
			'<p class="form-row woocommerce-SavedPaymentMethods-saveNew">
35
				<input id="wc-%1$s-new-payment-method" name="wc-%1$s-new-payment-method" type="checkbox" value="true" style="width:auto;" />
36
				<label for="wc-%1$s-new-payment-method" style="display:inline;">%2$s</label>
37
			</p>',
38
			esc_attr( $this->id ),
39
			esc_html( apply_filters( 'wc_stripe_save_to_account_text', __( 'Save payment information to my account for future purchases.', 'woocommerce-gateway-stripe' ) ) )
40
		);
41
	}
42
43
	/**
44
	 * Checks to see if request is invalid and that
45
	 * they are worth retrying.
46
	 *
47
	 * @since 4.0.5
48
	 * @param array $error
49
	 */
50
	public function is_retryable_error( $error ) {
51
		return (
52
			'invalid_request_error' === $error->type ||
53
			'idempotency_error' === $error->type ||
54
			'rate_limit_error' === $error->type ||
55
			'api_connection_error' === $error->type ||
56
			'api_error' === $error->type
57
		);
58
	}
59
60
	/**
61
	 * Checks to see if error is of same idempotency key
62
	 * error due to retries with different parameters.
63
	 *
64
	 * @since 4.1.0
65
	 * @param array $error
66
	 */
67
	public function is_same_idempotency_error( $error ) {
68
		return (
69
			$error &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $error 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...
70
			'idempotency_error' === $error->type &&
71
			preg_match( '/Keys for idempotent requests can only be used with the same parameters they were first used with./i', $error->message )
72
		);
73
	}
74
75
	/**
76
	 * Checks to see if error is of invalid request
77
	 * error and source is already consumed.
78
	 *
79
	 * @since 4.1.0
80
	 * @param array $error
81
	 */
82
	public function is_source_already_consumed_error( $error ) {
83
		return (
84
			$error &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $error 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...
85
			'invalid_request_error' === $error->type &&
86
			preg_match( '/The reusable source you provided is consumed because it was previously charged without being attached to a customer or was detached from a customer. To charge a reusable source multiple time you must attach it to a customer first./i', $error->message )
87
		);
88
	}
89
90
	/**
91
	 * Checks to see if error is of invalid request
92
	 * error and it is no such customer.
93
	 *
94
	 * @since 4.1.0
95
	 * @param array $error
96
	 */
97
	public function is_no_such_customer_error( $error ) {
98
		return (
99
			$error &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $error 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...
100
			'invalid_request_error' === $error->type &&
101
			preg_match( '/No such customer/i', $error->message )
102
		);
103
	}
104
105
	/**
106
	 * Checks to see if error is of invalid request
107
	 * error and it is no such token.
108
	 *
109
	 * @since 4.1.0
110
	 * @param array $error
111
	 */
112
	public function is_no_such_token_error( $error ) {
113
		return (
114
			$error &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $error 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...
115
			'invalid_request_error' === $error->type &&
116
			preg_match( '/No such token/i', $error->message )
117
		);
118
	}
119
120
	/**
121
	 * Checks to see if error is of invalid request
122
	 * error and it is no such source.
123
	 *
124
	 * @since 4.1.0
125
	 * @param array $error
126
	 */
127
	public function is_no_such_source_error( $error ) {
128
		return (
129
			$error &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $error 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...
130
			'invalid_request_error' === $error->type &&
131
			preg_match( '/No such source/i', $error->message )
132
		);
133
	}
134
135
	/**
136
	 * Checks to see if error is of invalid request
137
	 * error and it is no such source linked to customer.
138
	 *
139
	 * @since 4.1.0
140
	 * @param array $error
141
	 */
142
	public function is_no_linked_source_error( $error ) {
143
		return (
144
			$error &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $error 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...
145
			'invalid_request_error' === $error->type &&
146
			preg_match( '/does not have a linked source with ID/i', $error->message )
147
		);
148
	}
149
150
	/**
151
	 * Check to see if we need to update the idempotency
152
	 * key to be different from previous charge request.
153
	 *
154
	 * @since 4.1.0
155
	 * @param object $source_object
156
	 * @param object $error
157
	 * @return bool
158
	 */
159
	public function need_update_idempotency_key( $source_object, $error ) {
160
		return (
161
			$error &&
162
			1 < $this->retry_interval &&
163
			! empty( $source_object ) &&
164
			'chargeable' === $source_object->status &&
165
			self::is_same_idempotency_error( $error )
166
		);
167
	}
168
169
	/**
170
	 * Check if we need to make gateways available.
171
	 *
172
	 * @since 4.1.3
173
	 */
174
	public function is_available() {
175
		if ( 'yes' === $this->enabled ) {
176
			if ( ! $this->secret_key || ! $this->publishable_key ) {
177
				return false;
178
			}
179
			return true;
180
		}
181
182
		return parent::is_available();
183
	}
184
185
	/**
186
	 * Checks if we need to process pre orders when
187
	 * pre orders is in the cart.
188
	 *
189
	 * @since 4.1.0
190
	 * @param int $order_id
191
	 * @return bool
192
	 */
193
	public function maybe_process_pre_orders( $order_id ) {
194
		return (
195
			WC_Stripe_Helper::is_pre_orders_exists() &&
196
			$this->pre_orders->is_pre_order( $order_id ) &&
197
			WC_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ) &&
198
			! is_wc_endpoint_url( 'order-pay' )
199
		);
200
	}
201
202
	/**
203
	 * All payment icons that work with Stripe. Some icons references
204
	 * WC core icons.
205
	 *
206
	 * @since 4.0.0
207
	 * @since 4.1.0 Changed to using img with svg (colored) instead of fonts.
208
	 * @return array
209
	 */
210
	public function payment_icons() {
211
		return apply_filters(
212
			'wc_stripe_payment_icons',
213
			array(
214
				'visa'       => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/visa.svg" class="stripe-visa-icon stripe-icon" alt="Visa" />',
215
				'amex'       => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/amex.svg" class="stripe-amex-icon stripe-icon" alt="American Express" />',
216
				'mastercard' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/mastercard.svg" class="stripe-mastercard-icon stripe-icon" alt="Mastercard" />',
217
				'discover'   => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/discover.svg" class="stripe-discover-icon stripe-icon" alt="Discover" />',
218
				'diners'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/diners.svg" class="stripe-diners-icon stripe-icon" alt="Diners" />',
219
				'jcb'        => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/jcb.svg" class="stripe-jcb-icon stripe-icon" alt="JCB" />',
220
				'alipay'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/alipay.svg" class="stripe-alipay-icon stripe-icon" alt="Alipay" />',
221
				'wechat'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/wechat.svg" class="stripe-wechat-icon stripe-icon" alt="Wechat Pay" />',
222
				'bancontact' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/bancontact.svg" class="stripe-bancontact-icon stripe-icon" alt="Bancontact" />',
223
				'ideal'      => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/ideal.svg" class="stripe-ideal-icon stripe-icon" alt="iDeal" />',
224
				'p24'        => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/p24.svg" class="stripe-p24-icon stripe-icon" alt="P24" />',
225
				'giropay'    => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/giropay.svg" class="stripe-giropay-icon stripe-icon" alt="Giropay" />',
226
				'eps'        => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/eps.svg" class="stripe-eps-icon stripe-icon" alt="EPS" />',
227
				'multibanco' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/multibanco.svg" class="stripe-multibanco-icon stripe-icon" alt="Multibanco" />',
228
				'sofort'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/sofort.svg" class="stripe-sofort-icon stripe-icon" alt="SOFORT" />',
229
				'sepa'       => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/sepa.svg" class="stripe-sepa-icon stripe-icon" alt="SEPA" />',
230
			)
231
		);
232
	}
233
234
	/**
235
	 * Validates that the order meets the minimum order amount
236
	 * set by Stripe.
237
	 *
238
	 * @since 4.0.0
239
	 * @version 4.0.0
240
	 * @param object $order
241
	 */
242
	public function validate_minimum_order_amount( $order ) {
243
		if ( $order->get_total() * 100 < WC_Stripe_Helper::get_minimum_amount() ) {
244
			/* translators: 1) dollar amount */
245
			throw new WC_Stripe_Exception( 'Did not meet minimum amount', 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 ) ) );
246
		}
247
	}
248
249
	/**
250
	 * Gets the transaction URL linked to Stripe dashboard.
251
	 *
252
	 * @since 4.0.0
253
	 * @version 4.0.0
254
	 */
255
	public function get_transaction_url( $order ) {
256
		if ( $this->testmode ) {
257
			$this->view_transaction_url = 'https://dashboard.stripe.com/test/payments/%s';
258
		} else {
259
			$this->view_transaction_url = 'https://dashboard.stripe.com/payments/%s';
260
		}
261
262
		return parent::get_transaction_url( $order );
263
	}
264
265
	/**
266
	 * Gets the saved customer id if exists.
267
	 *
268
	 * @since 4.0.0
269
	 * @version 4.0.0
270
	 */
271
	public function get_stripe_customer_id( $order ) {
272
		$customer = get_user_meta( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->customer_user : $order->get_customer_id(), '_stripe_customer_id', true );
273
274
		if ( empty( $customer ) ) {
275
			// Try to get it via the order.
276
			if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
277
				return get_post_meta( $order->id, '_stripe_customer_id', true );
278
			} else {
279
				return $order->get_meta( '_stripe_customer_id', true );
280
			}
281
		} else {
282
			return $customer;
283
		}
284
285
		return false;
0 ignored issues
show
Unused Code introduced by
return false; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
286
	}
287
288
	/**
289
	 * Builds the return URL from redirects.
290
	 *
291
	 * @since 4.0.0
292
	 * @version 4.0.0
293
	 * @param object $order
294
	 * @param int $id Stripe session id.
295
	 */
296
	public function get_stripe_return_url( $order = null, $id = null ) {
297
		if ( is_object( $order ) ) {
298
			if ( empty( $id ) ) {
299
				$id = uniqid();
0 ignored issues
show
Unused Code introduced by
$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...
300
			}
301
302
			$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
303
304
			$args = array(
305
				'utm_nooverride' => '1',
306
				'order_id'       => $order_id,
307
			);
308
309
			return esc_url_raw( add_query_arg( $args, $this->get_return_url( $order ) ) );
310
		}
311
312
		return esc_url_raw( add_query_arg( array( 'utm_nooverride' => '1' ), $this->get_return_url() ) );
313
	}
314
315
	/**
316
	 * Is $order_id a subscription?
317
	 * @param  int  $order_id
318
	 * @return boolean
319
	 */
320
	public function has_subscription( $order_id ) {
321
		return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) );
322
	}
323
324
	/**
325
	 * Generate the request for the payment.
326
	 *
327
	 * @since 3.1.0
328
	 * @version 4.0.0
329
	 * @param  WC_Order $order
330
	 * @param  object $prepared_source
331
	 * @return array()
0 ignored issues
show
Documentation introduced by
The doc-type array() could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
332
	 */
333
	public function generate_payment_request( $order, $prepared_source ) {
334
		$settings              = get_option( 'woocommerce_stripe_settings', array() );
335
		$statement_descriptor  = ! empty( $settings['statement_descriptor'] ) ? str_replace( "'", '', $settings['statement_descriptor'] ) : '';
336
		$capture               = ! empty( $settings['capture'] ) && 'yes' === $settings['capture'] ? true : false;
337
		$post_data             = array();
338
		$post_data['currency'] = strtolower( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->get_order_currency() : $order->get_currency() );
339
		$post_data['amount']   = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $post_data['currency'] );
340
		/* translators: 1) blog name 2) order number */
341
		$post_data['description'] = sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() );
342
		$billing_email            = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_email : $order->get_billing_email();
343
		$billing_first_name       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_first_name : $order->get_billing_first_name();
344
		$billing_last_name        = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_last_name : $order->get_billing_last_name();
345
346
		if ( ! empty( $billing_email ) && apply_filters( 'wc_stripe_send_stripe_receipt', false ) ) {
347
			$post_data['receipt_email'] = $billing_email;
348
		}
349
350
		switch ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->payment_method : $order->get_payment_method() ) {
351
			case 'stripe':
352
				if ( ! empty( $statement_descriptor ) ) {
353
					$post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
354
				}
355
356
				$post_data['capture'] = $capture ? 'true' : 'false';
357
				break;
358
			case 'stripe_sepa':
359
				if ( ! empty( $statement_descriptor ) ) {
360
					$post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
361
				}
362
				break;
363
		}
364
365
		$post_data['expand[]'] = 'balance_transaction';
366
367
		$metadata = array(
368
			__( 'customer_name', 'woocommerce-gateway-stripe' ) => sanitize_text_field( $billing_first_name ) . ' ' . sanitize_text_field( $billing_last_name ),
369
			__( 'customer_email', 'woocommerce-gateway-stripe' ) => sanitize_email( $billing_email ),
370
			'order_id' => $order->get_order_number(),
371
		);
372
373
		if ( $this->has_subscription( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id() ) ) {
374
			$metadata += array(
375
				'payment_type' => 'recurring',
376
				'site_url'     => esc_url( get_site_url() ),
377
			);
378
		}
379
380
		$post_data['metadata'] = apply_filters( 'wc_stripe_payment_metadata', $metadata, $order, $prepared_source );
381
382
		if ( $prepared_source->customer ) {
383
			$post_data['customer'] = $prepared_source->customer;
384
		}
385
386
		if ( $prepared_source->source ) {
387
			$post_data['source'] = $prepared_source->source;
388
		}
389
390
		/**
391
		 * Filter the return value of the WC_Payment_Gateway_CC::generate_payment_request.
392
		 *
393
		 * @since 3.1.0
394
		 * @param array $post_data
395
		 * @param WC_Order $order
396
		 * @param object $source
397
		 */
398
		return apply_filters( 'wc_stripe_generate_payment_request', $post_data, $order, $prepared_source );
399
	}
400
401
	/**
402
	 * Store extra meta data for an order from a Stripe Response.
403
	 */
404
	public function process_response( $response, $order ) {
405
		WC_Stripe_Logger::log( 'Processing response: ' . print_r( $response, true ) );
406
407
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
408
		$captured = ( isset( $response->captured ) && $response->captured ) ? 'yes' : 'no';
409
410
		// Store charge data.
411
		WC_Stripe_Helper::is_wc_lt( '3.0' ) ? update_post_meta( $order_id, '_stripe_charge_captured', $captured ) : $order->update_meta_data( '_stripe_charge_captured', $captured );
412
413
		if ( isset( $response->balance_transaction ) ) {
414
			$this->update_fees( $order, is_string( $response->balance_transaction ) ? $response->balance_transaction : $response->balance_transaction->id );
415
		}
416
417
		if ( 'yes' === $captured ) {
418
			/**
419
			 * Charge can be captured but in a pending state. Payment methods
420
			 * that are asynchronous may take couple days to clear. Webhook will
421
			 * take care of the status changes.
422
			 */
423
			if ( 'pending' === $response->status ) {
424
				$order_stock_reduced = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? get_post_meta( $order_id, '_order_stock_reduced', true ) : $order->get_meta( '_order_stock_reduced', true );
425
426
				if ( ! $order_stock_reduced ) {
427
					WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->reduce_order_stock() : wc_reduce_stock_levels( $order_id );
428
				}
429
430
				WC_Stripe_Helper::is_wc_lt( '3.0' ) ? update_post_meta( $order_id, '_transaction_id', $response->id ) : $order->set_transaction_id( $response->id );
431
				/* translators: transaction id */
432
				$order->update_status( 'on-hold', sprintf( __( 'Stripe charge awaiting payment: %s.', 'woocommerce-gateway-stripe' ), $response->id ) );
433
			}
434
435
			if ( 'succeeded' === $response->status ) {
436
				$order->payment_complete( $response->id );
437
438
				/* translators: transaction id */
439
				$message = sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $response->id );
440
				$order->add_order_note( $message );
441
			}
442
443
			if ( 'failed' === $response->status ) {
444
				$localized_message = __( 'Payment processing failed. Please retry.', 'woocommerce-gateway-stripe' );
445
				$order->add_order_note( $localized_message );
446
				throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
447
			}
448
		} else {
449
			WC_Stripe_Helper::is_wc_lt( '3.0' ) ? update_post_meta( $order_id, '_transaction_id', $response->id ) : $order->set_transaction_id( $response->id );
450
451
			if ( $order->has_status( array( 'pending', 'failed' ) ) ) {
452
				WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->reduce_order_stock() : wc_reduce_stock_levels( $order_id );
453
			}
454
455
			/* translators: transaction id */
456
			$order->update_status( 'on-hold', sprintf( __( 'Stripe charge authorized (Charge ID: %s). Process order to take payment, or cancel to remove the pre-authorization.', 'woocommerce-gateway-stripe' ), $response->id ) );
457
		}
458
459
		if ( is_callable( array( $order, 'save' ) ) ) {
460
			$order->save();
461
		}
462
463
		do_action( 'wc_gateway_stripe_process_response', $response, $order );
464
465
		return $response;
466
	}
467
468
	/**
469
	 * Sends the failed order email to admin.
470
	 *
471
	 * @since 3.1.0
472
	 * @version 4.0.0
473
	 * @param int $order_id
474
	 * @return null
475
	 */
476
	public function send_failed_order_email( $order_id ) {
477
		$emails = WC()->mailer()->get_emails();
478
		if ( ! empty( $emails ) && ! empty( $order_id ) ) {
479
			$emails['WC_Email_Failed_Order']->trigger( $order_id );
480
		}
481
	}
482
483
	/**
484
	 * Get owner details.
485
	 *
486
	 * @since 4.0.0
487
	 * @version 4.0.0
488
	 * @param object $order
489
	 * @return object $details
490
	 */
491
	public function get_owner_details( $order ) {
492
		$billing_first_name = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_first_name : $order->get_billing_first_name();
493
		$billing_last_name  = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_last_name : $order->get_billing_last_name();
494
495
		$details = array();
496
497
		$name  = $billing_first_name . ' ' . $billing_last_name;
498
		$email = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_email : $order->get_billing_email();
499
		$phone = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_phone : $order->get_billing_phone();
500
501
		if ( ! empty( $phone ) ) {
502
			$details['phone'] = $phone;
503
		}
504
505
		if ( ! empty( $name ) ) {
506
			$details['name'] = $name;
507
		}
508
509
		if ( ! empty( $email ) ) {
510
			$details['email'] = $email;
511
		}
512
513
		$details['address']['line1']       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_address_1 : $order->get_billing_address_1();
514
		$details['address']['line2']       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_address_2 : $order->get_billing_address_2();
515
		$details['address']['state']       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_state : $order->get_billing_state();
516
		$details['address']['city']        = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_city : $order->get_billing_city();
517
		$details['address']['postal_code'] = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_postcode : $order->get_billing_postcode();
518
		$details['address']['country']     = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_country : $order->get_billing_country();
519
520
		return (object) apply_filters( 'wc_stripe_owner_details', $details, $order );
521
	}
522
523
	/**
524
	 * Get source object by source id.
525
	 *
526
	 * @since 4.0.3
527
	 * @param string $source_id The source ID to get source object for.
528
	 */
529
	public function get_source_object( $source_id = '' ) {
530
		if ( empty( $source_id ) ) {
531
			return '';
532
		}
533
534
		$source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
535
536
		if ( ! empty( $source_object->error ) ) {
537
			throw new WC_Stripe_Exception( print_r( $source_object, true ), $source_object->error->message );
538
		}
539
540
		return $source_object;
541
	}
542
543
	/**
544
	 * Checks if card is a prepaid card.
545
	 *
546
	 * @since 4.0.6
547
	 * @param object $source_object
548
	 * @return bool
549
	 */
550
	public function is_prepaid_card( $source_object ) {
551
		return ( $source_object && 'token' === $source_object->object && 'prepaid' === $source_object->card->funding );
552
	}
553
554
	/**
555
	 * Checks if source is of legacy type card.
556
	 *
557
	 * @since 4.0.8
558
	 * @param string $source_id
559
	 * @return bool
560
	 */
561
	public function is_type_legacy_card( $source_id ) {
562
		return ( preg_match( '/^card_/', $source_id ) );
563
	}
564
565
	/**
566
	 * Checks if payment is via saved payment source.
567
	 *
568
	 * @since 4.1.0
569
	 * @return bool
570
	 */
571
	public function is_using_saved_payment_method() {
572
		$payment_method = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
573
574
		return ( isset( $_POST[ 'wc-' . $payment_method . '-payment-token' ] ) && 'new' !== $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
575
	}
576
577
	/**
578
	 * Get payment source. This can be a new token/source or existing WC token.
579
	 * If user is logged in and/or has WC account, create an account on Stripe.
580
	 * This way we can attribute the payment to the user to better fight fraud.
581
	 *
582
	 * @since 3.1.0
583
	 * @version 4.0.0
584
	 * @param string $user_id
585
	 * @param bool $force_save_source Should we force save payment source.
586
	 *
587
	 * @throws Exception When card was not added or for and invalid card.
588
	 * @return object
589
	 */
590
	public function prepare_source( $user_id, $force_save_source = false ) {
591
		$customer          = new WC_Stripe_Customer( $user_id );
592
		$set_customer      = true;
593
		$force_save_source = apply_filters( 'wc_stripe_force_save_source', $force_save_source, $customer );
594
		$source_object     = '';
595
		$source_id         = '';
596
		$wc_token_id       = false;
597
		$payment_method    = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
598
		$is_token          = false;
599
600
		// New CC info was entered and we have a new source to process.
601
		if ( ! empty( $_POST['stripe_source'] ) ) {
602
			$source_object = self::get_source_object( wc_clean( $_POST['stripe_source'] ) );
603
			$source_id     = $source_object->id;
604
605
			// This checks to see if customer opted to save the payment method to file.
606
			$maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
607
608
			/**
609
			 * This is true if the user wants to store the card to their account.
610
			 * Criteria to save to file is they are logged in, they opted to save or product requirements and the source is
611
			 * actually reusable. Either that or force_save_source is true.
612
			 */
613
			if ( ( $user_id && $this->saved_cards && $maybe_saved_card && 'reusable' === $source_object->usage ) || $force_save_source ) {
614
				$response = $customer->add_source( $source_object->id );
615
616
				if ( ! empty( $response->error ) ) {
617
					throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
618
				}
619
			}
620
		} elseif ( $this->is_using_saved_payment_method() ) {
621
			// Use an existing token, and then process the payment.
622
			$wc_token_id = wc_clean( $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
623
			$wc_token    = WC_Payment_Tokens::get( $wc_token_id );
624
625
			if ( ! $wc_token || $wc_token->get_user_id() !== get_current_user_id() ) {
626
				WC()->session->set( 'refresh_totals', true );
627
				throw new WC_Stripe_Exception( 'Invalid payment method', __( 'Invalid payment method. Please input a new card number.', 'woocommerce-gateway-stripe' ) );
628
			}
629
630
			$source_id = $wc_token->get_token();
631
632
			if ( $this->is_type_legacy_card( $source_id ) ) {
633
				$is_token = true;
634
			}
635
		} elseif ( isset( $_POST['stripe_token'] ) && 'new' !== $_POST['stripe_token'] ) {
636
			$stripe_token     = wc_clean( $_POST['stripe_token'] );
637
			$maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
638
639
			// This is true if the user wants to store the card to their account.
640
			if ( ( $user_id && $this->saved_cards && $maybe_saved_card ) || $force_save_source ) {
641
				$response = $customer->add_source( $stripe_token );
642
643
				if ( ! empty( $response->error ) ) {
644
					throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
645
				}
646
			} else {
647
				$set_customer = false;
648
				$source_id    = $stripe_token;
649
				$is_token     = true;
650
			}
651
		}
652
653
		if ( ! $set_customer ) {
654
			$customer_id = false;
655
		} else {
656
			$customer_id = $customer->get_id() ? $customer->get_id() : false;
657
		}
658
659
		if ( empty( $source_object ) && ! $is_token ) {
660
			$source_object = self::get_source_object( $source_id );
661
		}
662
663
		return (object) array(
664
			'token_id'      => $wc_token_id,
665
			'customer'      => $customer_id,
666
			'source'        => $source_id,
667
			'source_object' => $source_object,
668
		);
669
	}
670
671
	/**
672
	 * Get payment source from an order. This could be used in the future for
673
	 * a subscription as an example, therefore using the current user ID would
674
	 * not work - the customer won't be logged in :)
675
	 *
676
	 * Not using 2.6 tokens for this part since we need a customer AND a card
677
	 * token, and not just one.
678
	 *
679
	 * @since 3.1.0
680
	 * @version 4.0.0
681
	 * @param object $order
682
	 * @return object
683
	 */
684
	public function prepare_order_source( $order = null ) {
685
		$stripe_customer = new WC_Stripe_Customer();
686
		$stripe_source   = false;
687
		$token_id        = false;
688
		$source_object   = false;
689
690
		if ( $order ) {
691
			$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
692
693
			$stripe_customer_id = get_post_meta( $order_id, '_stripe_customer_id', true );
694
695
			if ( $stripe_customer_id ) {
696
				$stripe_customer->set_id( $stripe_customer_id );
697
			}
698
699
			$source_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? get_post_meta( $order_id, '_stripe_source_id', true ) : $order->get_meta( '_stripe_source_id', true );
700
701
			// Since 4.0.0, we changed card to source so we need to account for that.
702
			if ( empty( $source_id ) ) {
703
				$source_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? get_post_meta( $order_id, '_stripe_card_id', true ) : $order->get_meta( '_stripe_card_id', true );
704
705
				// Take this opportunity to update the key name.
706
				WC_Stripe_Helper::is_wc_lt( '3.0' ) ? update_post_meta( $order_id, '_stripe_source_id', $source_id ) : $order->update_meta_data( '_stripe_source_id', $source_id );
707
708
				if ( is_callable( array( $order, 'save' ) ) ) {
709
					$order->save();
710
				}
711
			}
712
713
			if ( $source_id ) {
714
				$stripe_source = $source_id;
715
				$source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
716
			} elseif ( apply_filters( 'wc_stripe_use_default_customer_source', true ) ) {
717
				/*
718
				 * We can attempt to charge the customer's default source
719
				 * by sending empty source id.
720
				 */
721
				$stripe_source = '';
722
			}
723
		}
724
725
		return (object) array(
726
			'token_id'      => $token_id,
727
			'customer'      => $stripe_customer ? $stripe_customer->get_id() : false,
728
			'source'        => $stripe_source,
729
			'source_object' => $source_object,
730
		);
731
	}
732
733
	/**
734
	 * Save source to order.
735
	 *
736
	 * @since 3.1.0
737
	 * @version 4.0.0
738
	 * @param WC_Order $order For to which the source applies.
739
	 * @param stdClass $source Source information.
740
	 */
741
	public function save_source_to_order( $order, $source ) {
742
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
743
744
		// Store source in the order.
745
		if ( $source->customer ) {
746
			if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
747
				update_post_meta( $order_id, '_stripe_customer_id', $source->customer );
748
			} else {
749
				$order->update_meta_data( '_stripe_customer_id', $source->customer );
750
			}
751
		}
752
753
		if ( $source->source ) {
754
			if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
755
				update_post_meta( $order_id, '_stripe_source_id', $source->source );
756
			} else {
757
				$order->update_meta_data( '_stripe_source_id', $source->source );
758
			}
759
		}
760
761
		if ( is_callable( array( $order, 'save' ) ) ) {
762
			$order->save();
763
		}
764
	}
765
766
	/**
767
	 * Updates Stripe fees/net.
768
	 * e.g usage would be after a refund.
769
	 *
770
	 * @since 4.0.0
771
	 * @version 4.0.6
772
	 * @param object $order The order object
773
	 * @param int $balance_transaction_id
774
	 */
775
	public function update_fees( $order, $balance_transaction_id ) {
776
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
777
778
		$balance_transaction = WC_Stripe_API::retrieve( 'balance/history/' . $balance_transaction_id );
779
780
		if ( empty( $balance_transaction->error ) ) {
781
			if ( isset( $balance_transaction ) && isset( $balance_transaction->fee ) ) {
782
				// Fees and Net needs to both come from Stripe to be accurate as the returned
783
				// values are in the local currency of the Stripe account, not from WC.
784
				$fee_refund = ! empty( $balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'fee' ) : 0;
785
				$net_refund = ! empty( $balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'net' ) : 0;
786
787
				// Current data fee & net.
788
				$fee_current = WC_Stripe_Helper::get_stripe_fee( $order );
789
				$net_current = WC_Stripe_Helper::get_stripe_net( $order );
790
791
				// Calculation.
792
				$fee = (float) $fee_current + (float) $fee_refund;
793
				$net = (float) $net_current + (float) $net_refund;
794
795
				WC_Stripe_Helper::update_stripe_fee( $order, $fee );
796
				WC_Stripe_Helper::update_stripe_net( $order, $net );
797
798
				$currency = ! empty( $balance_transaction->currency ) ? strtoupper( $balance_transaction->currency ) : null;
799
				WC_Stripe_Helper::update_stripe_currency( $order, $currency );
800
801
				if ( is_callable( array( $order, 'save' ) ) ) {
802
					$order->save();
803
				}
804
			}
805
		} else {
806
			WC_Stripe_Logger::log( "Unable to update fees/net meta for order: {$order_id}" );
807
		}
808
	}
809
810
	/**
811
	 * Refund a charge.
812
	 *
813
	 * @since 3.1.0
814
	 * @version 4.0.0
815
	 * @param  int $order_id
816
	 * @param  float $amount
817
	 * @return bool
818
	 */
819
	public function process_refund( $order_id, $amount = null, $reason = '' ) {
820
		$order = wc_get_order( $order_id );
821
822
		if ( ! $order ) {
823
			return false;
824
		}
825
826
		$request = array();
827
828
		if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
829
			$order_currency = get_post_meta( $order_id, '_order_currency', true );
830
			$captured       = get_post_meta( $order_id, '_stripe_charge_captured', true );
831
			$charge_id      = get_post_meta( $order_id, '_transaction_id', true );
832
		} else {
833
			$order_currency = $order->get_currency();
834
			$captured       = $order->get_meta( '_stripe_charge_captured', true );
835
			$charge_id      = $order->get_transaction_id();
836
		}
837
838
		if ( ! $charge_id ) {
839
			return false;
840
		}
841
842
		if ( ! is_null( $amount ) ) {
843
			$request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $order_currency );
844
		}
845
846
		// If order is only authorized, don't pass amount.
847
		if ( 'yes' !== $captured ) {
848
			unset( $request['amount'] );
849
		}
850
851
		if ( $reason ) {
852
			$request['metadata'] = array(
853
				'reason' => $reason,
854
			);
855
		}
856
857
		$request['charge'] = $charge_id;
858
		WC_Stripe_Logger::log( "Info: Beginning refund for order {$charge_id} for the amount of {$amount}" );
859
860
		$request = apply_filters( 'wc_stripe_refund_request', $request, $order );
861
862
		$intent = $this->get_intent_from_order( $order );
863
		$intent_cancelled = false;
864
		if ( $intent ) {
865
			// If the order has a Payment Intent pending capture, then the Intent itself must be refunded (cancelled), not the Charge
866
			if ( ! empty( $intent->error ) ) {
867
				$response = $intent;
868
				$intent_cancelled = true;
869
			} elseif ( 'requires_capture' === $intent->status ) {
870
				$result = WC_Stripe_API::request(
871
					array(),
872
					'payment_intents/' . $intent->id . '/cancel'
873
				);
874
				$intent_cancelled = true;
875
876
				if ( ! empty( $result->error ) ) {
877
					$response = $result;
878
				} else {
879
					$charge = end( $result->charges->data );
880
					$response = end( $charge->refunds->data );
881
				}
882
			}
883
		}
884
885
		if ( ! $intent_cancelled ) {
886
			$response = WC_Stripe_API::request( $request, 'refunds' );
887
		}
888
889
		if ( ! empty( $response->error ) ) {
890
			WC_Stripe_Logger::log( 'Error: ' . $response->error->message );
0 ignored issues
show
Bug introduced by
The variable $response does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
891
892
			return $response;
893
894
		} elseif ( ! empty( $response->id ) ) {
895
			WC_Stripe_Helper::is_wc_lt( '3.0' ) ? update_post_meta( $order_id, '_stripe_refund_id', $response->id ) : $order->update_meta_data( '_stripe_refund_id', $response->id );
896
897
			$amount = wc_price( $response->amount / 100 );
898
899
			if ( in_array( strtolower( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->get_order_currency() : $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
900
				$amount = wc_price( $response->amount );
901
			}
902
903
			if ( isset( $response->balance_transaction ) ) {
904
				$this->update_fees( $order, $response->balance_transaction );
905
			}
906
907
			/* translators: 1) dollar amount 2) transaction id 3) refund message */
908
			$refund_message = ( isset( $captured ) && 'yes' === $captured ) ? sprintf( __( 'Refunded %1$s - Refund ID: %2$s - Reason: %3$s', 'woocommerce-gateway-stripe' ), $amount, $response->id, $reason ) : __( 'Pre-Authorization Released', 'woocommerce-gateway-stripe' );
909
910
			$order->add_order_note( $refund_message );
911
			WC_Stripe_Logger::log( 'Success: ' . html_entity_decode( wp_strip_all_tags( $refund_message ) ) );
912
913
			return true;
914
		}
915
	}
916
917
	/**
918
	 * Add payment method via account screen.
919
	 * We don't store the token locally, but to the Stripe API.
920
	 *
921
	 * @since 3.0.0
922
	 * @version 4.0.0
923
	 */
924
	public function add_payment_method() {
925
		$error     = false;
926
		$error_msg = __( 'There was a problem adding the payment method.', 'woocommerce-gateway-stripe' );
927
		$source_id = '';
928
929
		if ( empty( $_POST['stripe_source'] ) && empty( $_POST['stripe_token'] ) || ! is_user_logged_in() ) {
930
			$error = true;
931
		}
932
933
		$stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
934
935
		$source = ! empty( $_POST['stripe_source'] ) ? wc_clean( $_POST['stripe_source'] ) : '';
936
937
		$source_object = WC_Stripe_API::retrieve( 'sources/' . $source );
938
939
		if ( isset( $source_object ) ) {
940
			if ( ! empty( $source_object->error ) ) {
941
				$error = true;
942
			}
943
944
			$source_id = $source_object->id;
945
		} elseif ( isset( $_POST['stripe_token'] ) ) {
946
			$source_id = wc_clean( $_POST['stripe_token'] );
947
		}
948
949
		$response = $stripe_customer->add_source( $source_id );
950
951
		if ( ! $response || is_wp_error( $response ) || ! empty( $response->error ) ) {
952
			$error = true;
953
		}
954
955
		if ( $error ) {
956
			wc_add_notice( $error_msg, 'error' );
957
			WC_Stripe_Logger::log( 'Add payment method Error: ' . $error_msg );
958
			return;
959
		}
960
961
		do_action( 'wc_stripe_add_payment_method_' . $_POST['payment_method'] . '_success', $source_id, $source_object );
962
963
		return array(
964
			'result'   => 'success',
965
			'redirect' => wc_get_endpoint_url( 'payment-methods' ),
966
		);
967
	}
968
969
	/**
970
	 * Gets the locale with normalization that only Stripe accepts.
971
	 *
972
	 * @since 4.0.6
973
	 * @return string $locale
974
	 */
975
	public function get_locale() {
976
		$locale = get_locale();
977
978
		/*
979
		 * Stripe expects Norwegian to only be passed NO.
980
		 * But WP has different dialects.
981
		 */
982
		if ( 'NO' === substr( $locale, 3, 2 ) ) {
983
			$locale = 'no';
984
		} else {
985
			$locale = substr( get_locale(), 0, 2 );
986
		}
987
988
		return $locale;
989
	}
990
991
	/**
992
	 * Change the idempotency key so charge can
993
	 * process order as a different transaction.
994
	 *
995
	 * @since 4.0.6
996
	 * @param string $idempotency_key
997
	 * @param array $request
998
	 */
999
	public function change_idempotency_key( $idempotency_key, $request ) {
1000
		$customer = ! empty( $request['customer'] ) ? $request['customer'] : '';
1001
		$source   = ! empty( $request['source'] ) ? $request['source'] : $customer;
1002
		$count    = $this->retry_interval;
1003
1004
		return $request['metadata']['order_id'] . '-' . $count . '-' . $source;
1005
	}
1006
1007
	/**
1008
	 * Checks if request is the original to prevent double processing
1009
	 * on WC side. The original-request header and request-id header
1010
	 * needs to be the same to mean its the original request.
1011
	 *
1012
	 * @since 4.0.6
1013
	 * @param array $headers
1014
	 */
1015
	public function is_original_request( $headers ) {
1016
		if ( $headers['original-request'] === $headers['request-id'] ) {
1017
			return true;
1018
		}
1019
1020
		return false;
1021
	}
1022
1023
	/**
1024
	 * Create a new PaymentIntent.
1025
	 *
1026
	 * @param WC_Order $order           The order that is being paid for.
1027
	 * @param object   $prepared_source The source that is used for the payment.
1028
	 * @return object                   An intent or an error.
1029
	 */
1030
	public function create_intent( $order, $prepared_source ) {
1031
		// The request for a charge contains metadata for the intent.
1032
		$full_request = $this->generate_payment_request( $order, $prepared_source );
1033
1034
		$request = array(
1035
			'source'               => $prepared_source->source,
1036
			'amount'               => WC_Stripe_Helper::get_stripe_amount( $order->get_total() ),
1037
			'currency'             => strtolower( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->get_order_currency() : $order->get_currency() ),
1038
			'description'          => $full_request['description'],
1039
			'metadata'             => $full_request['metadata'],
1040
			'statement_descriptor' => $full_request['statement_descriptor'],
1041
			'capture_method'       => ( 'true' === $full_request['capture'] ) ? 'automatic' : 'manual',
1042
			'payment_method_types' => array(
1043
				'card',
1044
			),
1045
		);
1046
1047
		if ( $prepared_source->customer ) {
1048
			$request['customer'] = $prepared_source->customer;
1049
		}
1050
1051
		// Create an intent that awaits an action.
1052
		$intent = WC_Stripe_API::request( $request, 'payment_intents' );
1053
		if ( ! empty( $intent->error ) ) {
1054
			return $intent;
1055
		}
1056
1057
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1058
		WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id initiated for order $order_id" );
1059
1060
		// Save the intent ID to the order.
1061
		$this->save_intent_to_order( $order, $intent );
1062
1063
		return $intent;
1064
	}
1065
1066
	/**
1067
	 * Updates an existing intent with updated amount, source, and customer.
1068
	 *
1069
	 * @param object   $intent          The existing intent object.
1070
	 * @param WC_Order $order           The order.
1071
	 * @param object   $prepared_source Currently selected source.
1072
	 * @return object                   An updated intent.
1073
	 */
1074
	public function update_existing_intent( $intent, $order, $prepared_source ) {
1075
		$request = array();
1076
1077
		if ( $prepared_source->source !== $intent->source ) {
1078
			$request['source'] = $prepared_source->source;
1079
		}
1080
1081
		$new_amount = WC_Stripe_Helper::get_stripe_amount( $order->get_total() );
1082
		if ( $intent->amount !== $new_amount ) {
1083
			$request['amount'] = $new_amount;
1084
		}
1085
1086
		if ( $prepared_source->customer && $intent->customer !== $prepared_source->customer ) {
1087
			$request['customer'] = $prepared_source->customer;
1088
		}
1089
1090
		if ( empty( $request ) ) {
1091
			return $intent;
1092
		}
1093
1094
		return WC_Stripe_API::request( $request, "payment_intents/$intent->id" );
1095
	}
1096
1097
	/**
1098
	 * Confirms an intent if it is the `requires_confirmation` state.
1099
	 *
1100
	 * @since 4.2.1
1101
	 * @param object   $intent          The intent to confirm.
1102
	 * @param WC_Order $order           The order that the intent is associated with.
1103
	 * @param object   $prepared_source The source that is being charged.
1104
	 * @return object                   Either an error or the updated intent.
1105
	 */
1106
	public function confirm_intent( $intent, $order, $prepared_source ) {
1107
		if ( 'requires_confirmation' !== $intent->status ) {
1108
			return $intent;
1109
		}
1110
1111
		// Try to confirm the intent & capture the charge (if 3DS is not required).
1112
		$confirm_request = array(
1113
			'source' => $prepared_source->source,
1114
		);
1115
1116
		$confirmed_intent = WC_Stripe_API::request( $confirm_request, "payment_intents/$intent->id/confirm" );
1117
1118
		if ( ! empty( $confirmed_intent->error ) ) {
1119
			return $confirmed_intent;
1120
		}
1121
1122
		// Save a note about the status of the intent.
1123
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1124
		if ( 'succeeded' === $confirmed_intent->status ) {
1125
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
1126
		} elseif ( 'requires_action' === $confirmed_intent->status ) {
1127
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id requires authentication for order $order_id" );
1128
		}
1129
1130
		return $confirmed_intent;
1131
	}
1132
1133
	/**
1134
	 * Saves intent to order.
1135
	 *
1136
	 * @since 3.2.0
1137
	 * @param WC_Order $order For to which the source applies.
1138
	 * @param stdClass $intent Payment intent information.
1139
	 */
1140
	public function save_intent_to_order( $order, $intent ) {
1141
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1142
1143
		if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
1144
			update_post_meta( $order_id, '_stripe_intent_id', $intent->id );
1145
		} else {
1146
			$order->update_meta_data( '_stripe_intent_id', $intent->id );
1147
		}
1148
1149
		if ( is_callable( array( $order, 'save' ) ) ) {
1150
			$order->save();
1151
		}
1152
	}
1153
1154
	/**
1155
	 * Retrieves the payment intent, associated with an order.
1156
	 *
1157
	 * @since 4.2
1158
	 * @param WC_Order $order The order to retrieve an intent for.
1159
	 * @return obect|bool     Either the intent object or `false`.
1160
	 */
1161
	public function get_intent_from_order( $order ) {
1162
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1163
1164
		if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
1165
			$intent_id = get_post_meta( $order_id, '_stripe_intent_id', true );
1166
		} else {
1167
			$intent_id = $order->get_meta( '_stripe_intent_id' );
1168
		}
1169
1170
		if ( ! $intent_id ) {
1171
			return false;
1172
		}
1173
1174
		return WC_Stripe_API::request( array(), "payment_intents/$intent_id", 'GET' );
1175
	}
1176
1177
	/**
1178
	 * Locks an order for payment intent processing for 5 minutes.
1179
	 *
1180
	 * @since 4.2
1181
	 * @param WC_Order $order  The order that is being paid.
1182
	 * @param stdClass $intent The intent that is being processed.
1183
	 * @return bool            A flag that indicates whether the order is already locked.
1184
	 */
1185
	public function lock_order_payment( $order, $intent ) {
1186
		$order_id       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1187
		$transient_name = 'wc_stripe_processing_intent_' . $order_id;
1188
		$processing     = get_transient( $transient_name );
1189
1190
		// Block the process if the same intent is already being handled.
1191
		if ( $processing === $intent->id ) {
1192
			return true;
1193
		}
1194
1195
		// Save the new intent as a transient, eventually overwriting another one.
1196
		set_transient( $transient_name, $intent->id, 5 * MINUTE_IN_SECONDS );
1197
1198
		return false;
1199
	}
1200
1201
	/**
1202
	 * Unlocks an order for processing by payment intents.
1203
	 *
1204
	 * @since 4.2
1205
	 * @param WC_Order $order The order that is being unlocked.
1206
	 */
1207
	public function unlock_order_payment( $order ) {
1208
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1209
		delete_transient( 'wc_stripe_processing_intent_' . $order_id );
1210
	}
1211
}
1212