Completed
Push — master ( 216d34...8ed8a3 )
by Radoslav
01:58 queued 22s
created

WC_Stripe_Payment_Gateway::setup_intent()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 8
nop 2
dl 0
loc 21
rs 9.2728
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
	 * Check if we need to make gateways available.
156
	 *
157
	 * @since 4.1.3
158
	 */
159
	public function is_available() {
160
		if ( 'yes' === $this->enabled ) {
161
			if ( ! $this->secret_key || ! $this->publishable_key ) {
162
				return false;
163
			}
164
			return true;
165
		}
166
167
		return parent::is_available();
168
	}
169
170
	/**
171
	 * Checks if we need to process pre orders when
172
	 * pre orders is in the cart.
173
	 *
174
	 * @since 4.1.0
175
	 * @param int $order_id
176
	 * @return bool
177
	 */
178
	public function maybe_process_pre_orders( $order_id ) {
179
		return (
180
			WC_Stripe_Helper::is_pre_orders_exists() &&
181
			$this->pre_orders->is_pre_order( $order_id ) &&
182
			WC_Pre_Orders_Order::order_requires_payment_tokenization( $order_id ) &&
183
			! is_wc_endpoint_url( 'order-pay' )
184
		);
185
	}
186
187
	/**
188
	 * All payment icons that work with Stripe. Some icons references
189
	 * WC core icons.
190
	 *
191
	 * @since 4.0.0
192
	 * @since 4.1.0 Changed to using img with svg (colored) instead of fonts.
193
	 * @return array
194
	 */
195
	public function payment_icons() {
196
		return apply_filters(
197
			'wc_stripe_payment_icons',
198
			array(
199
				'visa'       => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/visa.svg" class="stripe-visa-icon stripe-icon" alt="Visa" />',
200
				'amex'       => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/amex.svg" class="stripe-amex-icon stripe-icon" alt="American Express" />',
201
				'mastercard' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/mastercard.svg" class="stripe-mastercard-icon stripe-icon" alt="Mastercard" />',
202
				'discover'   => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/discover.svg" class="stripe-discover-icon stripe-icon" alt="Discover" />',
203
				'diners'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/diners.svg" class="stripe-diners-icon stripe-icon" alt="Diners" />',
204
				'jcb'        => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/jcb.svg" class="stripe-jcb-icon stripe-icon" alt="JCB" />',
205
				'alipay'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/alipay.svg" class="stripe-alipay-icon stripe-icon" alt="Alipay" />',
206
				'wechat'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/wechat.svg" class="stripe-wechat-icon stripe-icon" alt="Wechat Pay" />',
207
				'bancontact' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/bancontact.svg" class="stripe-bancontact-icon stripe-icon" alt="Bancontact" />',
208
				'ideal'      => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/ideal.svg" class="stripe-ideal-icon stripe-icon" alt="iDeal" />',
209
				'p24'        => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/p24.svg" class="stripe-p24-icon stripe-icon" alt="P24" />',
210
				'giropay'    => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/giropay.svg" class="stripe-giropay-icon stripe-icon" alt="Giropay" />',
211
				'eps'        => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/eps.svg" class="stripe-eps-icon stripe-icon" alt="EPS" />',
212
				'multibanco' => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/multibanco.svg" class="stripe-multibanco-icon stripe-icon" alt="Multibanco" />',
213
				'sofort'     => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/sofort.svg" class="stripe-sofort-icon stripe-icon" alt="SOFORT" />',
214
				'sepa'       => '<img src="' . WC_STRIPE_PLUGIN_URL . '/assets/images/sepa.svg" class="stripe-sepa-icon stripe-icon" alt="SEPA" />',
215
			)
216
		);
217
	}
218
219
	/**
220
	 * Validates that the order meets the minimum order amount
221
	 * set by Stripe.
222
	 *
223
	 * @since 4.0.0
224
	 * @version 4.0.0
225
	 * @param object $order
226
	 */
227
	public function validate_minimum_order_amount( $order ) {
228 View Code Duplication
		if ( $order->get_total() * 100 < WC_Stripe_Helper::get_minimum_amount() ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
229
			/* translators: 1) dollar amount */
230
			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 ) ) );
231
		}
232
	}
233
234
	/**
235
	 * Gets the transaction URL linked to Stripe dashboard.
236
	 *
237
	 * @since 4.0.0
238
	 * @version 4.0.0
239
	 */
240
	public function get_transaction_url( $order ) {
241
		if ( $this->testmode ) {
242
			$this->view_transaction_url = 'https://dashboard.stripe.com/test/payments/%s';
243
		} else {
244
			$this->view_transaction_url = 'https://dashboard.stripe.com/payments/%s';
245
		}
246
247
		return parent::get_transaction_url( $order );
248
	}
249
250
	/**
251
	 * Gets the saved customer id if exists.
252
	 *
253
	 * @since 4.0.0
254
	 * @version 4.0.0
255
	 */
256
	public function get_stripe_customer_id( $order ) {
257
		$customer = get_user_meta( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->customer_user : $order->get_customer_id(), '_stripe_customer_id', true );
258
259
		if ( empty( $customer ) ) {
260
			// Try to get it via the order.
261
			if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
262
				return get_post_meta( $order->id, '_stripe_customer_id', true );
263
			} else {
264
				return $order->get_meta( '_stripe_customer_id', true );
265
			}
266
		} else {
267
			return $customer;
268
		}
269
270
		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...
271
	}
272
273
	/**
274
	 * Builds the return URL from redirects.
275
	 *
276
	 * @since 4.0.0
277
	 * @version 4.0.0
278
	 * @param object $order
279
	 * @param int $id Stripe session id.
280
	 */
