WC_Stripe_Payment_Gateway   F
last analyzed

Complexity

Total Complexity 240

Size/Duplication

Total Lines 1463
Duplicated Lines 1.03 %

Coupling/Cohesion

Components 3
Dependencies 5

Importance

Changes 0
Metric Value
dl 15
loc 1463
rs 0.8
c 0
b 0
f 0
wmc 240
lcom 3
cbo 5

50 Methods

Rating   Name   Duplication   Size   Complexity  
A display_admin_settings_webhook_description() 0 4 1
A save_payment_method_checkbox() 0 10 1
A is_retryable_error() 0 9 5
A is_same_idempotency_error() 0 7 3
A send_failed_order_email() 0 6 3
A get_source_object() 0 13 3
A is_no_such_customer_error() 0 7 3
A is_no_such_token_error() 0 7 3
A is_no_such_source_error() 0 7 3
A is_no_linked_source_error() 0 7 3
A need_update_idempotency_key() 0 9 5
C add_payment_method() 0 44 12
A get_locale() 0 15 2
A change_idempotency_key() 0 7 3
A is_original_request() 0 7 2
A are_keys_set() 9 12 4
A is_available() 0 7 2
A maybe_process_pre_orders() 0 8 4
A payment_icons() 0 23 1
A validate_minimum_order_amount() 0 6 2
A get_transaction_url() 0 9 2
A get_stripe_customer_id() 0 12 2
A get_stripe_return_url() 0 18 3
A has_subscription() 0 3 4
C process_response() 0 63 12
A get_owner_details() 0 31 4
A is_prepaid_card() 0 7 4
A is_type_legacy_card() 0 3 1
A is_using_saved_payment_method() 0 5 3
F prepare_source() 6 85 26
B prepare_order_source() 0 48 8
A save_source_to_order() 0 14 4
B update_fees() 0 32 8
F process_refund() 0 91 17
A generate_create_intent_request() 0 38 5
A get_level3_data_from_order() 0 46 4
A create_intent() 0 17 2
F generate_payment_request() 0 81 16
A confirm_intent() 0 32 5
A save_intent_to_order() 0 7 2
A get_intent_from_order() 0 16 3
A get_intent() 0 16 4
A lock_order_payment() 0 15 5
A unlock_order_payment() 0 4 1
A is_authentication_required_for_payment() 0 4 4
A setup_intent() 0 17 3
C create_and_confirm_intent_for_off_session() 0 73 11
A ensure_subscription_has_customer_id() 0 10 3
A is_valid_us_zip_code() 0 3 2
B update_existing_intent() 0 34 7

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_Payment_Gateway 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_Payment_Gateway, and based on these observations, apply Extract Interface, too.

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 it is no such customer.
78
	 *
79
	 * @since 4.1.0
80
	 * @param array $error
81
	 */
82
	public function is_no_such_customer_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( '/No such customer/i', $error->message )
87
		);
88
	}
89
90
	/**
91
	 * Checks to see if error is of invalid request
92
	 * error and it is no such token.
93
	 *
94
	 * @since 4.1.0
95
	 * @param array $error
96
	 */
97
	public function is_no_such_token_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 token/i', $error->message )
102
		);
103
	}
104
105
	/**
106
	 * Checks to see if error is of invalid request
107
	 * error and it is no such source.
108
	 *
109
	 * @since 4.1.0
110
	 * @param array $error
111
	 */
112
	public function is_no_such_source_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 source/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 linked to customer.
123
	 *
124
	 * @since 4.1.0
125
	 * @param array $error
126
	 */
127
	public function is_no_linked_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( '/does not have a linked source with ID/i', $error->message )
132
		);
133
	}
134
135
	/**
136
	 * Check to see if we need to update the idempotency
137
	 * key to be different from previous charge request.
138
	 *
139
	 * @since 4.1.0
140
	 * @param object $source_object
141
	 * @param object $error
142
	 * @return bool
143
	 */
144
	public function need_update_idempotency_key( $source_object, $error ) {
145
		return (
146
			$error &&
147
			1 < $this->retry_interval &&
148
			! empty( $source_object ) &&
149
			'chargeable' === $source_object->status &&
150
			self::is_same_idempotency_error( $error )
151
		);
152
	}
153
154
	/**
155
	 * Checks if keys are set and valid.
156
	 *
157
	 * @since 4.0.6
158
	 * @return bool True if the keys are set *and* valid, false otherwise (for example, if keys are empty or the secret key was pasted as publishable key).
159
	 */
160 View Code Duplication
	public function are_keys_set() {
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...
161
		// NOTE: updates to this function should be added to are_keys_set()
162
		// in includes/payment-methods/class-wc-stripe-payment-request.php
163
164
		if ( $this->testmode ) {
165
			return preg_match( '/^pk_test_/', $this->publishable_key )
166
				&& preg_match( '/^[rs]k_test_/', $this->secret_key );
167
		} else {
168
			return preg_match( '/^pk_live_/', $this->publishable_key )
169
			    && preg_match( '/^[rs]k_live_/', $this->secret_key );
170
		}
171
	}
172
173
	/**
174
	 * Check if we need to make gateways available.
175
	 *
176
	 * @since 4.1.3
177
	 */
178
	public function is_available() {
179
		if ( 'yes' === $this->enabled ) {
180
			return $this->are_keys_set();
181
		}
182
183
		return parent::is_available();
184
	}
185
186
	/**
187
	 * Checks if we need to process pre orders when
188
	 * pre orders is in the cart.
189
	 *
190
	 * @since 4.1.0
191
	 * @param int $order_id
192
	 * @return bool
193
	 */
194
	public function maybe_process_pre_orders( $order_id ) {
195
		return (
196
			WC_Stripe_Helper::is_pre_orders_exists() &&
197
			$this->pre_orders->is_pre_order( $order_id ) &&
198
			WC_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ) &&
199
			! is_wc_endpoint_url( 'order-pay' )
200
		);
201
	}
202
203
	/**
204
	 * All payment icons that work with Stripe. Some icons references
205
	 * WC core icons.
206
	 *
207
	 * @since 4.0.0
208
	 * @since 4.1.0 Changed to using img with svg (colored) instead of fonts.
209
	 * @return array
210
	 */
