Completed
Pull Request — master (#1407)
by
unknown
02:20
created

should_save_payment_method()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 9
nop 3
dl 0
loc 29
rs 8.0555
c 0
b 0
f 0
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
	 * Checks if the payment method is being saved in the current request.
594
	 *
595
	 * @param stdClass $source
596
	 * @param WC_Stripe_Customer $customer
597
	 * @param bool $force_save
598
	 *
599
	 * @return bool
600
	 * @since 4.6.0
601
	 */
602
	public function should_save_payment_method( $source, $customer, $force_save ) {
603
		if ( apply_filters( 'wc_stripe_force_save_source', $force_save, $customer ) ) {
604
			return true;
605
		}
606
607
		// The customer has disabled "Saved cards" functionality.
608
		if ( ! $this->saved_cards ) {
609
			return false;
610
		}
611
612
		// The customer is not logged in.
613
		if ( ! $customer->get_user_id() ) {
614
			return false;
615
		}
616
617
		// The payment method in use is already saved.
618
		if ( $this->is_using_saved_payment_method() ) {
619
			return false;
620
		}
621
622
		// If the payment method is explicitly not reusable, dont try to save it.
623
		if ( ! empty( $source ) && 'reusable' !== $source->usage ) {
624
			return false;
625
		}
626
627
		// Lastly, check if the user has chosen to save the payment method.
628
		$payment_method = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
629
		return isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
630
	}
631
632
	/**
633
	 * Get payment source. This can be a new token/source or existing WC token.
634
	 * If user is logged in and/or has WC account, create an account on Stripe.
635
	 * This way we can attribute the payment to the user to better fight fraud.
636
	 *
637
	 * @since 3.1.0
638
	 * @version 4.0.0
639
	 * @param string $user_id
640
	 * @param bool $force_save_source Should we force save payment source.
641
	 *
642
	 * @throws Exception When card was not added or for and invalid card.
643
	 * @return object
644
	 */
645
	public function prepare_source( $user_id, $force_save_source = false, $existing_customer_id = null ) {
646
		$customer = new WC_Stripe_Customer( $user_id );
647
		if ( ! empty( $existing_customer_id ) ) {
648
			$customer->set_id( $existing_customer_id );
649
		}
650
651
		$source_object = '';
652
		$wc_token_id   = false;
653
654
		if ( $this->is_using_saved_payment_method() ) {
655
			// Use an existing token, and then process the payment.
656
			$payment_method = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
657
			$wc_token_id    = wc_clean( $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
658
			$wc_token       = WC_Payment_Tokens::get( $wc_token_id );
659
660
			if ( ! $wc_token || $wc_token->get_user_id() !== get_current_user_id() ) {
661
				WC()->session->set( 'refresh_totals', true );
662
				throw new WC_Stripe_Exception( 'Invalid payment method', __( 'Invalid payment method. Please input a new card number.', 'woocommerce-gateway-stripe' ) );
663
			}
664
665
			$source_id = $wc_token->get_token();
666
667
			if ( ! $this->is_type_legacy_card( $source_id ) ) {
668
				$source_object = self::get_source_object( $source_id );
669
			}
670
		} else {
671
			// New CC info was entered and we have a new source to process.
672
			if ( ! empty( $_POST['stripe_source'] ) ) {
673
				$source_object = self::get_source_object( wc_clean( $_POST['stripe_source'] ) );
674
				$source_id     = $source_object->id;
675
			} elseif ( isset( $_POST['stripe_token'] ) && 'new' !== $_POST['stripe_token'] ) {
676
				$source_id = wc_clean( $_POST['stripe_token'] );
677
			}
678
679
			if ( $this->should_save_payment_method( $source_object, $customer, $force_save_source ) ) {
680
				$response = $customer->add_source( $source_id );
0 ignored issues
show
Bug introduced by
The variable $source_id 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...
681
682 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...
683
					throw new WC_Stripe_Exception( print_r( $response, true ), $this->get_localized_error_message_from_response( $response ) );
684
				}
685
686
				$source_id = $response;
687
				if ( empty( $source_object ) ) {
688
					$source_object = self::get_source_object( $source_id );
689
				}
690
			}
691
		}
692
693
		$customer_id = $customer->get_id();
694
		if ( ! $customer_id ) {
695
			$customer->set_id( $customer->create_customer() );
696
			$customer_id = $customer->get_id();
697
		} else {
698
			$customer_id = $customer->update_customer();
699
		}
700
701
		return (object) array(
702
			'token_id'      => $wc_token_id,
703
			'customer'      => $customer_id,
704
			'source'        => $source_id,
705
			'source_object' => $source_object,
706
		);
707
	}
708
709
	/**
710
	 * Get payment source from an order. This could be used in the future for
711
	 * a subscription as an example, therefore using the current user ID would
712
	 * not work - the customer won't be logged in :)
713
	 *
714
	 * Not using 2.6 tokens for this part since we need a customer AND a card
715
	 * token, and not just one.
716
	 *
717
	 * @since 3.1.0
718
	 * @version 4.0.0
719
	 * @param object $order
720
	 * @return object
721
	 */
722
	public function prepare_order_source( $order = null ) {
723
		$stripe_customer = new WC_Stripe_Customer();
724
		$stripe_source   = false;
725
		$token_id        = false;
726
		$source_object   = false;
727
728
		if ( $order ) {
729
			$order_id = $order->get_id();
730
731
			$stripe_customer_id = get_post_meta( $order_id, '_stripe_customer_id', true );
732
733
			if ( $stripe_customer_id ) {
734
				$stripe_customer->set_id( $stripe_customer_id );
735
			}
736
737
			$source_id = $order->get_meta( '_stripe_source_id', true );
738
739
			// Since 4.0.0, we changed card to source so we need to account for that.
740
			if ( empty( $source_id ) ) {
741
				$source_id = $order->get_meta( '_stripe_card_id', true );
742
743
				// Take this opportunity to update the key name.
744
				$order->update_meta_data( '_stripe_source_id', $source_id );
745
746
				if ( is_callable( array( $order, 'save' ) ) ) {
747
					$order->save();
748
				}
749
			}
750
751
			if ( $source_id ) {
752
				$stripe_source = $source_id;
753
				$source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
754
			} elseif ( apply_filters( 'wc_stripe_use_default_customer_source', true ) ) {
755
				/*
756
				 * We can attempt to charge the customer's default source
757
				 * by sending empty source id.
758
				 */
759
				$stripe_source = '';
760
			}
761
		}
762
763
		return (object) array(
764
			'token_id'      => $token_id,
765
			'customer'      => $stripe_customer ? $stripe_customer->get_id() : false,
766
			'source'        => $stripe_source,
767
			'source_object' => $source_object,
768
		);
769
	}
770
771
	/**
772
	 * Save source to order.
773
	 *
774
	 * @since 3.1.0
775
	 * @version 4.0.0
776
	 * @param WC_Order $order For to which the source applies.
777
	 * @param stdClass $source Source information.
778
	 */
779
	public function save_source_to_order( $order, $source ) {
780
		// Store source in the order.
781
		if ( $source->customer ) {
782
			$order->update_meta_data( '_stripe_customer_id', $source->customer );
783
		}
784
785
		if ( $source->source ) {
786
			$order->update_meta_data( '_stripe_source_id', $source->source );
787
		}
788
789
		if ( is_callable( array( $order, 'save' ) ) ) {
790
			$order->save();
791
		}
792
	}
793
794
	/**
795
	 * Updates Stripe fees/net.
796
	 * e.g usage would be after a refund.
797
	 *
798
	 * @since 4.0.0
799
	 * @version 4.0.6
800
	 * @param object $order The order object
801
	 * @param int $balance_transaction_id
802
	 */
803
	public function update_fees( $order, $balance_transaction_id ) {
804
		$balance_transaction = WC_Stripe_API::retrieve( 'balance/history/' . $balance_transaction_id );
805
806
		if ( empty( $balance_transaction->error ) ) {
807
			if ( isset( $balance_transaction ) && isset( $balance_transaction->fee ) ) {
808
				// Fees and Net needs to both come from Stripe to be accurate as the returned
809
				// values are in the local currency of the Stripe account, not from WC.
810
				$fee_refund = ! empty( $balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'fee' ) : 0;
811
				$net_refund = ! empty( $balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'net' ) : 0;
812
813
				// Current data fee & net.
814
				$fee_current = WC_Stripe_Helper::get_stripe_fee( $order );
815
				$net_current = WC_Stripe_Helper::get_stripe_net( $order );
816
817
				// Calculation.
818
				$fee = (float) $fee_current + (float) $fee_refund;
819
				$net = (float) $net_current + (float) $net_refund;
820
821
				WC_Stripe_Helper::update_stripe_fee( $order, $fee );
822
				WC_Stripe_Helper::update_stripe_net( $order, $net );
823
824
				$currency = ! empty( $balance_transaction->currency ) ? strtoupper( $balance_transaction->currency ) : null;
825
				WC_Stripe_Helper::update_stripe_currency( $order, $currency );
826
827
				if ( is_callable( array( $order, 'save' ) ) ) {
828
					$order->save();
829
				}
830
			}
831
		} else {
832
			WC_Stripe_Logger::log( 'Unable to update fees/net meta for order: ' . $order->get_id() );
833
		}
834
	}
835
836
	/**
837
	 * Refund a charge.
838
	 *
839
	 * @since 3.1.0
840
	 * @version 4.0.0
841
	 * @param  int $order_id
842
	 * @param  float $amount
843
	 * @return bool
844
	 */
845
	public function process_refund( $order_id, $amount = null, $reason = '' ) {
846
		$order = wc_get_order( $order_id );
847
848
		if ( ! $order ) {
849
			return false;
850
		}
851
852
		$request = array();
853
854
		$order_currency = $order->get_currency();
855
		$captured       = $order->get_meta( '_stripe_charge_captured', true );
856
		$charge_id      = $order->get_transaction_id();
857
858
		if ( ! $charge_id ) {
859
			return false;
860
		}
861
862
		if ( ! is_null( $amount ) ) {
863
			$request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $order_currency );
864
		}
865
866
		// If order is only authorized, don't pass amount.
867
		if ( 'yes' !== $captured ) {
868
			unset( $request['amount'] );
869
		}
870
871
		if ( $reason ) {
872
			$request['metadata'] = array(
873
				'reason' => $reason,
874
			);
875
		}
876
877
		$request['charge'] = $charge_id;
878
		WC_Stripe_Logger::log( "Info: Beginning refund for order {$charge_id} for the amount of {$amount}" );
879
880
		$request = apply_filters( 'wc_stripe_refund_request', $request, $order );
881
882
		$intent = $this->get_intent_from_order( $order );
883
		$intent_cancelled = false;
884
		if ( $intent ) {
885
			// If the order has a Payment Intent pending capture, then the Intent itself must be refunded (cancelled), not the Charge
886
			if ( ! empty( $intent->error ) ) {
887
				$response = $intent;
888
				$intent_cancelled = true;
889
			} elseif ( 'requires_capture' === $intent->status ) {
890
				$result = WC_Stripe_API::request(
891
					array(),
892
					'payment_intents/' . $intent->id . '/cancel'
893
				);
894
				$intent_cancelled = true;
895
896
				if ( ! empty( $result->error ) ) {
897
					$response = $result;
898
				} else {
899
					$charge = end( $result->charges->data );
900
					$response = end( $charge->refunds->data );
901
				}
902
			}
903
		}
904
905
		if ( ! $intent_cancelled ) {
906
			$response = WC_Stripe_API::request( $request, 'refunds' );
907
		}
908
909
		if ( ! empty( $response->error ) ) {
910
			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...
911
912
			return $response;
913
914
		} elseif ( ! empty( $response->id ) ) {
915
			$order->update_meta_data( '_stripe_refund_id', $response->id );
916
917
			$amount = wc_price( $response->amount / 100 );
918
919
			if ( in_array( strtolower( $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
920
				$amount = wc_price( $response->amount );
921
			}
922
923
			if ( isset( $response->balance_transaction ) ) {
924
				$this->update_fees( $order, $response->balance_transaction );
925
			}
926
927
			/* translators: 1) dollar amount 2) transaction id 3) refund message */
928
			$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' );
929
930
			$order->add_order_note( $refund_message );
931
			WC_Stripe_Logger::log( 'Success: ' . html_entity_decode( wp_strip_all_tags( $refund_message ) ) );
932
933
			return true;
934
		}
935
	}
936
937
	/**
938
	 * Add payment method via account screen.
939
	 * We don't store the token locally, but to the Stripe API.
940
	 *
941
	 * @since 3.0.0
942
	 * @version 4.0.0
943
	 */
944
	public function add_payment_method() {
945
		$error     = false;
946
		$error_msg = __( 'There was a problem adding the payment method.', 'woocommerce-gateway-stripe' );
947
		$source_id = '';
948
949
		if ( empty( $_POST['stripe_source'] ) && empty( $_POST['stripe_token'] ) || ! is_user_logged_in() ) {
950
			$error = true;
951
		}
952
953
		$stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
954
955
		$source = ! empty( $_POST['stripe_source'] ) ? wc_clean( $_POST['stripe_source'] ) : '';
956
957
		$source_object = WC_Stripe_API::retrieve( 'sources/' . $source );
958
959
		if ( isset( $source_object ) ) {
960
			if ( ! empty( $source_object->error ) ) {
961
				$error = true;
962
			}
963
964
			$source_id = $source_object->id;
965
		} elseif ( isset( $_POST['stripe_token'] ) ) {
966
			$source_id = wc_clean( $_POST['stripe_token'] );
967
		}
968
969
		$response = $stripe_customer->add_source( $source_id );
970
971
		if ( ! $response || is_wp_error( $response ) || ! empty( $response->error ) ) {
972
			$error = true;
973
		}
974
975
		if ( $error ) {
976
			wc_add_notice( $error_msg, 'error' );
977
			WC_Stripe_Logger::log( 'Add payment method Error: ' . $error_msg );
978
			return;
979
		}
980
981
		do_action( 'wc_stripe_add_payment_method_' . $_POST['payment_method'] . '_success', $source_id, $source_object );
982
983
		return array(
984
			'result'   => 'success',
985
			'redirect' => wc_get_endpoint_url( 'payment-methods' ),
986
		);
987
	}
988
989
	/**
990
	 * Gets the locale with normalization that only Stripe accepts.
991
	 *
992
	 * @since 4.0.6
993
	 * @return string $locale
994
	 */
995
	public function get_locale() {
996
		$locale = get_locale();
997
998
		/*
999
		 * Stripe expects Norwegian to only be passed NO.
1000
		 * But WP has different dialects.
1001
		 */
1002
		if ( 'NO' === substr( $locale, 3, 2 ) ) {
1003
			$locale = 'no';
1004
		} else {
1005
			$locale = substr( get_locale(), 0, 2 );
1006
		}
1007
1008
		return $locale;
1009
	}
1010
1011
	/**
1012
	 * Change the idempotency key so charge can
1013
	 * process order as a different transaction.
1014
	 *
1015
	 * @since 4.0.6
1016
	 * @param string $idempotency_key
1017
	 * @param array $request
1018
	 */
1019
	public function change_idempotency_key( $idempotency_key, $request ) {
1020
		$customer = ! empty( $request['customer'] ) ? $request['customer'] : '';
1021
		$source   = ! empty( $request['source'] ) ? $request['source'] : $customer;
1022
		$count    = $this->retry_interval;
1023
1024
		return $request['metadata']['order_id'] . '-' . $count . '-' . $source;
1025
	}
1026
1027
	/**
1028
	 * Checks if request is the original to prevent double processing
1029
	 * on WC side. The original-request header and request-id header
1030
	 * needs to be the same to mean its the original request.
1031
	 *
1032
	 * @since 4.0.6
1033
	 * @param array $headers
1034
	 */
1035
	public function is_original_request( $headers ) {
1036
		if ( $headers['original-request'] === $headers['request-id'] ) {
1037
			return true;
1038
		}
1039
1040
		return false;
1041
	}
1042
1043
	/**
1044
	 * Generates the request when creating a new payment intent.
1045
	 *
1046
	 * @param WC_Order $order           The order that is being paid for.
1047
	 * @param object   $prepared_source The source that is used for the payment.
1048
	 * @return array                    The arguments for the request.
1049
	 */
1050
	public function generate_create_intent_request( $order, $prepared_source ) {
1051
		// The request for a charge contains metadata for the intent.
1052
		$full_request = $this->generate_payment_request( $order, $prepared_source );
1053
1054
		$request = array(
1055
			'source'               => $prepared_source->source,
1056
			'amount'               => WC_Stripe_Helper::get_stripe_amount( $order->get_total() ),
1057
			'currency'             => strtolower( $order->get_currency() ),
1058
			'description'          => $full_request['description'],
1059
			'metadata'             => $full_request['metadata'],
1060
			'capture_method'       => ( 'true' === $full_request['capture'] ) ? 'automatic' : 'manual',
1061
			'payment_method_types' => array(
1062
				'card',
1063
			),
1064
		);
1065
1066
		if ( $prepared_source->customer ) {
1067
			$request['customer'] = $prepared_source->customer;
1068
		}
1069
1070
		if ( isset( $full_request['statement_descriptor'] ) ) {
1071
			$request['statement_descriptor'] = $full_request['statement_descriptor'];
1072
		}
1073
1074
		if ( isset( $full_request['shipping'] ) ) {
1075
			$request['shipping'] = $full_request['shipping'];
1076
		}
1077
1078
		/**
1079
		 * Filter the return value of the WC_Payment_Gateway_CC::generate_create_intent_request.
1080
		 *
1081
		 * @since 3.1.0
1082
		 * @param array $request
1083
		 * @param WC_Order $order
1084
		 * @param object $source
1085
		 */
1086
		return apply_filters( 'wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1087
	}
1088
1089
	/**
1090
	 * Create the level 3 data array to send to Stripe when making a purchase.
1091
	 *
1092
	 * @param WC_Order $order The order that is being paid for.
1093
	 * @return array          The level 3 data to send to Stripe.
1094
	 */
1095
	public function get_level3_data_from_order( $order ) {
1096
		// Get the order items. Don't need their keys, only their values.
1097
		// Order item IDs are used as keys in the original order items array.
1098
		$order_items = array_values( $order->get_items() );
1099
		$currency    = $order->get_currency();
1100
1101
		$stripe_line_items = array_map(function( $item ) use ( $currency ) {
1102
			$product_id          = $item->get_variation_id()
1103
				? $item->get_variation_id()
1104
				: $item->get_product_id();
1105
			$product_description = substr( $item->get_name(), 0, 26 );
1106
			$quantity            = $item->get_quantity();
1107
			$unit_cost           = WC_Stripe_Helper::get_stripe_amount( ( $item->get_subtotal() / $quantity ), $currency );
1108
			$tax_amount          = WC_Stripe_Helper::get_stripe_amount( $item->get_total_tax(), $currency );
1109
			$discount_amount     = WC_Stripe_Helper::get_stripe_amount( $item->get_subtotal() - $item->get_total(), $currency );
1110
1111
			return (object) array(
1112
				'product_code'        => (string) $product_id, // Up to 12 characters that uniquely identify the product.
1113
				'product_description' => $product_description, // Up to 26 characters long describing the product.
1114
				'unit_cost'           => $unit_cost, // Cost of the product, in cents, as a non-negative integer.
1115
				'quantity'            => $quantity, // The number of items of this type sold, as a non-negative integer.
1116
				'tax_amount'          => $tax_amount, // The amount of tax this item had added to it, in cents, as a non-negative integer.
1117
				'discount_amount'     => $discount_amount, // The amount an item was discounted—if there was a sale,for example, as a non-negative integer.
1118
			);
1119
		}, $order_items);
1120
1121
		$level3_data = array(
1122
			'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”.
1123
			'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.
1124
			'line_items'           => $stripe_line_items,
1125
		);
1126
1127
		// The customer’s U.S. shipping ZIP code.
1128
		$shipping_address_zip = $order->get_shipping_postcode();
1129
		if ( $this->is_valid_us_zip_code( $shipping_address_zip ) ) {
1130
			$level3_data['shipping_address_zip'] = $shipping_address_zip;
1131
		}
1132
1133
		// The merchant’s U.S. shipping ZIP code.
1134
		$store_postcode = get_option( 'woocommerce_store_postcode' );
1135
		if ( $this->is_valid_us_zip_code( $store_postcode ) ) {
1136
			$level3_data['shipping_from_zip'] = $store_postcode;
1137
		}
1138
1139
		return $level3_data;
1140
	}
1141
1142
	/**
1143
	 * Create a new PaymentIntent.
1144
	 *
1145
	 * @param WC_Order $order           The order that is being paid for.
1146
	 * @param object   $prepared_source The source that is used for the payment.
1147
	 * @return object                   An intent or an error.
1148
	 */
1149
	public function create_intent( $order, $prepared_source ) {
1150
		$request = $this->generate_create_intent_request( $order, $prepared_source );
1151
1152
		// Create an intent that awaits an action.
1153
		$intent = WC_Stripe_API::request( $request, 'payment_intents' );
1154
		if ( ! empty( $intent->error ) ) {
1155
			return $intent;
1156
		}
1157
1158
		$order_id = $order->get_id();
1159
		WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id initiated for order $order_id" );
1160
1161
		// Save the intent ID to the order.
1162
		$this->save_intent_to_order( $order, $intent );
1163
1164
		return $intent;
1165
	}
1166
1167
	/**
1168
	 * Updates an existing intent with updated amount, source, and customer.
1169
	 *
1170
	 * @param object   $intent          The existing intent object.
1171
	 * @param WC_Order $order           The order.
1172
	 * @param object   $prepared_source Currently selected source.
1173
	 * @return object                   An updated intent.
1174
	 */
1175
	public function update_existing_intent( $intent, $order, $prepared_source ) {
1176
		$request = array();
1177
1178
		if ( $prepared_source->source !== $intent->source ) {
1179
			$request['source'] = $prepared_source->source;
1180
		}
1181
1182
		$new_amount = WC_Stripe_Helper::get_stripe_amount( $order->get_total() );
1183
		if ( $intent->amount !== $new_amount ) {
1184
			$request['amount'] = $new_amount;
1185
		}
1186
1187
		if ( $prepared_source->customer && $intent->customer !== $prepared_source->customer ) {
1188
			$request['customer'] = $prepared_source->customer;
1189
		}
1190
1191
		if ( $this->has_subscription( $order ) ) {
1192
			// If this is a failed subscription order payment, the intent should be
1193
			// prepared for future usage.
1194
			$request['setup_future_usage'] = 'off_session';
1195
		}
1196
1197
		if ( empty( $request ) ) {
1198
			return $intent;
1199
		}
1200
1201
		$level3_data = $this->get_level3_data_from_order( $order );
1202
		return WC_Stripe_API::request_with_level3_data(
1203
			$request,
1204
			"payment_intents/$intent->id",
1205
			$level3_data,
1206
			$order
1207
		);
1208
	}
1209
1210
	/**
1211
	 * Confirms an intent if it is the `requires_confirmation` state.
1212
	 *
1213
	 * @since 4.2.1
1214
	 * @param object   $intent          The intent to confirm.
1215
	 * @param WC_Order $order           The order that the intent is associated with.
1216
	 * @param object   $prepared_source The source that is being charged.
1217
	 * @return object                   Either an error or the updated intent.
1218
	 */
1219
	public function confirm_intent( $intent, $order, $prepared_source ) {
1220
		if ( 'requires_confirmation' !== $intent->status ) {
1221
			return $intent;
1222
		}
1223
1224
		// Try to confirm the intent & capture the charge (if 3DS is not required).
1225
		$confirm_request = array(
1226
			'source' => $prepared_source->source,
1227
		);
1228
1229
		$level3_data = $this->get_level3_data_from_order( $order );
1230
		$confirmed_intent = WC_Stripe_API::request_with_level3_data(
1231
			$confirm_request,
1232
			"payment_intents/$intent->id/confirm",
1233
			$level3_data,
1234
			$order
1235
		);
1236
1237
		if ( ! empty( $confirmed_intent->error ) ) {
1238
			return $confirmed_intent;
1239
		}
1240
1241
		// Save a note about the status of the intent.
1242
		$order_id = $order->get_id();
1243
		if ( 'succeeded' === $confirmed_intent->status ) {
1244
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
1245
		} elseif ( 'requires_action' === $confirmed_intent->status ) {
1246
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id requires authentication for order $order_id" );
1247
		}
1248
1249
		return $confirmed_intent;
1250
	}
1251
1252
	/**
1253
	 * Saves intent to order.
1254
	 *
1255
	 * @since 3.2.0
1256
	 * @param WC_Order $order For to which the source applies.
1257
	 * @param stdClass $intent Payment intent information.
1258
	 */
1259
	public function save_intent_to_order( $order, $intent ) {
1260
		$order->update_meta_data( '_stripe_intent_id', $intent->id );
1261
1262
		if ( is_callable( array( $order, 'save' ) ) ) {
1263
			$order->save();
1264
		}
1265
	}
1266
1267
	/**
1268
	 * Retrieves the payment intent, associated with an order.
1269
	 *
1270
	 * @since 4.2
1271
	 * @param WC_Order $order The order to retrieve an intent for.
1272
	 * @return obect|bool     Either the intent object or `false`.
1273
	 */
1274
	public function get_intent_from_order( $order ) {
1275
		$intent_id = $order->get_meta( '_stripe_intent_id' );
1276
1277
		if ( $intent_id ) {
1278
			return $this->get_intent( 'payment_intents', $intent_id );
1279
		}
1280
1281
		// The order doesn't have a payment intent, but it may have a setup intent.
1282
		$intent_id = $order->get_meta( '_stripe_setup_intent' );
1283
1284
		if ( $intent_id ) {
1285
			return $this->get_intent( 'setup_intents', $intent_id );
1286
		}
1287
1288
		return false;
1289
	}
1290
1291
	/**
1292
	 * Retrieves intent from Stripe API by intent id.
1293
	 *
1294
	 * @param string $intent_type 	Either 'payment_intents' or 'setup_intents'.
1295
	 * @param string $intent_id		Intent id.
1296
	 * @return object|bool 			Either the intent object or `false`.
1297
	 * @throws Exception 			Throws exception for unknown $intent_type.
1298
	 */
1299
	private function get_intent( $intent_type, $intent_id ) {
1300
		if ( ! in_array( $intent_type, [ 'payment_intents', 'setup_intents' ] ) ) {
1301
			throw new Exception( "Failed to get intent of type $intent_type. Type is not allowed" );
1302
		}
1303
1304
		$response = WC_Stripe_API::request( array(), "$intent_type/$intent_id", 'GET' );
1305
1306
		if ( $response && isset( $response->{ 'error' } ) ) {
1307
			$error_response_message = print_r( $response, true );
1308
			WC_Stripe_Logger::log("Failed to get Stripe intent $intent_type/$intent_id.");
1309
			WC_Stripe_Logger::log("Response: $error_response_message");
1310
			return false;
1311
		}
1312
1313
		return $response;
1314
	}
1315
1316
	/**
1317
	 * Locks an order for payment intent processing for 5 minutes.
1318
	 *
1319
	 * @since 4.2
1320
	 * @param WC_Order $order  The order that is being paid.
1321
	 * @param stdClass $intent The intent that is being processed.
1322
	 * @return bool            A flag that indicates whether the order is already locked.
1323
	 */
1324
	public function lock_order_payment( $order, $intent = null ) {
1325
		$order_id       = $order->get_id();
1326
		$transient_name = 'wc_stripe_processing_intent_' . $order_id;
1327
		$processing     = get_transient( $transient_name );
1328
1329
		// Block the process if the same intent is already being handled.
1330
		if ( "-1" === $processing || ( isset( $intent->id ) && $processing === $intent->id ) ) {
1331
			return true;
1332
		}
1333
1334
		// Save the new intent as a transient, eventually overwriting another one.
1335
		set_transient( $transient_name, empty( $intent ) ? '-1' : $intent->id, 5 * MINUTE_IN_SECONDS );
1336
1337
		return false;
1338
	}
1339
1340
	/**
1341
	 * Unlocks an order for processing by payment intents.
1342
	 *
1343
	 * @since 4.2
1344
	 * @param WC_Order $order The order that is being unlocked.
1345
	 */
1346
	public function unlock_order_payment( $order ) {
1347
		$order_id = $order->get_id();
1348
		delete_transient( 'wc_stripe_processing_intent_' . $order_id );
1349
	}
1350
1351
	/**
1352
	 * Given a response from Stripe, check if it's a card error where authentication is required
1353
	 * to complete the payment.
1354
	 *
1355
	 * @param object $response The response from Stripe.
1356
	 * @return boolean Whether or not it's a 'authentication_required' error
1357
	 */
1358
	public function is_authentication_required_for_payment( $response ) {
1359
		return ( ! empty( $response->error ) && 'authentication_required' === $response->error->code )
1360
			|| ( ! empty( $response->last_payment_error ) && 'authentication_required' === $response->last_payment_error->code );
1361
	}
1362
1363
	/**
1364
	 * Creates a SetupIntent for future payments, and saves it to the order.
1365
	 *
1366
	 * @param WC_Order $order           The ID of the (free/pre- order).
1367
	 * @param object   $prepared_source The source, entered/chosen by the customer.
1368
	 * @return string                   The client secret of the intent, used for confirmation in JS.
1369
	 */
1370
	public function setup_intent( $order, $prepared_source ) {
1371
		$order_id     = $order->get_id();
1372
		$setup_intent = WC_Stripe_API::request( array(
1373
			'payment_method' => $prepared_source->source,
1374
			'customer'       => $prepared_source->customer,
1375
			'confirm'        => 'true',
1376
		), 'setup_intents' );
1377
1378
		if ( is_wp_error( $setup_intent ) ) {
1379
			WC_Stripe_Logger::log( "Unable to create SetupIntent for Order #$order_id: " . print_r( $setup_intent, true ) );
1380
		} elseif ( 'requires_action' === $setup_intent->status ) {
1381
			$order->update_meta_data( '_stripe_setup_intent', $setup_intent->id );
1382
			$order->save();
1383
1384
			return $setup_intent->client_secret;
1385
		}
1386
	}
1387
1388
	/**
1389
	 * Create and confirm a new PaymentIntent.
1390
	 *
1391
	 * @param WC_Order $order           The order that is being paid for.
1392
	 * @param object   $prepared_source The source that is used for the payment.
1393
	 * @param float    $amount          The amount to charge. If not specified, it will be read from the order.
1394
	 * @return object                   An intent or an error.
1395
	 */
1396
	public function create_and_confirm_intent_for_off_session( $order, $prepared_source, $amount = NULL ) {
1397
		// The request for a charge contains metadata for the intent.
1398
		$full_request = $this->generate_payment_request( $order, $prepared_source );
1399
1400
		$request = array(
1401
			'amount'               => $amount ? WC_Stripe_Helper::get_stripe_amount( $amount, $full_request['currency'] ) : $full_request['amount'],
1402
			'currency'             => $full_request['currency'],
1403
			'description'          => $full_request['description'],
1404
			'metadata'             => $full_request['metadata'],
1405
			'payment_method_types' => array(
1406
				'card',
1407
			),
1408
			'off_session'          => 'true',
1409
			'confirm'              => 'true',
1410
			'confirmation_method'  => 'automatic',
1411
		);
1412
1413
		if ( isset( $full_request['statement_descriptor'] ) ) {
1414
			$request['statement_descriptor'] = $full_request['statement_descriptor'];
1415
		}
1416
1417
		if ( isset( $full_request['customer'] ) ) {
1418
			$request['customer'] = $full_request['customer'];
1419
		}
1420
1421
		if ( isset( $full_request['source'] ) ) {
1422
			$is_source = 'src_' === substr( $full_request['source'], 0, 4 );
1423
			$request[ $is_source ? 'source' : 'payment_method' ] = $full_request['source'];
1424
		}
1425
1426
		/**
1427
		 * Filter the value of the request.
1428
		 *
1429
		 * @since 4.5.0
1430
		 * @param array $request
1431
		 * @param WC_Order $order
1432
		 * @param object $source
1433
		 */
1434
		$request = apply_filters('wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1435
1436
		if ( isset( $full_request['shipping'] ) ) {
1437
			$request['shipping'] = $full_request['shipping'];
1438
		}
1439
1440
		$level3_data = $this->get_level3_data_from_order( $order );
1441
		$intent = WC_Stripe_API::request_with_level3_data(
1442
			$request,
1443
			'payment_intents',
1444
			$level3_data,
1445
			$order
1446
		);
1447
		$is_authentication_required = $this->is_authentication_required_for_payment( $intent );
1448
1449
		if ( ! empty( $intent->error ) && ! $is_authentication_required ) {
1450
			return $intent;
1451
		}
1452
1453
		$intent_id      = ( ! empty( $intent->error )
1454
			? $intent->error->payment_intent->id
1455
			: $intent->id
1456
		);
1457
		$payment_intent = ( ! empty( $intent->error )
1458
			? $intent->error->payment_intent
1459
			: $intent
1460
		);
1461
		$order_id       = $order->get_id();
1462
		WC_Stripe_Logger::log( "Stripe PaymentIntent $intent_id initiated for order $order_id" );
1463
1464
		// Save the intent ID to the order.
1465
		$this->save_intent_to_order( $order, $payment_intent );
1466
1467
		return $intent;
1468
	}
1469
1470
	/**
1471
	 * Checks if subscription has a Stripe customer ID and adds it if doesn't.
1472
	 *
1473
	 * Fix renewal for existing subscriptions affected by https://github.com/woocommerce/woocommerce-gateway-stripe/issues/1072.
1474
	 * @param int $order_id subscription renewal order id.
1475
	 */
1476
	public function ensure_subscription_has_customer_id( $order_id ) {
1477
		$subscriptions_ids = wcs_get_subscriptions_for_order( $order_id, array( 'order_type' => 'any' ) );
1478
		foreach( $subscriptions_ids as $subscription_id => $subscription ) {
1479
			if ( ! metadata_exists( 'post', $subscription_id, '_stripe_customer_id' ) ) {
1480
				$stripe_customer = new WC_Stripe_Customer( $subscription->get_user_id() );
1481
				update_post_meta( $subscription_id, '_stripe_customer_id', $stripe_customer->get_id() );
1482
				update_post_meta( $order_id, '_stripe_customer_id', $stripe_customer->get_id() );
1483
			}
1484
		}
1485
	}
1486
1487
	/** Verifies whether a certain ZIP code is valid for the US, incl. 4-digit extensions.
1488
	 *
1489
	 * @param string $zip The ZIP code to verify.
1490
	 * @return boolean
1491
	 */
1492
	public function is_valid_us_zip_code( $zip ) {
1493
		return ! empty( $zip ) && preg_match( '/^\d{5,5}(-\d{4,4})?$/', $zip );
1494
	}
1495
}
1496