281
	public function get_stripe_return_url( $order = null, $id = null ) {
282
		if ( is_object( $order ) ) {
283
			if ( empty( $id ) ) {
284
				$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...
285
			}
286
287
			$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
288
289
			$args = array(
290
				'utm_nooverride' => '1',
291
				'order_id'       => $order_id,
292
			);
293
294
			return esc_url_raw( add_query_arg( $args, $this->get_return_url( $order ) ) );
295
		}
296
297
		return esc_url_raw( add_query_arg( array( 'utm_nooverride' => '1' ), $this->get_return_url() ) );
298
	}
299
300
	/**
301
	 * Is $order_id a subscription?
302
	 * @param  int  $order_id
303
	 * @return boolean
304
	 */
305
	public function has_subscription( $order_id ) {
306
		return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) );
307
	}
308
309
	/**
310
	 * Generate the request for the payment.
311
	 *
312
	 * @since 3.1.0
313
	 * @version 4.0.0
314
	 * @param  WC_Order $order
315
	 * @param  object $prepared_source
316
	 * @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...
317
	 */
318
	public function generate_payment_request( $order, $prepared_source ) {
319
		$settings              = get_option( 'woocommerce_stripe_settings', array() );
320
		$statement_descriptor  = ! empty( $settings['statement_descriptor'] ) ? str_replace( "'", '', $settings['statement_descriptor'] ) : '';
321
		$capture               = ! empty( $settings['capture'] ) && 'yes' === $settings['capture'] ? true : false;
322
		$post_data             = array();
323
		$post_data['currency'] = strtolower( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->get_order_currency() : $order->get_currency() );
324
		$post_data['amount']   = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $post_data['currency'] );
325
		/* translators: 1) blog name 2) order number */
326
		$post_data['description'] = sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() );
327
		$billing_email            = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_email : $order->get_billing_email();
328
		$billing_first_name       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_first_name : $order->get_billing_first_name();
329
		$billing_last_name        = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_last_name : $order->get_billing_last_name();
330
331
		if ( ! empty( $billing_email ) && apply_filters( 'wc_stripe_send_stripe_receipt', false ) ) {
332
			$post_data['receipt_email'] = $billing_email;
333
		}
334
335
		switch ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->payment_method : $order->get_payment_method() ) {
336
			case 'stripe':
337
				if ( ! empty( $statement_descriptor ) ) {
338
					$post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
339
				}
340
341
				$post_data['capture'] = $capture ? 'true' : 'false';
342
				break;
343
			case 'stripe_sepa':
344
				if ( ! empty( $statement_descriptor ) ) {
345
					$post_data['statement_descriptor'] = WC_Stripe_Helper::clean_statement_descriptor( $statement_descriptor );
346
				}
347
				break;
348
		}
349
350
		$post_data['expand[]'] = 'balance_transaction';
351
352
		$metadata = array(
353
			__( 'customer_name', 'woocommerce-gateway-stripe' ) => sanitize_text_field( $billing_first_name ) . ' ' . sanitize_text_field( $billing_last_name ),
354
			__( 'customer_email', 'woocommerce-gateway-stripe' ) => sanitize_email( $billing_email ),
355
			'order_id' => $order->get_order_number(),
356
		);
357
358
		if ( $this->has_subscription( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id() ) ) {
359
			$metadata += array(
360
				'payment_type' => 'recurring',
361
				'site_url'     => esc_url( get_site_url() ),
362
			);
363
		}
364
365
		$post_data['metadata'] = apply_filters( 'wc_stripe_payment_metadata', $metadata, $order, $prepared_source );
366
367
		if ( $prepared_source->customer ) {
368
			$post_data['customer'] = $prepared_source->customer;
369
		}
370
371
		if ( $prepared_source->source ) {
372
			$post_data['source'] = $prepared_source->source;
373
		}
374
375
		/**
376
		 * Filter the return value of the WC_Payment_Gateway_CC::generate_payment_request.
377
		 *
378
		 * @since 3.1.0
379
		 * @param array $post_data
380
		 * @param WC_Order $order
381
		 * @param object $source
382
		 */
383
		return apply_filters( 'wc_stripe_generate_payment_request', $post_data, $order, $prepared_source );
384
	}
385
386
	/**
387
	 * Store extra meta data for an order from a Stripe Response.
388
	 */
389
	public function process_response( $response, $order ) {
390
		WC_Stripe_Logger::log( 'Processing response: ' . print_r( $response, true ) );
391
392
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
393
		$captured = ( isset( $response->captured ) && $response->captured ) ? 'yes' : 'no';
394
395
		// Store charge data.
396
		WC_Stripe_Helper::is_wc_lt( '3.0' ) ? update_post_meta( $order_id, '_stripe_charge_captured', $captured ) : $order->update_meta_data( '_stripe_charge_captured', $captured );
397
398
		if ( isset( $response->balance_transaction ) ) {
399
			$this->update_fees( $order, is_string( $response->balance_transaction ) ? $response->balance_transaction : $response->balance_transaction->id );
400
		}
401
402
		if ( 'yes' === $captured ) {
403
			/**
404
			 * Charge can be captured but in a pending state. Payment methods
405
			 * that are asynchronous may take couple days to clear. Webhook will
406
			 * take care of the status changes.
407
			 */
408
			if ( 'pending' === $response->status ) {
409
				$order_stock_reduced = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? get_post_meta( $order_id, '_order_stock_reduced', true ) : $order->get_meta( '_order_stock_reduced', true );
410
411
				if ( ! $order_stock_reduced ) {
412
					WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->reduce_order_stock() : wc_reduce_stock_levels( $order_id );
413
				}
414
415
				WC_Stripe_Helper::is_wc_lt( '3.0' ) ? update_post_meta( $order_id, '_transaction_id', $response->id ) : $order->set_transaction_id( $response->id );
416
				/* translators: transaction id */
417
				$order->update_status( 'on-hold', sprintf( __( 'Stripe charge awaiting payment: %s.', 'woocommerce-gateway-stripe' ), $response->id ) );
418
			}
419
420
			if ( 'succeeded' === $response->status ) {
421
				$order->payment_complete( $response->id );
422
423
				/* translators: transaction id */
424
				$message = sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $response->id );
425
				$order->add_order_note( $message );
426
			}
427
428
			if ( 'failed' === $response->status ) {
429
				$localized_message = __( 'Payment processing failed. Please retry.', 'woocommerce-gateway-stripe' );
430
				$order->add_order_note( $localized_message );
431
				throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
432
			}
433
		} else {
434
			WC_Stripe_Helper::is_wc_lt( '3.0' ) ? update_post_meta( $order_id, '_transaction_id', $response->id ) : $order->set_transaction_id( $response->id );
435
436
			if ( $order->has_status( array( 'pending', 'failed' ) ) ) {
437
				WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->reduce_order_stock() : wc_reduce_stock_levels( $order_id );
438
			}
439
440
			/* translators: transaction id */
441
			$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 ) );