211
	public function payment_icons() {
212
		return apply_filters(
213
			'wc_stripe_payment_icons',
214
			array(
215
				'visa'       => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/visa.svg" class="stripe-visa-icon stripe-icon" alt="Visa" />',
216
				'amex'       => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/amex.svg" class="stripe-amex-icon stripe-icon" alt="American Express" />',
217
				'mastercard' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/mastercard.svg" class="stripe-mastercard-icon stripe-icon" alt="Mastercard" />',
218
				'discover'   => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/discover.svg" class="stripe-discover-icon stripe-icon" alt="Discover" />',
219
				'diners'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/diners.svg" class="stripe-diners-icon stripe-icon" alt="Diners" />',
220
				'jcb'        => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/jcb.svg" class="stripe-jcb-icon stripe-icon" alt="JCB" />',
221
				'alipay'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/alipay.svg" class="stripe-alipay-icon stripe-icon" alt="Alipay" />',
222
				'wechat'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/wechat.svg" class="stripe-wechat-icon stripe-icon" alt="Wechat Pay" />',
223
				'bancontact' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/bancontact.svg" class="stripe-bancontact-icon stripe-icon" alt="Bancontact" />',
224
				'ideal'      => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/ideal.svg" class="stripe-ideal-icon stripe-icon" alt="iDeal" />',
225
				'p24'        => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/p24.svg" class="stripe-p24-icon stripe-icon" alt="P24" />',
226
				'giropay'    => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/giropay.svg" class="stripe-giropay-icon stripe-icon" alt="Giropay" />',
227
				'eps'        => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/eps.svg" class="stripe-eps-icon stripe-icon" alt="EPS" />',
228
				'multibanco' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/multibanco.svg" class="stripe-multibanco-icon stripe-icon" alt="Multibanco" />',
229
				'sofort'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/sofort.svg" class="stripe-sofort-icon stripe-icon" alt="SOFORT" />',
230
				'sepa'       => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/sepa.svg" class="stripe-sepa-icon stripe-icon" alt="SEPA" />',
231
			)
232
		);
233
	}
234
235
	/**
236
	 * Validates that the order meets the minimum order amount
237
	 * set by Stripe.
238
	 *
239
	 * @since 4.0.0
240
	 * @version 4.0.0
241
	 * @param object $order
242
	 */
243
	public function validate_minimum_order_amount( $order ) {
244
		if ( $order->get_total() * 100 < WC_Stripe_Helper::get_minimum_amount() ) {
245
			/* translators: 1) dollar amount */
246
			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 ) ) );
247
		}
248
	}
249
250
	/**
251
	 * Gets the transaction URL linked to Stripe dashboard.
252
	 *
253
	 * @since 4.0.0
254
	 * @version 4.0.0
255
	 */
256
	public function get_transaction_url( $order ) {
257
		if ( $this->testmode ) {
258
			$this->view_transaction_url = 'https://dashboard.stripe.com/test/payments/%s';
259
		} else {
260
			$this->view_transaction_url = 'https://dashboard.stripe.com/payments/%s';
261
		}
262
263
		return parent::get_transaction_url( $order );
264
	}
265
266
	/**
267
	 * Gets the saved customer id if exists.
268
	 *
269
	 * @since 4.0.0
270
	 * @version 4.0.0
271
	 */
272
	public function get_stripe_customer_id( $order ) {
273
		$customer = get_user_option( '_stripe_customer_id', $order->get_customer_id() );
274
275
		if ( empty( $customer ) ) {
276
			// Try to get it via the order.
277
			return $order->get_meta( '_stripe_customer_id', true );
278
		} else {
279
			return $customer;
280
		}
281
282
		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...
283
	}
284
285
	/**
286
	 * Builds the return URL from redirects.
287
	 *
288
	 * @since 4.0.0
289
	 * @version 4.0.0
290
	 * @param object $order
291
	 * @param int $id Stripe session id.
292
	 */
293
	public function get_stripe_return_url( $order = null, $id = null ) {
294
		if ( is_object( $order ) ) {
295
			if ( empty( $id ) ) {
296
				$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...
297
			}
298
299
			$order_id = $order->get_id();
300
301
			$args = array(
302
				'utm_nooverride' => '1',
303
				'order_id'       => $order_id,
304
			);
305
306
			return wp_sanitize_redirect( esc_url_raw( add_query_arg( $args, $this->get_return_url( $order ) ) ) );
307
		}
308
309
		return wp_sanitize_redirect( esc_url_raw( add_query_arg( array( 'utm_nooverride' => '1' ), $this->get_return_url() ) ) );
310
	}
311
312
	/**
313
	 * Is $order_id a subscription?
314
	 * @param  int  $order_id
315
	 * @return boolean
316
	 */
317
	public function has_subscription( $order_id ) {
318
		return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) );
319
	}
320
321
	/**
322
	 * Generate the request for the payment.
323
	 *
324
	 * @since 3.1.0
325
	 * @version 4.5.4
326
	 * @param  WC_Order $order
327
	 * @param  object $prepared_source
328
	 * @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...
329
	 */
330
	public function generate_payment_request( $order, $prepared_source ) {
331
		$settings              = get_option( 'woocommerce_stripe_settings', array() );
332
		$statement_descriptor  = ! empty( $settings['statement_descriptor'] ) ? str_replace( "'", '', $settings['statement_descriptor'] ) : '';
333
		$capture               = ! empty( $settings['capture'] ) && 'yes' === $settings['capture'] ? true : false;
334
		$post_data             = array();
335
		$post_data['currency'] = strtolower( $order->get_currency() );
336
		$post_data['amount']   = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $post_data['currency'] );
337
		/* translators: 1) blog name 2) order number */
338
		$post_data['description'] = sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() );
339
		$billing_email            = $order->get_billing_email();
340
		$billing_first_name       = $order->get_billing_first_name();
341
		$billing_last_name        = $order->get_billing_last_name();
342
343
		if ( ! empty( $billing_email ) && apply_filters( 'wc_stripe_send_stripe_receipt', false ) ) {
344
			$post_data['receipt_email'] = $billing_email;
345
		}
346
347
		switch ( $order->get_payment_method() ) {
348
			case 'stripe':
349
				if ( ! empty( $statement_descriptor ) ) {
350
					$post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
351
				}
352
353
				$post_data['capture'] = $capture ? 'true' : 'false';
354
				break;
355
			case 'stripe_sepa':
356
				if ( ! empty( $statement_descriptor ) ) {
357
					$post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
358
				}
359
				break;
360
		}
361
362
		if ( method_exists( $order, 'get_shipping_postcode' ) && ! empty( $order->get_shipping_postcode() ) ) {
363
			$post_data['shipping'] = array(
364
				'name'    => trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() ),
365
				'address' => array(
366
					'line1'       => $order->get_shipping_address_1(),
367
					'line2'       => $order->get_shipping_address_2(),
368
					'city'        => $order->get_shipping_city(),
369
					'country'     => $order->get_shipping_country(),
370
					'postal_code' => $order->get_shipping_postcode(),
371
					'state'       => $order->get_shipping_state(),
372
				)
373
			);
374
		}
375
376
		$post_data['expand[]'] = 'balance_transaction';
377
378
		$metadata = array(
379
			__( 'customer_name', 'woocommerce-gateway-stripe' ) => sanitize_text_field( $billing_first_name ) . ' ' . sanitize_text_field( $billing_last_name ),
380
			__( 'customer_email', 'woocommerce-gateway-stripe' ) => sanitize_email( $billing_email ),
381
			'order_id' => $order->get_order_number(),
382
			'site_url' => esc_url( get_site_url() ),
383
		);
384
385
		if ( $this->has_subscription( $order->get_id() ) ) {
386
			$metadata += array(
387
				'payment_type' => 'recurring',
388
			);
389
		}