442
		}
443
444
		if ( is_callable( array( $order, 'save' ) ) ) {
445
			$order->save();
446
		}
447
448
		do_action( 'wc_gateway_stripe_process_response', $response, $order );
449
450
		return $response;
451
	}
452
453
	/**
454
	 * Sends the failed order email to admin.
455
	 *
456
	 * @since 3.1.0
457
	 * @version 4.0.0
458
	 * @param int $order_id
459
	 * @return null
460
	 */
461
	public function send_failed_order_email( $order_id ) {
462
		$emails = WC()->mailer()->get_emails();
463
		if ( ! empty( $emails ) && ! empty( $order_id ) ) {
464
			$emails['WC_Email_Failed_Order']->trigger( $order_id );
465
		}
466
	}
467
468
	/**
469
	 * Get owner details.
470
	 *
471
	 * @since 4.0.0
472
	 * @version 4.0.0
473
	 * @param object $order
474
	 * @return object $details
475
	 */
476
	public function get_owner_details( $order ) {
477
		$billing_first_name = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_first_name : $order->get_billing_first_name();
478
		$billing_last_name  = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_last_name : $order->get_billing_last_name();
479
480
		$details = array();
481
482
		$name  = $billing_first_name . ' ' . $billing_last_name;
483
		$email = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_email : $order->get_billing_email();
484
		$phone = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_phone : $order->get_billing_phone();
485
486
		if ( ! empty( $phone ) ) {
487
			$details['phone'] = $phone;
488
		}
489
490
		if ( ! empty( $name ) ) {
491
			$details['name'] = $name;
492
		}
493
494
		if ( ! empty( $email ) ) {
495
			$details['email'] = $email;
496
		}
497
498
		$details['address']['line1']       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_address_1 : $order->get_billing_address_1();
499
		$details['address']['line2']       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_address_2 : $order->get_billing_address_2();
500
		$details['address']['state']       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_state : $order->get_billing_state();
501
		$details['address']['city']        = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_city : $order->get_billing_city();
502
		$details['address']['postal_code'] = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_postcode : $order->get_billing_postcode();
503
		$details['address']['country']     = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->billing_country : $order->get_billing_country();
504
505
		return (object) apply_filters( 'wc_stripe_owner_details', $details, $order );
506
	}
507
508
	/**
509
	 * Get source object by source id.
510
	 *
511
	 * @since 4.0.3
512
	 * @param string $source_id The source ID to get source object for.
513
	 */
514
	public function get_source_object( $source_id = '' ) {
515
		if ( empty( $source_id ) ) {
516
			return '';
517
		}
518
519
		$source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
520
521
		if ( ! empty( $source_object->error ) ) {
522
			throw new WC_Stripe_Exception( print_r( $source_object, true ), $source_object->error->message );
523
		}
524
525
		return $source_object;
526
	}
527
528
	/**
529
	 * Checks if card is a prepaid card.
530
	 *
531
	 * @since 4.0.6
532
	 * @param object $source_object
533
	 * @return bool
534
	 */
535
	public function is_prepaid_card( $source_object ) {
536
		return (
537
			$source_object
538
			&& ( 'token' === $source_object->object || 'source' === $source_object->object )
539
			&& 'prepaid' === $source_object->card->funding
540
		);
541
	}
542
543
	/**
544
	 * Checks if source is of legacy type card.
545
	 *
546
	 * @since 4.0.8
547
	 * @param string $source_id
548
	 * @return bool
549
	 */
550
	public function is_type_legacy_card( $source_id ) {
551
		return ( preg_match( '/^card_/', $source_id ) );
552
	}
553
554
	/**
555
	 * Checks if payment is via saved payment source.
556
	 *
557
	 * @since 4.1.0
558
	 * @return bool
559
	 */
560
	public function is_using_saved_payment_method() {
561
		$payment_method = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
562
563
		return ( isset( $_POST[ 'wc-' . $payment_method . '-payment-token' ] ) && 'new' !== $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
564
	}
565
566
	/**
567
	 * Get payment source. This can be a new token/source or existing WC token.
568
	 * If user is logged in and/or has WC account, create an account on Stripe.
569
	 * This way we can attribute the payment to the user to better fight fraud.
570
	 *
571
	 * @since 3.1.0
572
	 * @version 4.0.0
573
	 * @param string $user_id
574
	 * @param bool $force_save_source Should we force save payment source.
575
	 *
576
	 * @throws Exception When card was not added or for and invalid card.
577
	 * @return object
578
	 */
579
	public function prepare_source( $user_id, $force_save_source = false ) {
580
		$customer          = new WC_Stripe_Customer( $user_id );
581
		$force_save_source = apply_filters( 'wc_stripe_force_save_source', $force_save_source, $customer );
582
		$source_object     = '';
583
		$source_id         = '';
584
		$wc_token_id       = false;
585
		$payment_method    = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
586
		$is_token          = false;
587
588
		// New CC info was entered and we have a new source to process.
589
		if ( ! empty( $_POST['stripe_source'] ) ) {
590
			$source_object = self::get_source_object( wc_clean( $_POST['stripe_source'] ) );
591
			$source_id     = $source_object->id;
592
593
			// This checks to see if customer opted to save the payment method to file.
594
			$maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
595
596
			/**
597
			 * This is true if the user wants to store the card to their account.
598
			 * Criteria to save to file is they are logged in, they opted to save or product requirements and the source is
599
			 * actually reusable. Either that or force_save_source is true.
600
			 */
601
			if ( ( $user_id && $this->saved_cards && $maybe_saved_card && 'reusable' === $source_object->usage ) || $force_save_source ) {
602
				$response = $customer->add_source( $source_object->id );
603
604
				if ( ! empty( $response->error ) ) {
605
					throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
606
				}
607
			}
608
		} elseif ( $this->is_using_saved_payment_method() ) {
609
			// Use an existing token, and then process the payment.
610
			$wc_token_id = wc_clean( $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
611
			$wc_token    = WC_Payment_Tokens::get( $wc_token_id );
612
613
			if ( ! $wc_token || $wc_token->get_user_id() !== get_current_user_id() ) {
614
				WC()->session->set( 'refresh_totals', true );
615
				throw new WC_Stripe_Exception( 'Invalid payment method', __( 'Invalid payment method. Please input a new card number.', 'woocommerce-gateway-stripe' ) );
616
			}
617
618
			$source_id = $wc_token->get_token();
619
620
			if ( $this->is_type_legacy_card( $source_id ) ) {
621
				$is_token = true;
622
			}
623
		} elseif ( isset( $_POST['stripe_token'] ) && 'new' !== $_POST['stripe_token'] ) {
624
			$stripe_token     = wc_clean( $_POST['stripe_token'] );
625
			$maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
626
627
			// This is true if the user wants to store the card to their account.
628
			if ( ( $user_id && $this->saved_cards && $maybe_saved_card ) || $force_save_source ) {
629
				$response = $customer->add_source( $stripe_token );
630
631
				if ( ! empty( $response->error ) ) {
632
					throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
633
				}
634
			} else {
635
				$source_id    = $stripe_token;
636
				$is_token     = true;
637
			}
638
		}
639
640
		$customer_id = $customer->get_id();
641
		if ( ! $customer_id ) {
642
			$customer->set_id( $customer->create_customer() );
643
			$customer_id = $customer->get_id();
644
		}
645
646
		if ( empty( $source_object ) && ! $is_token ) {
647
			$source_object = self::get_source_object( $source_id );
648
		}
649
650
		return (object) array(
651
			'token_id'      => $wc_token_id,
652
			'customer'      => $customer_id,
653
			'source'        => $source_id,
654
			'source_object' => $source_object,
655
		);
656
	}
657
658
	/**
659
	 * Get payment source from an order. This could be used in the future for
660
	 * a subscription as an example, therefore using the current user ID would
661
	 * not work - the customer won't be logged in :)
662
	 *
663
	 * Not using 2.6 tokens for this part since we need a customer AND a card
664
	 * token, and not just one.
665
	 *
666
	 * @since 3.1.0
667
	 * @version 4.0.0
668
	 * @param object $order
669
	 * @return object
670
	 */
671
	public function prepare_order_source( $order = null ) {
672
		$stripe_customer = new WC_Stripe_Customer();
673
		$stripe_source   = false;
674
		$token_id        = false;
675
		$source_object   = false;
676
677
		if ( $order ) {
678
			$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
679
680
			$stripe_customer_id = get_post_meta( $order_id, '_stripe_customer_id', true );
681
682
			if ( $stripe_customer_id ) {
683
				$stripe_customer->set_id( $stripe_customer_id );
684
			}
685
686
			$source_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? get_post_meta( $order_id, '_stripe_source_id', true ) : $order->get_meta( '_stripe_source_id', true );
687
688
			// Since 4.0.0, we changed card to source so we need to account for that.
689
			if ( empty( $source_id ) ) {
690
				$source_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? get_post_meta( $order_id, '_stripe_card_id', true ) : $order->get_meta( '_stripe_card_id', true );
691
692
				// Take this opportunity to update the key name.
693
				WC_Stripe_Helper::is_wc_lt( '3.0' ) ? update_post_meta( $order_id, '_stripe_source_id', $source_id ) : $order->update_meta_data( '_stripe_source_id', $source_id );
694
695
				if ( is_callable( array( $order, 'save' ) ) ) {
696
					$order->save();
697
				}
698
			}
699
700
			if ( $source_id ) {
701
				$stripe_source = $source_id;
702
				$source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
703
			} elseif ( apply_filters( 'wc_stripe_use_default_customer_source', true ) ) {
704
				/*
705
				 * We can attempt to charge the customer's default source
706
				 * by sending empty source id.
707
				 */
708
				$stripe_source = '';
709
			}
710
		}
711
712
		return (object) array(
713
			'token_id'      => $token_id,
714
			'customer'      => $stripe_customer ? $stripe_customer->get_id() : false,
715
			'source'        => $stripe_source,
716
			'source_object' => $source_object,
717
		);
718
	}
719
720
	/**
721
	 * Save source to order.
722
	 *
723
	 * @since 3.1.0
724
	 * @version 4.0.0
725
	 * @param WC_Order $order For to which the source applies.
726
	 * @param stdClass $source Source information.
727
	 */
728
	public function save_source_to_order( $order, $source ) {
729
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
730
731
		// Store source in the order.
732
		if ( $source->customer ) {
733
			if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
734
				update_post_meta( $order_id, '_stripe_customer_id', $source->customer );
735
			} else {
736
				$order->update_meta_data( '_stripe_customer_id', $source->customer );
737
			}
738
		}
739
740
		if ( $source->source ) {
741
			if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
742
				update_post_meta( $order_id, '_stripe_source_id', $source->source );
743
			} else {
744
				$order->update_meta_data( '_stripe_source_id', $source->source );
745
			}
746
		}
747
748
		if ( is_callable( array( $order, 'save' ) ) ) {
749
			$order->save();
750
		}
751
	}