390
391
		$post_data['metadata'] = apply_filters( 'wc_stripe_payment_metadata', $metadata, $order, $prepared_source );
392
393
		if ( $prepared_source->customer ) {
394
			$post_data['customer'] = $prepared_source->customer;
395
		}
396
397
		if ( $prepared_source->source ) {
398
			$post_data['source'] = $prepared_source->source;
399
		}
400
401
		/**
402
		 * Filter the return value of the WC_Payment_Gateway_CC::generate_payment_request.
403
		 *
404
		 * @since 3.1.0
405
		 * @param array $post_data
406
		 * @param WC_Order $order
407
		 * @param object $source
408
		 */
409
		return apply_filters( 'wc_stripe_generate_payment_request', $post_data, $order, $prepared_source );
410
	}
411
412
	/**
413
	 * Store extra meta data for an order from a Stripe Response.
414
	 */
415
	public function process_response( $response, $order ) {
416
		WC_Stripe_Logger::log( 'Processing response: ' . print_r( $response, true ) );
417
418
		$order_id = $order->get_id();
419
		$captured = ( isset( $response->captured ) && $response->captured ) ? 'yes' : 'no';
420
421
		// Store charge data.
422
		$order->update_meta_data( '_stripe_charge_captured', $captured );
423
424
		if ( isset( $response->balance_transaction ) ) {
425
			$this->update_fees( $order, is_string( $response->balance_transaction ) ? $response->balance_transaction : $response->balance_transaction->id );
426
		}
427
428
		if ( 'yes' === $captured ) {
429
			/**
430
			 * Charge can be captured but in a pending state. Payment methods
431
			 * that are asynchronous may take couple days to clear. Webhook will
432
			 * take care of the status changes.
433
			 */
434
			if ( 'pending' === $response->status ) {
435
				$order_stock_reduced = $order->get_meta( '_order_stock_reduced', true );
436
437
				if ( ! $order_stock_reduced ) {
438
					wc_reduce_stock_levels( $order_id );
439
				}
440
441
				$order->set_transaction_id( $response->id );
442
				/* translators: transaction id */
443
				$order->update_status( 'on-hold', sprintf( __( 'Stripe charge awaiting payment: %s.', 'woocommerce-gateway-stripe' ), $response->id ) );
444
			}
445
446
			if ( 'succeeded' === $response->status ) {
447
				$order->payment_complete( $response->id );
448
449
				/* translators: transaction id */
450
				$message = sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $response->id );
451
				$order->add_order_note( $message );
452
			}
453
454
			if ( 'failed' === $response->status ) {
455
				$localized_message = __( 'Payment processing failed. Please retry.', 'woocommerce-gateway-stripe' );
456
				$order->add_order_note( $localized_message );
457
				throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
458
			}
459
		} else {
460
			$order->set_transaction_id( $response->id );
461
462
			if ( $order->has_status( array( 'pending', 'failed' ) ) ) {
463
				wc_reduce_stock_levels( $order_id );
464
			}
465
466
			/* translators: transaction id */
467
			$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 ) );
468
		}
469
470
		if ( is_callable( array( $order, 'save' ) ) ) {
471
			$order->save();
472
		}
473
474
		do_action( 'wc_gateway_stripe_process_response', $response, $order );
475
476
		return $response;
477
	}
478
479
	/**
480
	 * Sends the failed order email to admin.
481
	 *
482
	 * @since 3.1.0
483
	 * @version 4.0.0
484
	 * @param int $order_id
485
	 * @return null
486
	 */
487
	public function send_failed_order_email( $order_id ) {
488
		$emails = WC()->mailer()->get_emails();
489
		if ( ! empty( $emails ) && ! empty( $order_id ) ) {
490
			$emails['WC_Email_Failed_Order']->trigger( $order_id );
491
		}
492
	}
493
494
	/**
495
	 * Get owner details.
496
	 *
497
	 * @since 4.0.0
498
	 * @version 4.0.0
499
	 * @param object $order
500
	 * @return object $details
501
	 */
502
	public function get_owner_details( $order ) {
503
		$billing_first_name = $order->get_billing_first_name();
504
		$billing_last_name  = $order->get_billing_last_name();
505
506
		$details = array();
507
508
		$name  = $billing_first_name . ' ' . $billing_last_name;
509
		$email = $order->get_billing_email();
510
		$phone = $order->get_billing_phone();
511
512
		if ( ! empty( $phone ) ) {
513
			$details['phone'] = $phone;
514
		}
515
516
		if ( ! empty( $name ) ) {
517
			$details['name'] = $name;
518
		}
519
520
		if ( ! empty( $email ) ) {
521
			$details['email'] = $email;
522
		}
523
524
		$details['address']['line1']       = $order->get_billing_address_1();
525
		$details['address']['line2']       = $order->get_billing_address_2();
526
		$details['address']['state']       = $order->get_billing_state();
527
		$details['address']['city']        = $order->get_billing_city();
528
		$details['address']['postal_code'] = $order->get_billing_postcode();
529
		$details['address']['country']     = $order->get_billing_country();
530
531
		return (object) apply_filters( 'wc_stripe_owner_details', $details, $order );
532
	}
533
534
	/**
535
	 * Get source object by source id.
536
	 *
537
	 * @since 4.0.3
538
	 * @param string $source_id The source ID to get source object for.
539
	 */
540
	public function get_source_object( $source_id = '' ) {
541
		if ( empty( $source_id ) ) {
542
			return '';
543
		}
544
545
		$source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
546
547
		if ( ! empty( $source_object->error ) ) {
548
			throw new WC_Stripe_Exception( print_r( $source_object, true ), $source_object->error->message );
549
		}
550
551
		return $source_object;
552
	}
553
554
	/**
555
	 * Checks if card is a prepaid card.
556
	 *
557
	 * @since 4.0.6
558
	 * @param object $source_object
559
	 * @return bool
560
	 */
561
	public function is_prepaid_card( $source_object ) {
562
		return (
563
			$source_object
564
			&& ( 'token' === $source_object->object || 'source' === $source_object->object )
565
			&& 'prepaid' === $source_object->card->funding
566
		);
567
	}
568
569
	/**
570
	 * Checks if source is of legacy type card.
571
	 *
572
	 * @since 4.0.8
573
	 * @param string $source_id
574
	 * @return bool
575
	 */
576
	public function is_type_legacy_card( $source_id ) {
577
		return ( preg_match( '/^card_/', $source_id ) );
578
	}
579
580
	/**
581
	 * Checks if payment is via saved payment source.
582
	 *
583
	 * @since 4.1.0
584
	 * @return bool
585
	 */