752
753
	/**
754
	 * Updates Stripe fees/net.
755
	 * e.g usage would be after a refund.
756
	 *
757
	 * @since 4.0.0
758
	 * @version 4.0.6
759
	 * @param object $order The order object
760
	 * @param int $balance_transaction_id
761
	 */
762
	public function update_fees( $order, $balance_transaction_id ) {
763
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
764
765
		$balance_transaction = WC_Stripe_API::retrieve( 'balance/history/' . $balance_transaction_id );
766
767
		if ( empty( $balance_transaction->error ) ) {
768
			if ( isset( $balance_transaction ) && isset( $balance_transaction->fee ) ) {
769
				// Fees and Net needs to both come from Stripe to be accurate as the returned
770
				// values are in the local currency of the Stripe account, not from WC.
771
				$fee_refund = ! empty( $balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'fee' ) : 0;
772
				$net_refund = ! empty( $balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'net' ) : 0;
773
774
				// Current data fee & net.
775
				$fee_current = WC_Stripe_Helper::get_stripe_fee( $order );
776
				$net_current = WC_Stripe_Helper::get_stripe_net( $order );
777
778
				// Calculation.
779
				$fee = (float) $fee_current + (float) $fee_refund;
780
				$net = (float) $net_current + (float) $net_refund;
781
782
				WC_Stripe_Helper::update_stripe_fee( $order, $fee );
783
				WC_Stripe_Helper::update_stripe_net( $order, $net );
784
785
				$currency = ! empty( $balance_transaction->currency ) ? strtoupper( $balance_transaction->currency ) : null;
786
				WC_Stripe_Helper::update_stripe_currency( $order, $currency );
787
788
				if ( is_callable( array( $order, 'save' ) ) ) {
789
					$order->save();
790
				}
791
			}
792
		} else {
793
			WC_Stripe_Logger::log( "Unable to update fees/net meta for order: {$order_id}" );
794
		}
795
	}
796
797
	/**
798
	 * Refund a charge.
799
	 *
800
	 * @since 3.1.0
801
	 * @version 4.0.0
802
	 * @param  int $order_id
803
	 * @param  float $amount
804
	 * @return bool
805
	 */
806
	public function process_refund( $order_id, $amount = null, $reason = '' ) {
807
		$order = wc_get_order( $order_id );
808
809
		if ( ! $order ) {
810
			return false;
811
		}
812
813
		$request = array();
814
815
		if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
816
			$order_currency = get_post_meta( $order_id, '_order_currency', true );
817
			$captured       = get_post_meta( $order_id, '_stripe_charge_captured', true );
818
			$charge_id      = get_post_meta( $order_id, '_transaction_id', true );
819
		} else {
820
			$order_currency = $order->get_currency();
821
			$captured       = $order->get_meta( '_stripe_charge_captured', true );
822
			$charge_id      = $order->get_transaction_id();
823
		}
824
825
		if ( ! $charge_id ) {
826
			return false;
827
		}
828
829
		if ( ! is_null( $amount ) ) {
830
			$request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $order_currency );
831
		}
832
833
		// If order is only authorized, don't pass amount.
834
		if ( 'yes' !== $captured ) {
835
			unset( $request['amount'] );
836
		}
837
838
		if ( $reason ) {
839
			$request['metadata'] = array(
840
				'reason' => $reason,
841
			);
842
		}
843
844
		$request['charge'] = $charge_id;
845
		WC_Stripe_Logger::log( "Info: Beginning refund for order {$charge_id} for the amount of {$amount}" );
846
847
		$request = apply_filters( 'wc_stripe_refund_request', $request, $order );
848
849
		$intent = $this->get_intent_from_order( $order );
850
		$intent_cancelled = false;
851
		if ( $intent ) {
852
			// If the order has a Payment Intent pending capture, then the Intent itself must be refunded (cancelled), not the Charge
853
			if ( ! empty( $intent->error ) ) {
854
				$response = $intent;
855
				$intent_cancelled = true;
856
			} elseif ( 'requires_capture' === $intent->status ) {
857
				$result = WC_Stripe_API::request(
858
					array(),
859
					'payment_intents/' . $intent->id . '/cancel'
860
				);
861
				$intent_cancelled = true;
862
863
				if ( ! empty( $result->error ) ) {
864
					$response = $result;
865
				} else {
866
					$charge = end( $result->charges->data );
867
					$response = end( $charge->refunds->data );
868
				}
869
			}
870
		}
871
872
		if ( ! $intent_cancelled ) {
873
			$response = WC_Stripe_API::request( $request, 'refunds' );
874
		}
875
876
		if ( ! empty( $response->error ) ) {
877
			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...
878
879
			return $response;
880
881
		} elseif ( ! empty( $response->id ) ) {
882
			WC_Stripe_Helper::is_wc_lt( '3.0' ) ? update_post_meta( $order_id, '_stripe_refund_id', $response->id ) : $order->update_meta_data( '_stripe_refund_id', $response->id );
883
884
			$amount = wc_price( $response->amount / 100 );
885
886
			if ( in_array( strtolower( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->get_order_currency() : $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
887
				$amount = wc_price( $response->amount );
888
			}
889
890
			if ( isset( $response->balance_transaction ) ) {
891
				$this->update_fees( $order, $response->balance_transaction );
892
			}
893
894
			/* translators: 1) dollar amount 2) transaction id 3) refund message */
895
			$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' );
896
897
			$order->add_order_note( $refund_message );
898
			WC_Stripe_Logger::log( 'Success: ' . html_entity_decode( wp_strip_all_tags( $refund_message ) ) );
899
900
			return true;
901
		}
902
	}
903
904
	/**
905
	 * Add payment method via account screen.
906
	 * We don't store the token locally, but to the Stripe API.
907
	 *
908
	 * @since 3.0.0
909
	 * @version 4.0.0
910
	 */
911
	public function add_payment_method() {
912
		$error     = false;
913
		$error_msg = __( 'There was a problem adding the payment method.', 'woocommerce-gateway-stripe' );
914
		$source_id = '';
915
916
		if ( empty( $_POST['stripe_source'] ) && empty( $_POST['stripe_token'] ) || ! is_user_logged_in() ) {
917
			$error = true;
918
		}
919
920
		$stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
921
922
		$source = ! empty( $_POST['stripe_source'] ) ? wc_clean( $_POST['stripe_source'] ) : '';
923
924
		$source_object = WC_Stripe_API::retrieve( 'sources/' . $source );
925
926
		if ( isset( $source_object ) ) {
927
			if ( ! empty( $source_object->error ) ) {
928
				$error = true;
929
			}
930
931
			$source_id = $source_object->id;
932
		} elseif ( isset( $_POST['stripe_token'] ) ) {
933
			$source_id = wc_clean( $_POST['stripe_token'] );
934
		}
935
936
		$response = $stripe_customer->add_source( $source_id );
937
938
		if ( ! $response || is_wp_error( $response ) || ! empty( $response->error ) ) {
939
			$error = true;
940
		}
941
942
		if ( $error ) {
943
			wc_add_notice( $error_msg, 'error' );
944
			WC_Stripe_Logger::log( 'Add payment method Error: ' . $error_msg );
945
			return;
946
		}
947
948
		do_action( 'wc_stripe_add_payment_method_' . $_POST['payment_method'] . '_success', $source_id, $source_object );
949
950
		return array(
951
			'result'   => 'success',
952
			'redirect' => wc_get_endpoint_url( 'payment-methods' ),
953
		);
954
	}
955
956
	/**
957
	 * Gets the locale with normalization that only Stripe accepts.
958
	 *
959
	 * @since 4.0.6
960
	 * @return string $locale
961
	 */
962
	public function get_locale() {
963
		$locale = get_locale();
964
965
		/*
966
		 * Stripe expects Norwegian to only be passed NO.
967
		 * But WP has different dialects.
968
		 */
969
		if ( 'NO' === substr( $locale, 3, 2 ) ) {
970
			$locale = 'no';
971
		} else {
972
			$locale = substr( get_locale(), 0, 2 );
973
		}
974
975
		return $locale;
976
	}
977
978
	/**
979
	 * Change the idempotency key so charge can
980
	 * process order as a different transaction.
981
	 *
982
	 * @since 4.0.6
983
	 * @param string $idempotency_key
984
	 * @param array $request
985
	 */
986
	public function change_idempotency_key( $idempotency_key, $request ) {
987
		$customer = ! empty( $request['customer'] ) ? $request['customer'] : '';
988
		$source   = ! empty( $request['source'] ) ? $request['source'] : $customer;
989
		$count    = $this->retry_interval;
990
991
		return $request['metadata']['order_id'] . '-' . $count . '-' . $source;
992
	}
993
994
	/**
995
	 * Checks if request is the original to prevent double processing
996
	 * on WC side. The original-request header and request-id header
997
	 * needs to be the same to mean its the original request.
998
	 *
999
	 * @since 4.0.6
1000
	 * @param array $headers
1001
	 */
1002
	public function is_original_request( $headers ) {
1003
		if ( $headers['original-request'] === $headers['request-id'] ) {
1004
			return true;
1005
		}
1006
1007
		return false;
1008
	}
1009
1010
	/**
1011
	 * Generates the request when creating a new payment intent.
1012
	 *
1013
	 * @param WC_Order $order           The order that is being paid for.
1014
	 * @param object   $prepared_source The source that is used for the payment.
1015
	 * @return array                    The arguments for the request.
1016
	 */
1017
	public function generate_create_intent_request( $order, $prepared_source ) {
1018
		// The request for a charge contains metadata for the intent.
1019
		$full_request = $this->generate_payment_request( $order, $prepared_source );
1020
1021
		$request = array(
1022
			'source'               => $prepared_source->source,
1023
			'amount'               => WC_Stripe_Helper::get_stripe_amount( $order->get_total() ),
1024
			'currency'             => strtolower( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->get_order_currency() : $order->get_currency() ),
1025
			'description'          => $full_request['description'],
1026
			'metadata'             => $full_request['metadata'],
1027
			'capture_method'       => ( 'true' === $full_request['capture'] ) ? 'automatic' : 'manual',
1028
			'payment_method_types' => array(
1029
				'card',
1030
			),
1031
		);
1032
1033
		if ( $prepared_source->customer ) {
1034
			$request['customer'] = $prepared_source->customer;
1035
		}
1036
1037
		if ( isset( $full_request['statement_descriptor'] ) ) {
1038
			$request['statement_descriptor'] = $full_request['statement_descriptor'];
1039
		}
1040
1041
		/**
1042
		 * Filter the return value of the WC_Payment_Gateway_CC::generate_create_intent_request.
1043
		 *
1044
		 * @since 3.1.0
1045
		 * @param array $request
1046
		 * @param WC_Order $order
1047
		 * @param object $source
1048
		 */
1049
		return apply_filters( 'wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1050
	}
1051
1052
	/**
1053
	 * Create a new PaymentIntent.
1054
	 *
1055
	 * @param WC_Order $order           The order that is being paid for.
1056
	 * @param object   $prepared_source The source that is used for the payment.
1057
	 * @return object                   An intent or an error.
1058
	 */
1059
	public function create_intent( $order, $prepared_source ) {
1060
		$request = $this->generate_create_intent_request( $order, $prepared_source );
1061
1062
		// Create an intent that awaits an action.
1063
		$intent = WC_Stripe_API::request( $request, 'payment_intents' );
1064
		if ( ! empty( $intent->error ) ) {
1065
			return $intent;
1066
		}
1067
1068
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1069
		WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id initiated for order $order_id" );
1070
1071
		// Save the intent ID to the order.
1072
		$this->save_intent_to_order( $order, $intent );
1073
1074
		return $intent;
1075
	}
1076
1077
	/**
1078
	 * Updates an existing intent with updated amount, source, and customer.
1079
	 *
1080
	 * @param object   $intent          The existing intent object.
1081
	 * @param WC_Order $order           The order.
1082
	 * @param object   $prepared_source Currently selected source.
1083
	 * @return object                   An updated intent.
1084
	 */
1085
	public function update_existing_intent( $intent, $order, $prepared_source ) {
1086
		$request = array();
1087
1088
		if ( $prepared_source->source !== $intent->source ) {
1089
			$request['source'] = $prepared_source->source;
1090
		}
1091
1092
		$new_amount = WC_Stripe_Helper::get_stripe_amount( $order->get_total() );
1093
		if ( $intent->amount !== $new_amount ) {
1094
			$request['amount'] = $new_amount;
1095
		}
1096
1097
		if ( $prepared_source->customer && $intent->customer !== $prepared_source->customer ) {
1098
			$request['customer'] = $prepared_source->customer;
1099
		}
1100
1101
		if ( empty( $request ) ) {
1102
			return $intent;
1103
		}
1104
1105
		return WC_Stripe_API::request( $request, "payment_intents/$intent->id" );
1106
	}
1107
1108
	/**
1109
	 * Confirms an intent if it is the `requires_confirmation` state.
1110
	 *
1111
	 * @since 4.2.1
1112
	 * @param object   $intent          The intent to confirm.
1113
	 * @param WC_Order $order           The order that the intent is associated with.
1114
	 * @param object   $prepared_source The source that is being charged.
1115
	 * @return object                   Either an error or the updated intent.
1116
	 */
1117
	public function confirm_intent( $intent, $order, $prepared_source ) {
1118
		if ( 'requires_confirmation' !== $intent->status ) {
1119
			return $intent;
1120
		}
1121
1122
		// Try to confirm the intent & capture the charge (if 3DS is not required).
1123
		$confirm_request = array(
1124
			'source' => $prepared_source->source,
1125
		);
1126
1127
		$confirmed_intent = WC_Stripe_API::request( $confirm_request, "payment_intents/$intent->id/confirm" );
1128
1129
		if ( ! empty( $confirmed_intent->error ) ) {
1130
			return $confirmed_intent;
1131
		}
1132
1133
		// Save a note about the status of the intent.
1134
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1135
		if ( 'succeeded' === $confirmed_intent->status ) {
1136
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
1137
		} elseif ( 'requires_action' === $confirmed_intent->status ) {
1138
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id requires authentication for order $order_id" );
1139
		}
1140
1141
		return $confirmed_intent;
1142
	}
1143
1144
	/**
1145
	 * Saves intent to order.
1146
	 *
1147
	 * @since 3.2.0
1148
	 * @param WC_Order $order For to which the source applies.
1149
	 * @param stdClass $intent Payment intent information.
1150
	 */
1151
	public function save_intent_to_order( $order, $intent ) {
1152
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1153
1154
		if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
1155
			update_post_meta( $order_id, '_stripe_intent_id', $intent->id );
1156
		} else {
1157
			$order->update_meta_data( '_stripe_intent_id', $intent->id );
1158
		}
1159
1160
		if ( is_callable( array( $order, 'save' ) ) ) {
1161
			$order->save();
1162
		}
1163
	}
1164
1165
	/**
1166
	 * Retrieves the payment intent, associated with an order.
1167
	 *
1168
	 * @since 4.2
1169
	 * @param WC_Order $order The order to retrieve an intent for.
1170
	 * @return obect|bool     Either the intent object or `false`.
1171
	 */
1172
	public function get_intent_from_order( $order ) {
1173
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1174
1175 View Code Duplication
		if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
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...
1176
			$intent_id = get_post_meta( $order_id, '_stripe_intent_id', true );
1177
		} else {
1178
			$intent_id = $order->get_meta( '_stripe_intent_id' );
1179
		}
1180
1181
		if ( $intent_id ) {
1182
			return WC_Stripe_API::request( array(), "payment_intents/$intent_id", 'GET' );
1183
		}
1184
1185
		// The order doesn't have a payment intent, but it may have a setup intent.
1186 View Code Duplication
		if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
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...
1187
			$intent_id = get_post_meta( $order_id, '_stripe_setup_intent', true );
1188
		} else {
1189
			$intent_id = $order->get_meta( '_stripe_setup_intent' );
1190
		}