586
	public function is_using_saved_payment_method() {
587
		$payment_method = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
588
589
		return ( isset( $_POST[ 'wc-' . $payment_method . '-payment-token' ] ) && 'new' !== $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
590
	}
591
592
	/**
593
	 * Get payment source. This can be a new token/source or existing WC token.
594
	 * If user is logged in and/or has WC account, create an account on Stripe.
595
	 * This way we can attribute the payment to the user to better fight fraud.
596
	 *
597
	 * @since 3.1.0
598
	 * @version 4.0.0
599
	 * @param string $user_id
600
	 * @param bool $force_save_source Should we force save payment source.
601
	 *
602
	 * @throws Exception When card was not added or for and invalid card.
603
	 * @return object
604
	 */
605
	public function prepare_source( $user_id, $force_save_source = false, $existing_customer_id = null ) {
606
		$customer = new WC_Stripe_Customer( $user_id );
607
		if ( ! empty( $existing_customer_id ) ) {
608
			$customer->set_id( $existing_customer_id );
609
		}
610
611
		$force_save_source = apply_filters( 'wc_stripe_force_save_source', $force_save_source, $customer );
612
		$source_object     = '';
613
		$source_id         = '';
614
		$wc_token_id       = false;
615
		$payment_method    = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
616
		$is_token          = false;
617
618
		// New CC info was entered and we have a new source to process.
619
		if ( ! empty( $_POST['stripe_source'] ) ) {
620
			$source_object = self::get_source_object( wc_clean( $_POST['stripe_source'] ) );
621
			$source_id     = $source_object->id;
622
623
			// This checks to see if customer opted to save the payment method to file.
624
			$maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
625
626
			/**
627
			 * This is true if the user wants to store the card to their account.
628
			 * Criteria to save to file is they are logged in, they opted to save or product requirements and the source is
629
			 * actually reusable. Either that or force_save_source is true.
630
			 */
631
			if ( ( $user_id && $this->saved_cards && $maybe_saved_card && 'reusable' === $source_object->usage ) || $force_save_source ) {
632
				$response = $customer->add_source( $source_object->id );
633
634 View Code Duplication
				if ( ! empty( $response->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...
635
					throw new WC_Stripe_Exception( print_r( $response, true ), $this->get_localized_error_message_from_response( $response ) );
636
				}
637
			}
638
		} elseif ( $this->is_using_saved_payment_method() ) {
639
			// Use an existing token, and then process the payment.
640
			$wc_token_id = wc_clean( $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
641
			$wc_token    = WC_Payment_Tokens::get( $wc_token_id );
642
643
			if ( ! $wc_token || $wc_token->get_user_id() !== get_current_user_id() ) {
644
				WC()->session->set( 'refresh_totals', true );
645
				throw new WC_Stripe_Exception( 'Invalid payment method', __( 'Invalid payment method. Please input a new card number.', 'woocommerce-gateway-stripe' ) );
646
			}
647
648
			$source_id = $wc_token->get_token();
649
650
			if ( $this->is_type_legacy_card( $source_id ) ) {
651
				$is_token = true;
652
			}
653
		} elseif ( isset( $_POST['stripe_token'] ) && 'new' !== $_POST['stripe_token'] ) {
654
			$stripe_token     = wc_clean( $_POST['stripe_token'] );
655
			$maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
656
657
			// This is true if the user wants to store the card to their account.
658
			if ( ( $user_id && $this->saved_cards && $maybe_saved_card ) || $force_save_source ) {
659
				$response = $customer->add_source( $stripe_token );
660
661 View Code Duplication
				if ( ! empty( $response->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...
662
					throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
663
				}
664
				$source_id    = $response;
665
			} else {
666
				$source_id    = $stripe_token;
667
				$is_token     = true;
668
			}
669
		}
670
671
		$customer_id = $customer->get_id();
672
		if ( ! $customer_id ) {
673
			$customer->set_id( $customer->create_customer() );
674
			$customer_id = $customer->get_id();
675
		} else {
676
			$customer_id = $customer->update_customer();
677
		}
678
679
		if ( empty( $source_object ) && ! $is_token ) {
680
			$source_object = self::get_source_object( $source_id );
681
		}
682
683
		return (object) array(
684
			'token_id'      => $wc_token_id,
685
			'customer'      => $customer_id,
686
			'source'        => $source_id,
687
			'source_object' => $source_object,
688
		);
689
	}
690
691
	/**
692
	 * Get payment source from an order. This could be used in the future for
693
	 * a subscription as an example, therefore using the current user ID would
694
	 * not work - the customer won't be logged in :)
695
	 *
696
	 * Not using 2.6 tokens for this part since we need a customer AND a card
697
	 * token, and not just one.
698
	 *
699
	 * @since 3.1.0
700
	 * @version 4.0.0
701
	 * @param object $order
702
	 * @return object
703
	 */
704
	public function prepare_order_source( $order = null ) {
705
		$stripe_customer = new WC_Stripe_Customer();
706
		$stripe_source   = false;
707
		$token_id        = false;
708
		$source_object   = false;
709
710
		if ( $order ) {
711
			$order_id = $order->get_id();
712
713
			$stripe_customer_id = get_post_meta( $order_id, '_stripe_customer_id', true );
714
715
			if ( $stripe_customer_id ) {
716
				$stripe_customer->set_id( $stripe_customer_id );
717
			}
718
719
			$source_id = $order->get_meta( '_stripe_source_id', true );
720
721
			// Since 4.0.0, we changed card to source so we need to account for that.
722
			if ( empty( $source_id ) ) {
723
				$source_id = $order->get_meta( '_stripe_card_id', true );
724
725
				// Take this opportunity to update the key name.
726
				$order->update_meta_data( '_stripe_source_id', $source_id );
727
728
				if ( is_callable( array( $order, 'save' ) ) ) {
729
					$order->save();
730
				}
731
			}
732
733
			if ( $source_id ) {
734
				$stripe_source = $source_id;
735
				$source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
736
			} elseif ( apply_filters( 'wc_stripe_use_default_customer_source', true ) ) {
737
				/*
738
				 * We can attempt to charge the customer's default source
739
				 * by sending empty source id.
740
				 */
741
				$stripe_source = '';
742
			}
743
		}
744
745
		return (object) array(
746
			'token_id'      => $token_id,
747
			'customer'      => $stripe_customer ? $stripe_customer->get_id() : false,
748
			'source'        => $stripe_source,
749
			'source_object' => $source_object,
750
		);
751
	}
752
753
	/**
754
	 * Save source to order.
755
	 *
756
	 * @since 3.1.0
757
	 * @version 4.0.0
758
	 * @param WC_Order $order For to which the source applies.
759
	 * @param stdClass $source Source information.
760
	 */
761
	public function save_source_to_order( $order, $source ) {
762
		// Store source in the order.
763
		if ( $source->customer ) {
764
			$order->update_meta_data( '_stripe_customer_id', $source->customer );
765
		}
766
767
		if ( $source->source ) {
768
			$order->update_meta_data( '_stripe_source_id', $source->source );
769
		}
770
771
		if ( is_callable( array( $order, 'save' ) ) ) {
772
			$order->save();
773
		}
774
	}
775
776
	/**
777
	 * Updates Stripe fees/net.
778
	 * e.g usage would be after a refund.
779
	 *
780
	 * @since 4.0.0
781
	 * @version 4.0.6
782
	 * @param object $order The order object
783
	 * @param int $balance_transaction_id
784
	 */
785
	public function update_fees( $order, $balance_transaction_id ) {
786
		$balance_transaction = WC_Stripe_API::retrieve( 'balance/history/' . $balance_transaction_id );
787
788
		if ( empty( $balance_transaction->error ) ) {
789
			if ( isset( $balance_transaction ) && isset( $balance_transaction->fee ) ) {
790
				// Fees and Net needs to both come from Stripe to be accurate as the returned
791
				// values are in the local currency of the Stripe account, not from WC.
792
				$fee_refund = ! empty( $balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'fee' ) : 0;
793
				$net_refund = ! empty( $balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'net' ) : 0;
794
795
				// Current data fee & net.
796
				$fee_current = WC_Stripe_Helper::get_stripe_fee( $order );
797
				$net_current = WC_Stripe_Helper::get_stripe_net( $order );
798
799
				// Calculation.
800
				$fee = (float) $fee_current + (float) $fee_refund;
801
				$net = (float) $net_current + (float) $net_refund;
802
803
				WC_Stripe_Helper::update_stripe_fee( $order, $fee );
804
				WC_Stripe_Helper::update_stripe_net( $order, $net );
805
806
				$currency = ! empty( $balance_transaction->currency ) ? strtoupper( $balance_transaction->currency ) : null;
807
				WC_Stripe_Helper::update_stripe_currency( $order, $currency );
808
809
				if ( is_callable( array( $order, 'save' ) ) ) {
810
					$order->save();
811
				}
812
			}
813
		} else {
814
			WC_Stripe_Logger::log( 'Unable to update fees/net meta for order: ' . $order->get_id() );
815
		}
816
	}
817
818
	/**
819
	 * Refund a charge.
820
	 *
821
	 * @since 3.1.0
822
	 * @version 4.0.0
823
	 * @param  int $order_id
824
	 * @param  float $amount
825
	 * @return bool
826
	 */
827
	public function process_refund( $order_id, $amount = null, $reason = '' ) {
828
		$order = wc_get_order( $order_id );
829
830
		if ( ! $order ) {
831
			return false;
832
		}
833
834
		$request = array();
835
836
		$order_currency = $order->get_currency();
837
		$captured       = $order->get_meta( '_stripe_charge_captured', true );
838
		$charge_id      = $order->get_transaction_id();
839
840
		if ( ! $charge_id ) {
841
			return false;
842
		}
843
844
		if ( ! is_null( $amount ) ) {
845
			$request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $order_currency );
846
		}
847
848
		// If order is only authorized, don't pass amount.
849
		if ( 'yes' !== $captured ) {
850
			unset( $request['amount'] );
851
		}
852
853
		if ( $reason ) {
854
			$request['metadata'] = array(
855
				'reason' => $reason,
856
			);
857
		}
858
859
		$request['charge'] = $charge_id;
860
		WC_Stripe_Logger::log( "Info: Beginning refund for order {$charge_id} for the amount of {$amount}" );
861
862
		$request = apply_filters( 'wc_stripe_refund_request', $request, $order );
863
864
		$intent = $this->get_intent_from_order( $order );
865
		$intent_cancelled = false;
866
		if ( $intent ) {
867
			// If the order has a Payment Intent pending capture, then the Intent itself must be refunded (cancelled), not the Charge
868
			if ( ! empty( $intent->error ) ) {
869
				$response = $intent;
870
				$intent_cancelled = true;
871
			} elseif ( 'requires_capture' === $intent->status ) {
872
				$result = WC_Stripe_API::request(
873
					array(),
874
					'payment_intents/' . $intent->id . '/cancel'
875
				);
876
				$intent_cancelled = true;
877
878
				if ( ! empty( $result->error ) ) {
879
					$response = $result;
880
				} else {
881
					$charge = end( $result->charges->data );
882
					$response = end( $charge->refunds->data );
883
				}
884
			}
885
		}
886
887
		if ( ! $intent_cancelled ) {
888
			$response = WC_Stripe_API::request( $request, 'refunds' );
889
		}
890
891
		if ( ! empty( $response->error ) ) {
892
			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...
893
894
			return $response;
895
896
		} elseif ( ! empty( $response->id ) ) {
897
			$order->update_meta_data( '_stripe_refund_id', $response->id );
898
899
			$amount = wc_price( $response->amount / 100 );
900
901
			if ( in_array( strtolower( $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
902
				$amount = wc_price( $response->amount );
903
			}
904
905
			if ( isset( $response->balance_transaction ) ) {
906
				$this->update_fees( $order, $response->balance_transaction );
907
			}
908
909
			/* translators: 1) dollar amount 2) transaction id 3) refund message */
910
			$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' );
911
912
			$order->add_order_note( $refund_message );
913
			WC_Stripe_Logger::log( 'Success: ' . html_entity_decode( wp_strip_all_tags( $refund_message ) ) );
914
915
			return true;
916
		}
917
	}
918
919
	/**
920
	 * Add payment method via account screen.
921
	 * We don't store the token locally, but to the Stripe API.
922
	 *
923
	 * @since 3.0.0
924
	 * @version 4.0.0
925
	 */
926
	public function add_payment_method() {
927
		$error     = false;
928
		$error_msg = __( 'There was a problem adding the payment method.', 'woocommerce-gateway-stripe' );
929
		$source_id = '';
930
931
		if ( empty( $_POST['stripe_source'] ) && empty( $_POST['stripe_token'] ) || ! is_user_logged_in() ) {
932
			$error = true;
933
		}
934
935
		$stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
936
937
		$source = ! empty( $_POST['stripe_source'] ) ? wc_clean( $_POST['stripe_source'] ) : '';
938
939
		$source_object = WC_Stripe_API::retrieve( 'sources/' . $source );
940
941
		if ( isset( $source_object ) ) {
942
			if ( ! empty( $source_object->error ) ) {
943
				$error = true;
944
			}
945
946
			$source_id = $source_object->id;
947
		} elseif ( isset( $_POST['stripe_token'] ) ) {
948
			$source_id = wc_clean( $_POST['stripe_token'] );
949
		}
950
951
		$response = $stripe_customer->add_source( $source_id );
952
953
		if ( ! $response || is_wp_error( $response ) || ! empty( $response->error ) ) {
954
			$error = true;
955
		}
956
957
		if ( $error ) {
958
			wc_add_notice( $error_msg, 'error' );
959
			WC_Stripe_Logger::log( 'Add payment method Error: ' . $error_msg );
960
			return;
961
		}
962
963
		do_action( 'wc_stripe_add_payment_method_' . $_POST['payment_method'] . '_success', $source_id, $source_object );
964
965
		return array(
966
			'result'   => 'success',
967
			'redirect' => wc_get_endpoint_url( 'payment-methods' ),
968
		);
969
	}
970
971
	/**
972
	 * Gets the locale with normalization that only Stripe accepts.
973
	 *
974
	 * @since 4.0.6
975
	 * @return string $locale
976
	 */
977
	public function get_locale() {
978
		$locale = get_locale();
979
980
		/*
981
		 * Stripe expects Norwegian to only be passed NO.
982
		 * But WP has different dialects.
983
		 */
984
		if ( 'NO' === substr( $locale, 3, 2 ) ) {
985
			$locale = 'no';
986
		} else {
987
			$locale = substr( get_locale(), 0, 2 );
988
		}
989
990
		return $locale;
991
	}
992
993
	/**
994
	 * Change the idempotency key so charge can
995
	 * process order as a different transaction.
996
	 *
997
	 * @since 4.0.6
998
	 * @param string $idempotency_key
999
	 * @param array $request
1000
	 */
1001
	public function change_idempotency_key( $idempotency_key, $request ) {
1002
		$customer = ! empty( $request['customer'] ) ? $request['customer'] : '';
1003
		$source   = ! empty( $request['source'] ) ? $request['source'] : $customer;
1004
		$count    = $this->retry_interval;
1005
1006
		return $request['metadata']['order_id'] . '-' . $count . '-' . $source;
1007
	}
1008
1009
	/**
1010
	 * Checks if request is the original to prevent double processing
1011
	 * on WC side. The original-request header and request-id header
1012
	 * needs to be the same to mean its the original request.
1013
	 *
1014
	 * @since 4.0.6
1015
	 * @param array $headers
1016
	 */
1017
	public function is_original_request( $headers ) {
1018
		if ( $headers['original-request'] === $headers['request-id'] ) {
1019
			return true;
1020
		}
1021
1022
		return false;
1023
	}
1024
1025
	/**
1026
	 * Generates the request when creating a new payment intent.
1027
	 *
1028
	 * @param WC_Order $order           The order that is being paid for.
1029
	 * @param object   $prepared_source The source that is used for the payment.
1030
	 * @return array                    The arguments for the request.
1031
	 */
1032
	public function generate_create_intent_request( $order, $prepared_source ) {
1033
		// The request for a charge contains metadata for the intent.
1034
		$full_request = $this->generate_payment_request( $order, $prepared_source );
1035
1036
		$request = array(
1037
			'source'               => $prepared_source->source,
1038
			'amount'               => WC_Stripe_Helper::get_stripe_amount( $order->get_total() ),
1039
			'currency'             => strtolower( $order->get_currency() ),
1040
			'description'          => $full_request['description'],
1041
			'metadata'             => $full_request['metadata'],
1042
			'capture_method'       => ( 'true' === $full_request['capture'] ) ? 'automatic' : 'manual',
1043
			'payment_method_types' => array(
1044
				'card',
1045
			),
1046
		);
1047
1048
		if ( $prepared_source->customer ) {
1049
			$request['customer'] = $prepared_source->customer;
1050
		}
1051
1052
		if ( isset( $full_request['statement_descriptor'] ) ) {
1053
			$request['statement_descriptor'] = $full_request['statement_descriptor'];
1054
		}
1055
1056
		if ( isset( $full_request['shipping'] ) ) {
1057
			$request['shipping'] = $full_request['shipping'];
1058
		}
1059
1060
		/**
1061
		 * Filter the return value of the WC_Payment_Gateway_CC::generate_create_intent_request.
1062
		 *
1063
		 * @since 3.1.0
1064
		 * @param array $request
1065
		 * @param WC_Order $order
1066
		 * @param object $source
1067
		 */
1068
		return apply_filters( 'wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1069
	}
1070
1071
	/**
1072
	 * Create the level 3 data array to send to Stripe when making a purchase.
1073
	 *
1074
	 * @param WC_Order $order The order that is being paid for.
1075
	 * @return array          The level 3 data to send to Stripe.
1076
	 */
1077
	public function get_level3_data_from_order( $order ) {
1078
		// Get the order items. Don't need their keys, only their values.
1079
		// Order item IDs are used as keys in the original order items array.
1080
		$order_items = array_values( $order->get_items() );
1081
		$currency    = $order->get_currency();
1082
1083
		$stripe_line_items = array_map(function( $item ) use ( $currency ) {
1084
			$product_id          = $item->get_variation_id()
1085
				? $item->get_variation_id()
1086
				: $item->get_product_id();
1087
			$product_description = substr( $item->get_name(), 0, 26 );
1088
			$quantity            = $item->get_quantity();
1089
			$unit_cost           = WC_Stripe_Helper::get_stripe_amount( ( $item->get_subtotal() / $quantity ), $currency );
1090
			$tax_amount          = WC_Stripe_Helper::get_stripe_amount( $item->get_total_tax(), $currency );
1091
			$discount_amount     = WC_Stripe_Helper::get_stripe_amount( $item->get_subtotal() - $item->get_total(), $currency );
1092
1093
			return (object) array(
1094
				'product_code'        => (string) $product_id, // Up to 12 characters that uniquely identify the product.
1095
				'product_description' => $product_description, // Up to 26 characters long describing the product.
1096
				'unit_cost'           => $unit_cost, // Cost of the product, in cents, as a non-negative integer.
1097
				'quantity'            => $quantity, // The number of items of this type sold, as a non-negative integer.
1098
				'tax_amount'          => $tax_amount, // The amount of tax this item had added to it, in cents, as a non-negative integer.
1099
				'discount_amount'     => $discount_amount, // The amount an item was discounted—if there was a sale,for example, as a non-negative integer.
1100
			);
1101
		}, $order_items);
1102
1103
		$level3_data = array(
1104
			'merchant_reference'   => $order->get_id(), // An alphanumeric string of up to  characters in length. This unique value is assigned by the merchant to identify the order. Also known as an “Order ID”.
1105
			'shipping_amount'      => WC_Stripe_Helper::get_stripe_amount( (float) $order->get_shipping_total() + (float) $order->get_shipping_tax(), $currency), // The shipping cost, in cents, as a non-negative integer.
1106
			'line_items'           => $stripe_line_items,
1107
		);
1108
1109
		// The customer’s U.S. shipping ZIP code.
1110
		$shipping_address_zip = $order->get_shipping_postcode();
1111
		if ( $this->is_valid_us_zip_code( $shipping_address_zip ) ) {
1112
			$level3_data['shipping_address_zip'] = $shipping_address_zip;
1113
		}
1114
1115
		// The merchant’s U.S. shipping ZIP code.
1116
		$store_postcode = get_option( 'woocommerce_store_postcode' );
1117
		if ( $this->is_valid_us_zip_code( $store_postcode ) ) {
1118
			$level3_data['shipping_from_zip'] = $store_postcode;
1119
		}
1120
1121
		return $level3_data;
1122
	}
1123
1124
	/**
1125
	 * Create a new PaymentIntent.
1126
	 *
1127
	 * @param WC_Order $order           The order that is being paid for.
1128
	 * @param object   $prepared_source The source that is used for the payment.
1129
	 * @return object                   An intent or an error.
1130
	 */
1131
	public function create_intent( $order, $prepared_source ) {
1132
		$request = $this->generate_create_intent_request( $order, $prepared_source );
1133
1134
		// Create an intent that awaits an action.
1135
		$intent = WC_Stripe_API::request( $request, 'payment_intents' );
1136
		if ( ! empty( $intent->error ) ) {
1137
			return $intent;
1138
		}
1139
1140
		$order_id = $order->get_id();
1141
		WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id initiated for order $order_id" );
1142
1143
		// Save the intent ID to the order.
1144
		$this->save_intent_to_order( $order, $intent );
1145
1146
		return $intent;
1147
	}
1148
1149
	/**
1150
	 * Updates an existing intent with updated amount, source, and customer.
1151
	 *
1152
	 * @param object   $intent          The existing intent object.
1153
	 * @param WC_Order $order           The order.
1154
	 * @param object   $prepared_source Currently selected source.
1155
	 * @return object                   An updated intent.
1156
	 */
1157
	public function update_existing_intent( $intent, $order, $prepared_source ) {
1158
		$request = array();
1159
1160
		if ( $prepared_source->source !== $intent->source ) {
1161
			$request['source'] = $prepared_source->source;
1162
		}
1163
1164
		$new_amount = WC_Stripe_Helper::get_stripe_amount( $order->get_total() );
1165
		if ( $intent->amount !== $new_amount ) {
1166
			$request['amount'] = $new_amount;
1167
		}
1168
1169
		if ( $prepared_source->customer && $intent->customer !== $prepared_source->customer ) {
1170
			$request['customer'] = $prepared_source->customer;
1171
		}
1172
1173
		if ( $this->has_subscription( $order ) ) {
1174
			// If this is a failed subscription order payment, the intent should be
1175
			// prepared for future usage.
1176
			$request['setup_future_usage'] = 'off_session';
1177
		}
1178
1179
		if ( empty( $request ) ) {
1180
			return $intent;
1181
		}
1182
1183
		$level3_data = $this->get_level3_data_from_order( $order );
1184
		return WC_Stripe_API::request_with_level3_data(
1185
			$request,
1186
			"payment_intents/$intent->id",
1187
			$level3_data,
1188
			$order
1189
		);
1190
	}
1191
1192
	/**
1193
	 * Confirms an intent if it is the `requires_confirmation` state.
1194
	 *
1195
	 * @since 4.2.1
1196
	 * @param object   $intent          The intent to confirm.
1197
	 * @param WC_Order $order           The order that the intent is associated with.
1198
	 * @param object   $prepared_source The source that is being charged.
1199
	 * @return object                   Either an error or the updated intent.
1200
	 */
1201
	public function confirm_intent( $intent, $order, $prepared_source ) {
1202
		if ( 'requires_confirmation' !== $intent->status ) {
1203
			return $intent;
1204
		}
1205
1206
		// Try to confirm the intent & capture the charge (if 3DS is not required).
1207
		$confirm_request = array(
1208
			'source' => $prepared_source->source,
1209
		);
1210
1211
		$level3_data = $this->get_level3_data_from_order( $order );
1212
		$confirmed_intent = WC_Stripe_API::request_with_level3_data(
1213
			$confirm_request,
1214
			"payment_intents/$intent->id/confirm",
1215
			$level3_data,
1216
			$order
1217
		);
1218
1219
		if ( ! empty( $confirmed_intent->error ) ) {
1220
			return $confirmed_intent;
1221
		}
1222
1223
		// Save a note about the status of the intent.
1224
		$order_id = $order->get_id();
1225
		if ( 'succeeded' === $confirmed_intent->status ) {
1226
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
1227
		} elseif ( 'requires_action' === $confirmed_intent->status ) {
1228
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id requires authentication for order $order_id" );
1229
		}
1230
1231
		return $confirmed_intent;
1232
	}
1233
1234
	/**
1235
	 * Saves intent to order.
1236
	 *
1237
	 * @since 3.2.0
1238
	 * @param WC_Order $order For to which the source applies.
1239
	 * @param stdClass $intent Payment intent information.
1240
	 */
1241
	public function save_intent_to_order( $order, $intent ) {
1242
		$order->update_meta_data( '_stripe_intent_id', $intent->id );
1243
1244
		if ( is_callable( array( $order, 'save' ) ) ) {
1245
			$order->save();
1246
		}
1247
	}
1248
1249
	/**
1250
	 * Retrieves the payment intent, associated with an order.
1251
	 *
1252
	 * @since 4.2
1253
	 * @param WC_Order $order The order to retrieve an intent for.
1254
	 * @return obect|bool     Either the intent object or `false`.
1255
	 */
1256
	public function get_intent_from_order( $order ) {
1257
		$intent_id = $order->get_meta( '_stripe_intent_id' );
1258
1259
		if ( $intent_id ) {
1260
			return $this->get_intent( 'payment_intents', $intent_id );
1261
		}
1262
1263
		// The order doesn't have a payment intent, but it may have a setup intent.
1264
		$intent_id = $order->get_meta( '_stripe_setup_intent' );
1265
1266
		if ( $intent_id ) {
1267
			return $this->get_intent( 'setup_intents', $intent_id );
1268
		}
1269
1270
		return false;
1271
	}
1272
1273
	/**
1274
	 * Retrieves intent from Stripe API by intent id.
1275
	 *
1276
	 * @param string $intent_type 	Either 'payment_intents' or 'setup_intents'.
1277
	 * @param string $intent_id		Intent id.
1278
	 * @return object|bool 			Either the intent object or `false`.
1279
	 * @throws Exception 			Throws exception for unknown $intent_type.
1280
	 */
1281
	private function get_intent( $intent_type, $intent_id ) {
1282
		if ( ! in_array( $intent_type, [ 'payment_intents', 'setup_intents' ] ) ) {
1283
			throw new Exception( "Failed to get intent of type $intent_type. Type is not allowed" );
1284
		}
1285
1286
		$response = WC_Stripe_API::request( array(), "$intent_type/$intent_id", 'GET' );
1287
1288
		if ( $response && isset( $response->{ 'error' } ) ) {
1289
			$error_response_message = print_r( $response, true );
1290
			WC_Stripe_Logger::log("Failed to get Stripe intent $intent_type/$intent_id.");
1291
			WC_Stripe_Logger::log("Response: $error_response_message");
1292
			return false;
1293
		}
1294
1295
		return $response;
1296
	}
1297
1298
	/**
1299
	 * Locks an order for payment intent processing for 5 minutes.
1300
	 *
1301
	 * @since 4.2
1302
	 * @param WC_Order $order  The order that is being paid.
1303
	 * @param stdClass $intent The intent that is being processed.
1304
	 * @return bool            A flag that indicates whether the order is already locked.
1305
	 */
1306
	public function lock_order_payment( $order, $intent = null ) {
1307
		$order_id       = $order->get_id();
1308
		$transient_name = 'wc_stripe_processing_intent_' . $order_id;
1309
		$processing     = get_transient( $transient_name );
1310
1311
		// Block the process if the same intent is already being handled.
1312
		if ( "-1" === $processing || ( isset( $intent->id ) && $processing === $intent->id ) ) {
1313
			return true;
1314
		}
1315
1316
		// Save the new intent as a transient, eventually overwriting another one.
1317
		set_transient( $transient_name, empty( $intent ) ? '-1' : $intent->id, 5 * MINUTE_IN_SECONDS );
1318
1319
		return false;
1320
	}
1321
1322
	/**
1323
	 * Unlocks an order for processing by payment intents.
1324
	 *
1325
	 * @since 4.2
1326
	 * @param WC_Order $order The order that is being unlocked.
1327
	 */
1328
	public function unlock_order_payment( $order ) {
1329
		$order_id = $order->get_id();
1330
		delete_transient( 'wc_stripe_processing_intent_' . $order_id );
1331
	}
1332
1333
	/**
1334
	 * Given a response from Stripe, check if it's a card error where authentication is required
1335
	 * to complete the payment.
1336
	 *
1337
	 * @param object $response The response from Stripe.
1338
	 * @return boolean Whether or not it's a 'authentication_required' error
1339
	 */
1340
	public function is_authentication_required_for_payment( $response ) {
1341
		return ( ! empty( $response->error ) && 'authentication_required' === $response->error->code )
1342
			|| ( ! empty( $response->last_payment_error ) && 'authentication_required' === $response->last_payment_error->code );
1343
	}
1344
1345
	/**
1346
	 * Creates a SetupIntent for future payments, and saves it to the order.
1347
	 *
1348
	 * @param WC_Order $order           The ID of the (free/pre- order).
1349
	 * @param object   $prepared_source The source, entered/chosen by the customer.
1350
	 * @return string                   The client secret of the intent, used for confirmation in JS.
1351
	 */
1352
	public function setup_intent( $order, $prepared_source ) {
1353
		$order_id     = $order->get_id();
1354
		$setup_intent = WC_Stripe_API::request( array(
1355
			'payment_method' => $prepared_source->source,
1356
			'customer'       => $prepared_source->customer,
1357
			'confirm'        => 'true',
1358
		), 'setup_intents' );
1359
1360
		if ( is_wp_error( $setup_intent ) ) {
1361
			WC_Stripe_Logger::log( "Unable to create SetupIntent for Order #$order_id: " . print_r( $setup_intent, true ) );
1362
		} elseif ( 'requires_action' === $setup_intent->status ) {
1363
			$order->update_meta_data( '_stripe_setup_intent', $setup_intent->id );
1364
			$order->save();
1365
1366
			return $setup_intent->client_secret;
1367
		}
1368
	}
1369
1370
	/**
1371
	 * Create and confirm a new PaymentIntent.
1372
	 *
1373
	 * @param WC_Order $order           The order that is being paid for.
1374
	 * @param object   $prepared_source The source that is used for the payment.
1375
	 * @param float    $amount          The amount to charge. If not specified, it will be read from the order.
1376
	 * @return object                   An intent or an error.
1377
	 */
1378
	public function create_and_confirm_intent_for_off_session( $order, $prepared_source, $amount = NULL ) {
1379
		// The request for a charge contains metadata for the intent.
1380
		$full_request = $this->generate_payment_request( $order, $prepared_source );
1381
1382
		$request = array(
1383
			'amount'               => $amount ? WC_Stripe_Helper::get_stripe_amount( $amount, $full_request['currency'] ) : $full_request['amount'],
1384
			'currency'             => $full_request['currency'],
1385
			'description'          => $full_request['description'],
1386
			'metadata'             => $full_request['metadata'],
1387
			'payment_method_types' => array(
1388
				'card',
1389
			),
1390
			'off_session'          => 'true',
1391
			'confirm'              => 'true',
1392
			'confirmation_method'  => 'automatic',
1393
		);
1394
1395
		if ( isset( $full_request['statement_descriptor'] ) ) {
1396
			$request['statement_descriptor'] = $full_request['statement_descriptor'];
1397
		}
1398
1399
		if ( isset( $full_request['customer'] ) ) {
1400
			$request['customer'] = $full_request['customer'];
1401
		}
1402
1403
		if ( isset( $full_request['source'] ) ) {
1404
			$is_source = 'src_' === substr( $full_request['source'], 0, 4 );
1405
			$request[ $is_source ? 'source' : 'payment_method' ] = $full_request['source'];
1406
		}
1407
1408
		/**
1409
		 * Filter the value of the request.
1410
		 *
1411
		 * @since 4.5.0
1412
		 * @param array $request
1413
		 * @param WC_Order $order
1414
		 * @param object $source
1415
		 */
1416
		$request = apply_filters('wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1417
1418
		if ( isset( $full_request['shipping'] ) ) {
1419
			$request['shipping'] = $full_request['shipping'];
1420
		}
1421
1422
		$level3_data = $this->get_level3_data_from_order( $order );
1423
		$intent = WC_Stripe_API::request_with_level3_data(
1424
			$request,
1425
			'payment_intents',
1426
			$level3_data,
1427
			$order
1428
		);
1429
		$is_authentication_required = $this->is_authentication_required_for_payment( $intent );
1430
1431
		if ( ! empty( $intent->error ) && ! $is_authentication_required ) {
1432
			return $intent;
1433
		}
1434
1435
		$intent_id      = ( ! empty( $intent->error )
1436
			? $intent->error->payment_intent->id
1437
			: $intent->id
1438
		);
1439
		$payment_intent = ( ! empty( $intent->error )
1440
			? $intent->error->payment_intent
1441
			: $intent
1442
		);
1443
		$order_id       = $order->get_id();
1444
		WC_Stripe_Logger::log( "Stripe PaymentIntent $intent_id initiated for order $order_id" );
1445
1446
		// Save the intent ID to the order.
1447
		$this->save_intent_to_order( $order, $payment_intent );
1448
1449
		return $intent;
1450
	}
1451
1452
	/**
1453
	 * Checks if subscription has a Stripe customer ID and adds it if doesn't.
1454
	 *
1455
	 * Fix renewal for existing subscriptions affected by https://github.com/woocommerce/woocommerce-gateway-stripe/issues/1072.
1456
	 * @param int $order_id subscription renewal order id.
1457
	 */
1458
	public function ensure_subscription_has_customer_id( $order_id ) {
1459
		$subscriptions_ids = wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'any' ) );
1460
		foreach( $subscriptions_ids as $subscription_id => $subscription ) {
1461
			if ( ! metadata_exists( 'post', $subscription_id, '_stripe_customer_id' ) ) {
1462
				$stripe_customer = new WC_Stripe_Customer( $subscription->get_user_id() );
1463
				update_post_meta( $subscription_id, '_stripe_customer_id', $stripe_customer->get_id() );
1464
				update_post_meta( $order_id, '_stripe_customer_id', $stripe_customer->get_id() );
1465
			}
1466
		}
1467
	}
1468
1469
	/** Verifies whether a certain ZIP code is valid for the US, incl. 4-digit extensions.
1470
	 *
1471
	 * @param string $zip The ZIP code to verify.
1472
	 * @return boolean
1473
	 */
1474
	public function is_valid_us_zip_code( $zip ) {
1475
		return ! empty( $zip ) && preg_match( '/^\d{5,5}(-\d{4,4})?$/', $zip );
1476
	}
1477
}
1478