1191
1192
		if ( $intent_id ) {
1193
			return WC_Stripe_API::request( array(), "setup_intents/$intent_id", 'GET' );
1194
		}
1195
1196
		return false;
1197
	}
1198
1199
	/**
1200
	 * Locks an order for payment intent processing for 5 minutes.
1201
	 *
1202
	 * @since 4.2
1203
	 * @param WC_Order $order  The order that is being paid.
1204
	 * @param stdClass $intent The intent that is being processed.
1205
	 * @return bool            A flag that indicates whether the order is already locked.
1206
	 */
1207
	public function lock_order_payment( $order, $intent ) {
1208
		$order_id       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1209
		$transient_name = 'wc_stripe_processing_intent_' . $order_id;
1210
		$processing     = get_transient( $transient_name );
1211
1212
		// Block the process if the same intent is already being handled.
1213
		if ( $processing === $intent->id ) {
1214
			return true;
1215
		}
1216
1217
		// Save the new intent as a transient, eventually overwriting another one.
1218
		set_transient( $transient_name, $intent->id, 5 * MINUTE_IN_SECONDS );
1219
1220
		return false;
1221
	}
1222
1223
	/**
1224
	 * Unlocks an order for processing by payment intents.
1225
	 *
1226
	 * @since 4.2
1227
	 * @param WC_Order $order The order that is being unlocked.
1228
	 */
1229
	public function unlock_order_payment( $order ) {
1230
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1231
		delete_transient( 'wc_stripe_processing_intent_' . $order_id );
1232
	}
1233
1234
	/**
1235
	 * Given a response from Stripe, check if it's a card error where authentication is required
1236
	 * to complete the payment.
1237
	 *
1238
	 * @param object $response The response from Stripe.
1239
	 * @return boolean Whether or not it's a 'authentication_required' error
1240
	 */
1241
	public function is_authentication_required_for_payment( $response ) {
1242
		return ( ! empty( $response->error ) && 'authentication_required' === $response->error->code )
1243
			|| ( ! empty( $response->last_payment_error ) && 'authentication_required' === $response->last_payment_error->code );
1244
	}
1245
1246
	/**
1247
	 * Creates a SetupIntent for future payments, and saves it to the order.
1248
	 *
1249
	 * @param WC_Order $order           The ID of the (free/pre- order).
1250
	 * @param object   $prepared_source The source, entered/chosen by the customer.
1251
	 * @return string                   The client secret of the intent, used for confirmation in JS.
1252
	 */
1253
	public function setup_intent( $order, $prepared_source ) {
1254
		$order_id     = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1255
		$setup_intent = WC_Stripe_API::request( array(
1256
			'payment_method' => $prepared_source->source,
1257
			'customer'       => $prepared_source->customer,
1258
			'confirm'        => 'true',
1259
		), 'setup_intents' );
1260
1261
		if ( is_wp_error( $setup_intent ) ) {
1262
			WC_Stripe_Logger::log( "Unable to create SetupIntent for Order #$order_id: " . print_r( $setup_intent, true ) );
1263
		} elseif ( 'requires_action' === $setup_intent->status ) {
1264
			if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
1265
				update_post_meta( $order_id, '_stripe_setup_intent', $setup_intent->id );
1266
			} else {
1267
				$order->update_meta_data( '_stripe_setup_intent', $setup_intent->id );
1268
				$order->save();
1269
			}
1270
1271
			return $setup_intent->client_secret;
1272
		}
1273
	}
1274
1275
	/**
1276
	 * Create and confirm a new PaymentIntent.
1277
	 *
1278
	 * @param WC_Order $order           The order that is being paid for.
1279
	 * @param object   $prepared_source The source that is used for the payment.
1280
	 * @param float    $amount          The amount to charge. If not specified, it will be read from the order.
1281
	 * @return object                   An intent or an error.
1282
	 */
1283
	public function create_and_confirm_intent_for_off_session( $order, $prepared_source, $amount = NULL ) {
1284
		// The request for a charge contains metadata for the intent.
1285
		$full_request = $this->generate_payment_request( $order, $prepared_source );
1286
1287
		$request = array(
1288
			'amount'               => $amount ? WC_Stripe_Helper::get_stripe_amount( $amount, $full_request['currency'] ) : $full_request['amount'],
1289
			'currency'             => $full_request['currency'],
1290
			'description'          => $full_request['description'],
1291
			'metadata'             => $full_request['metadata'],
1292
			'payment_method_types' => array(
1293
				'card',
1294
			),
1295
			'off_session'          => 'true',
1296
			'confirm'              => 'true',
1297
			'confirmation_method'  => 'automatic',
1298
		);
1299
1300
		if ( isset( $full_request['statement_descriptor'] ) ) {
1301
			$request['statement_descriptor'] = $full_request['statement_descriptor'];
1302
		}
1303
1304
		if ( isset( $full_request['customer'] ) ) {
1305
			$request['customer'] = $full_request['customer'];
1306
		}
1307
1308
		if ( isset( $full_request['source'] ) ) {
1309
			$request['source'] = $full_request['source'];
1310
		}
1311
1312
		$intent = WC_Stripe_API::request( $request, 'payment_intents' );
1313
		$is_authentication_required = $this->is_authentication_required_for_payment( $intent );
1314
1315
		if ( ! empty( $intent->error ) && ! $is_authentication_required ) {
1316
			return $intent;
1317
		}
1318
1319
		$intent_id      = ( ! empty( $intent->error )
1320
			? $intent->error->payment_intent->id
1321
			: $intent->id
1322
		);
1323
		$payment_intent = ( ! empty( $intent->error )
1324
			? $intent->error->payment_intent
1325
			: $intent
1326
		);
1327
		$order_id       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1328
		WC_Stripe_Logger::log( "Stripe PaymentIntent $intent_id initiated for order $order_id" );
1329
1330
		// Save the intent ID to the order.
1331
		$this->save_intent_to_order( $order, $payment_intent );
1332
1333
		return $intent;
1334
	}
1335
}
1336