Completed
Push — master ( 741225...ec1b6d )
by Radoslav
01:40
created

WC_Stripe_Payment_Gateway::get_source_object()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 13
rs 9.8333
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, $existing_customer_id = null ) {
580
		$customer = new WC_Stripe_Customer( $user_id );
581
		if ( ! empty( $existing_customer_id ) ) {
582
			$customer->set_id( $existing_customer_id );
583
		}
584
585
		$force_save_source = apply_filters( 'wc_stripe_force_save_source', $force_save_source, $customer );
586
		$source_object     = '';
587
		$source_id         = '';
588
		$wc_token_id       = false;
589
		$payment_method    = isset( $_POST['payment_method'] ) ? wc_clean( $_POST['payment_method'] ) : 'stripe';
590
		$is_token          = false;
591
592
		// New CC info was entered and we have a new source to process.
593
		if ( ! empty( $_POST['stripe_source'] ) ) {
594
			$source_object = self::get_source_object( wc_clean( $_POST['stripe_source'] ) );
595
			$source_id     = $source_object->id;
596
597
			// This checks to see if customer opted to save the payment method to file.
598
			$maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
599
600
			/**
601
			 * This is true if the user wants to store the card to their account.
602
			 * Criteria to save to file is they are logged in, they opted to save or product requirements and the source is
603
			 * actually reusable. Either that or force_save_source is true.
604
			 */
605
			if ( ( $user_id && $this->saved_cards && $maybe_saved_card && 'reusable' === $source_object->usage ) || $force_save_source ) {
606
				$response = $customer->add_source( $source_object->id );
607
608
				if ( ! empty( $response->error ) ) {
609
					throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
610
				}
611
			}
612
		} elseif ( $this->is_using_saved_payment_method() ) {
613
			// Use an existing token, and then process the payment.
614
			$wc_token_id = wc_clean( $_POST[ 'wc-' . $payment_method . '-payment-token' ] );
615
			$wc_token    = WC_Payment_Tokens::get( $wc_token_id );
616
617
			if ( ! $wc_token || $wc_token->get_user_id() !== get_current_user_id() ) {
618
				WC()->session->set( 'refresh_totals', true );
619
				throw new WC_Stripe_Exception( 'Invalid payment method', __( 'Invalid payment method. Please input a new card number.', 'woocommerce-gateway-stripe' ) );
620
			}
621
622
			$source_id = $wc_token->get_token();
623
624
			if ( $this->is_type_legacy_card( $source_id ) ) {
625
				$is_token = true;
626
			}
627
		} elseif ( isset( $_POST['stripe_token'] ) && 'new' !== $_POST['stripe_token'] ) {
628
			$stripe_token     = wc_clean( $_POST['stripe_token'] );
629
			$maybe_saved_card = isset( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] ) && ! empty( $_POST[ 'wc-' . $payment_method . '-new-payment-method' ] );
630
631
			// This is true if the user wants to store the card to their account.
632
			if ( ( $user_id && $this->saved_cards && $maybe_saved_card ) || $force_save_source ) {
633
				$response = $customer->add_source( $stripe_token );
634
635
				if ( ! empty( $response->error ) ) {
636
					throw new WC_Stripe_Exception( print_r( $response, true ), $response->error->message );
637
				}
638
			} else {
639
				$source_id    = $stripe_token;
640
				$is_token     = true;
641
			}
642
		}
643
644
		$customer_id = $customer->get_id();
645
		if ( ! $customer_id ) {
646
			$customer->set_id( $customer->create_customer() );
647
			$customer_id = $customer->get_id();
648
		} else {
649
			$customer->update_customer();
650
		}
651
652
		if ( empty( $source_object ) && ! $is_token ) {
653
			$source_object = self::get_source_object( $source_id );
654
		}
655
656
		return (object) array(
657
			'token_id'      => $wc_token_id,
658
			'customer'      => $customer_id,
659
			'source'        => $source_id,
660
			'source_object' => $source_object,
661
		);
662
	}
663
664
	/**
665
	 * Get payment source from an order. This could be used in the future for
666
	 * a subscription as an example, therefore using the current user ID would
667
	 * not work - the customer won't be logged in :)
668
	 *
669
	 * Not using 2.6 tokens for this part since we need a customer AND a card
670
	 * token, and not just one.
671
	 *
672
	 * @since 3.1.0
673
	 * @version 4.0.0
674
	 * @param object $order
675
	 * @return object
676
	 */
677
	public function prepare_order_source( $order = null ) {
678
		$stripe_customer = new WC_Stripe_Customer();
679
		$stripe_source   = false;
680
		$token_id        = false;
681
		$source_object   = false;
682
683
		if ( $order ) {
684
			$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
685
686
			$stripe_customer_id = get_post_meta( $order_id, '_stripe_customer_id', true );
687
688
			if ( $stripe_customer_id ) {
689
				$stripe_customer->set_id( $stripe_customer_id );
690
			}
691
692
			$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 );
693
694
			// Since 4.0.0, we changed card to source so we need to account for that.
695
			if ( empty( $source_id ) ) {
696
				$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 );
697
698
				// Take this opportunity to update the key name.
699
				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 );
700
701
				if ( is_callable( array( $order, 'save' ) ) ) {
702
					$order->save();
703
				}
704
			}
705
706
			if ( $source_id ) {
707
				$stripe_source = $source_id;
708
				$source_object = WC_Stripe_API::retrieve( 'sources/' . $source_id );
709
			} elseif ( apply_filters( 'wc_stripe_use_default_customer_source', true ) ) {
710
				/*
711
				 * We can attempt to charge the customer's default source
712
				 * by sending empty source id.
713
				 */
714
				$stripe_source = '';
715
			}
716
		}
717
718
		return (object) array(
719
			'token_id'      => $token_id,
720
			'customer'      => $stripe_customer ? $stripe_customer->get_id() : false,
721
			'source'        => $stripe_source,
722
			'source_object' => $source_object,
723
		);
724
	}
725
726
	/**
727
	 * Save source to order.
728
	 *
729
	 * @since 3.1.0
730
	 * @version 4.0.0
731
	 * @param WC_Order $order For to which the source applies.
732
	 * @param stdClass $source Source information.
733
	 */
734
	public function save_source_to_order( $order, $source ) {
735
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
736
737
		// Store source in the order.
738
		if ( $source->customer ) {
739
			if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
740
				update_post_meta( $order_id, '_stripe_customer_id', $source->customer );
741
			} else {
742
				$order->update_meta_data( '_stripe_customer_id', $source->customer );
743
			}
744
		}
745
746
		if ( $source->source ) {
747
			if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
748
				update_post_meta( $order_id, '_stripe_source_id', $source->source );
749
			} else {
750
				$order->update_meta_data( '_stripe_source_id', $source->source );
751
			}
752
		}
753
754
		if ( is_callable( array( $order, 'save' ) ) ) {
755
			$order->save();
756
		}
757
	}
758
759
	/**
760
	 * Updates Stripe fees/net.
761
	 * e.g usage would be after a refund.
762
	 *
763
	 * @since 4.0.0
764
	 * @version 4.0.6
765
	 * @param object $order The order object
766
	 * @param int $balance_transaction_id
767
	 */
768
	public function update_fees( $order, $balance_transaction_id ) {
769
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
770
771
		$balance_transaction = WC_Stripe_API::retrieve( 'balance/history/' . $balance_transaction_id );
772
773
		if ( empty( $balance_transaction->error ) ) {
774
			if ( isset( $balance_transaction ) && isset( $balance_transaction->fee ) ) {
775
				// Fees and Net needs to both come from Stripe to be accurate as the returned
776
				// values are in the local currency of the Stripe account, not from WC.
777
				$fee_refund = ! empty( $balance_transaction->fee ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'fee' ) : 0;
778
				$net_refund = ! empty( $balance_transaction->net ) ? WC_Stripe_Helper::format_balance_fee( $balance_transaction, 'net' ) : 0;
779
780
				// Current data fee & net.
781
				$fee_current = WC_Stripe_Helper::get_stripe_fee( $order );
782
				$net_current = WC_Stripe_Helper::get_stripe_net( $order );
783
784
				// Calculation.
785
				$fee = (float) $fee_current + (float) $fee_refund;
786
				$net = (float) $net_current + (float) $net_refund;
787
788
				WC_Stripe_Helper::update_stripe_fee( $order, $fee );
789
				WC_Stripe_Helper::update_stripe_net( $order, $net );
790
791
				$currency = ! empty( $balance_transaction->currency ) ? strtoupper( $balance_transaction->currency ) : null;
792
				WC_Stripe_Helper::update_stripe_currency( $order, $currency );
793
794
				if ( is_callable( array( $order, 'save' ) ) ) {
795
					$order->save();
796
				}
797
			}
798
		} else {
799
			WC_Stripe_Logger::log( "Unable to update fees/net meta for order: {$order_id}" );
800
		}
801
	}
802
803
	/**
804
	 * Refund a charge.
805
	 *
806
	 * @since 3.1.0
807
	 * @version 4.0.0
808
	 * @param  int $order_id
809
	 * @param  float $amount
810
	 * @return bool
811
	 */
812
	public function process_refund( $order_id, $amount = null, $reason = '' ) {
813
		$order = wc_get_order( $order_id );
814
815
		if ( ! $order ) {
816
			return false;
817
		}
818
819
		$request = array();
820
821
		if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
822
			$order_currency = get_post_meta( $order_id, '_order_currency', true );
823
			$captured       = get_post_meta( $order_id, '_stripe_charge_captured', true );
824
			$charge_id      = get_post_meta( $order_id, '_transaction_id', true );
825
		} else {
826
			$order_currency = $order->get_currency();
827
			$captured       = $order->get_meta( '_stripe_charge_captured', true );
828
			$charge_id      = $order->get_transaction_id();
829
		}
830
831
		if ( ! $charge_id ) {
832
			return false;
833
		}
834
835
		if ( ! is_null( $amount ) ) {
836
			$request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, $order_currency );
837
		}
838
839
		// If order is only authorized, don't pass amount.
840
		if ( 'yes' !== $captured ) {
841
			unset( $request['amount'] );
842
		}
843
844
		if ( $reason ) {
845
			$request['metadata'] = array(
846
				'reason' => $reason,
847
			);
848
		}
849
850
		$request['charge'] = $charge_id;
851
		WC_Stripe_Logger::log( "Info: Beginning refund for order {$charge_id} for the amount of {$amount}" );
852
853
		$request = apply_filters( 'wc_stripe_refund_request', $request, $order );
854
855
		$intent = $this->get_intent_from_order( $order );
856
		$intent_cancelled = false;
857
		if ( $intent ) {
858
			// If the order has a Payment Intent pending capture, then the Intent itself must be refunded (cancelled), not the Charge
859
			if ( ! empty( $intent->error ) ) {
860
				$response = $intent;
861
				$intent_cancelled = true;
862
			} elseif ( 'requires_capture' === $intent->status ) {
863
				$result = WC_Stripe_API::request(
864
					array(),
865
					'payment_intents/' . $intent->id . '/cancel'
866
				);
867
				$intent_cancelled = true;
868
869
				if ( ! empty( $result->error ) ) {
870
					$response = $result;
871
				} else {
872
					$charge = end( $result->charges->data );
873
					$response = end( $charge->refunds->data );
874
				}
875
			}
876
		}
877
878
		if ( ! $intent_cancelled ) {
879
			$response = WC_Stripe_API::request( $request, 'refunds' );
880
		}
881
882
		if ( ! empty( $response->error ) ) {
883
			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...
884
885
			return $response;
886
887
		} elseif ( ! empty( $response->id ) ) {
888
			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 );
889
890
			$amount = wc_price( $response->amount / 100 );
891
892
			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() ) ) {
893
				$amount = wc_price( $response->amount );
894
			}
895
896
			if ( isset( $response->balance_transaction ) ) {
897
				$this->update_fees( $order, $response->balance_transaction );
898
			}
899
900
			/* translators: 1) dollar amount 2) transaction id 3) refund message */
901
			$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' );
902
903
			$order->add_order_note( $refund_message );
904
			WC_Stripe_Logger::log( 'Success: ' . html_entity_decode( wp_strip_all_tags( $refund_message ) ) );
905
906
			return true;
907
		}
908
	}
909
910
	/**
911
	 * Add payment method via account screen.
912
	 * We don't store the token locally, but to the Stripe API.
913
	 *
914
	 * @since 3.0.0
915
	 * @version 4.0.0
916
	 */
917
	public function add_payment_method() {
918
		$error     = false;
919
		$error_msg = __( 'There was a problem adding the payment method.', 'woocommerce-gateway-stripe' );
920
		$source_id = '';
921
922
		if ( empty( $_POST['stripe_source'] ) && empty( $_POST['stripe_token'] ) || ! is_user_logged_in() ) {
923
			$error = true;
924
		}
925
926
		$stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
927
928
		$source = ! empty( $_POST['stripe_source'] ) ? wc_clean( $_POST['stripe_source'] ) : '';
929
930
		$source_object = WC_Stripe_API::retrieve( 'sources/' . $source );
931
932
		if ( isset( $source_object ) ) {
933
			if ( ! empty( $source_object->error ) ) {
934
				$error = true;
935
			}
936
937
			$source_id = $source_object->id;
938
		} elseif ( isset( $_POST['stripe_token'] ) ) {
939
			$source_id = wc_clean( $_POST['stripe_token'] );
940
		}
941
942
		$response = $stripe_customer->add_source( $source_id );
943
944
		if ( ! $response || is_wp_error( $response ) || ! empty( $response->error ) ) {
945
			$error = true;
946
		}
947
948
		if ( $error ) {
949
			wc_add_notice( $error_msg, 'error' );
950
			WC_Stripe_Logger::log( 'Add payment method Error: ' . $error_msg );
951
			return;
952
		}
953
954
		do_action( 'wc_stripe_add_payment_method_' . $_POST['payment_method'] . '_success', $source_id, $source_object );
955
956
		return array(
957
			'result'   => 'success',
958
			'redirect' => wc_get_endpoint_url( 'payment-methods' ),
959
		);
960
	}
961
962
	/**
963
	 * Gets the locale with normalization that only Stripe accepts.
964
	 *
965
	 * @since 4.0.6
966
	 * @return string $locale
967
	 */
968
	public function get_locale() {
969
		$locale = get_locale();
970
971
		/*
972
		 * Stripe expects Norwegian to only be passed NO.
973
		 * But WP has different dialects.
974
		 */
975
		if ( 'NO' === substr( $locale, 3, 2 ) ) {
976
			$locale = 'no';
977
		} else {
978
			$locale = substr( get_locale(), 0, 2 );
979
		}
980
981
		return $locale;
982
	}
983
984
	/**
985
	 * Change the idempotency key so charge can
986
	 * process order as a different transaction.
987
	 *
988
	 * @since 4.0.6
989
	 * @param string $idempotency_key
990
	 * @param array $request
991
	 */
992
	public function change_idempotency_key( $idempotency_key, $request ) {
993
		$customer = ! empty( $request['customer'] ) ? $request['customer'] : '';
994
		$source   = ! empty( $request['source'] ) ? $request['source'] : $customer;
995
		$count    = $this->retry_interval;
996
997
		return $request['metadata']['order_id'] . '-' . $count . '-' . $source;
998
	}
999
1000
	/**
1001
	 * Checks if request is the original to prevent double processing
1002
	 * on WC side. The original-request header and request-id header
1003
	 * needs to be the same to mean its the original request.
1004
	 *
1005
	 * @since 4.0.6
1006
	 * @param array $headers
1007
	 */
1008
	public function is_original_request( $headers ) {
1009
		if ( $headers['original-request'] === $headers['request-id'] ) {
1010
			return true;
1011
		}
1012
1013
		return false;
1014
	}
1015
1016
	/**
1017
	 * Generates the request when creating a new payment intent.
1018
	 *
1019
	 * @param WC_Order $order           The order that is being paid for.
1020
	 * @param object   $prepared_source The source that is used for the payment.
1021
	 * @return array                    The arguments for the request.
1022
	 */
1023
	public function generate_create_intent_request( $order, $prepared_source ) {
1024
		// The request for a charge contains metadata for the intent.
1025
		$full_request = $this->generate_payment_request( $order, $prepared_source );
1026
1027
		$request = array(
1028
			'source'               => $prepared_source->source,
1029
			'amount'               => WC_Stripe_Helper::get_stripe_amount( $order->get_total() ),
1030
			'currency'             => strtolower( WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->get_order_currency() : $order->get_currency() ),
1031
			'description'          => $full_request['description'],
1032
			'metadata'             => $full_request['metadata'],
1033
			'capture_method'       => ( 'true' === $full_request['capture'] ) ? 'automatic' : 'manual',
1034
			'payment_method_types' => array(
1035
				'card',
1036
			),
1037
		);
1038
1039
		if ( $prepared_source->customer ) {
1040
			$request['customer'] = $prepared_source->customer;
1041
		}
1042
1043
		if ( isset( $full_request['statement_descriptor'] ) ) {
1044
			$request['statement_descriptor'] = $full_request['statement_descriptor'];
1045
		}
1046
1047
		/**
1048
		 * Filter the return value of the WC_Payment_Gateway_CC::generate_create_intent_request.
1049
		 *
1050
		 * @since 3.1.0
1051
		 * @param array $request
1052
		 * @param WC_Order $order
1053
		 * @param object $source
1054
		 */
1055
		return apply_filters( 'wc_stripe_generate_create_intent_request', $request, $order, $prepared_source );
1056
	}
1057
1058
	/**
1059
	 * Create a new PaymentIntent.
1060
	 *
1061
	 * @param WC_Order $order           The order that is being paid for.
1062
	 * @param object   $prepared_source The source that is used for the payment.
1063
	 * @return object                   An intent or an error.
1064
	 */
1065
	public function create_intent( $order, $prepared_source ) {
1066
		$request = $this->generate_create_intent_request( $order, $prepared_source );
1067
1068
		// Create an intent that awaits an action.
1069
		$intent = WC_Stripe_API::request( $request, 'payment_intents' );
1070
		if ( ! empty( $intent->error ) ) {
1071
			return $intent;
1072
		}
1073
1074
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1075
		WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id initiated for order $order_id" );
1076
1077
		// Save the intent ID to the order.
1078
		$this->save_intent_to_order( $order, $intent );
1079
1080
		return $intent;
1081
	}
1082
1083
	/**
1084
	 * Updates an existing intent with updated amount, source, and customer.
1085
	 *
1086
	 * @param object   $intent          The existing intent object.
1087
	 * @param WC_Order $order           The order.
1088
	 * @param object   $prepared_source Currently selected source.
1089
	 * @return object                   An updated intent.
1090
	 */
1091
	public function update_existing_intent( $intent, $order, $prepared_source ) {
1092
		$request = array();
1093
1094
		if ( $prepared_source->source !== $intent->source ) {
1095
			$request['source'] = $prepared_source->source;
1096
		}
1097
1098
		$new_amount = WC_Stripe_Helper::get_stripe_amount( $order->get_total() );
1099
		if ( $intent->amount !== $new_amount ) {
1100
			$request['amount'] = $new_amount;
1101
		}
1102
1103
		if ( $prepared_source->customer && $intent->customer !== $prepared_source->customer ) {
1104
			$request['customer'] = $prepared_source->customer;
1105
		}
1106
1107
		if ( empty( $request ) ) {
1108
			return $intent;
1109
		}
1110
1111
		return WC_Stripe_API::request( $request, "payment_intents/$intent->id" );
1112
	}
1113
1114
	/**
1115
	 * Confirms an intent if it is the `requires_confirmation` state.
1116
	 *
1117
	 * @since 4.2.1
1118
	 * @param object   $intent          The intent to confirm.
1119
	 * @param WC_Order $order           The order that the intent is associated with.
1120
	 * @param object   $prepared_source The source that is being charged.
1121
	 * @return object                   Either an error or the updated intent.
1122
	 */
1123
	public function confirm_intent( $intent, $order, $prepared_source ) {
1124
		if ( 'requires_confirmation' !== $intent->status ) {
1125
			return $intent;
1126
		}
1127
1128
		// Try to confirm the intent & capture the charge (if 3DS is not required).
1129
		$confirm_request = array(
1130
			'source' => $prepared_source->source,
1131
		);
1132
1133
		$confirmed_intent = WC_Stripe_API::request( $confirm_request, "payment_intents/$intent->id/confirm" );
1134
1135
		if ( ! empty( $confirmed_intent->error ) ) {
1136
			return $confirmed_intent;
1137
		}
1138
1139
		// Save a note about the status of the intent.
1140
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1141
		if ( 'succeeded' === $confirmed_intent->status ) {
1142
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
1143
		} elseif ( 'requires_action' === $confirmed_intent->status ) {
1144
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id requires authentication for order $order_id" );
1145
		}
1146
1147
		return $confirmed_intent;
1148
	}
1149
1150
	/**
1151
	 * Saves intent to order.
1152
	 *
1153
	 * @since 3.2.0
1154
	 * @param WC_Order $order For to which the source applies.
1155
	 * @param stdClass $intent Payment intent information.
1156
	 */
1157
	public function save_intent_to_order( $order, $intent ) {
1158
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1159
1160
		if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
1161
			update_post_meta( $order_id, '_stripe_intent_id', $intent->id );
1162
		} else {
1163
			$order->update_meta_data( '_stripe_intent_id', $intent->id );
1164
		}
1165
1166
		if ( is_callable( array( $order, 'save' ) ) ) {
1167
			$order->save();
1168
		}
1169
	}
1170
1171
	/**
1172
	 * Retrieves the payment intent, associated with an order.
1173
	 *
1174
	 * @since 4.2
1175
	 * @param WC_Order $order The order to retrieve an intent for.
1176
	 * @return obect|bool     Either the intent object or `false`.
1177
	 */
1178
	public function get_intent_from_order( $order ) {
1179
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1180
1181 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...
1182
			$intent_id = get_post_meta( $order_id, '_stripe_intent_id', true );
1183
		} else {
1184
			$intent_id = $order->get_meta( '_stripe_intent_id' );
1185
		}
1186
1187
		if ( $intent_id ) {
1188
			return WC_Stripe_API::request( array(), "payment_intents/$intent_id", 'GET' );
1189
		}
1190
1191
		// The order doesn't have a payment intent, but it may have a setup intent.
1192 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...
1193
			$intent_id = get_post_meta( $order_id, '_stripe_setup_intent', true );
1194
		} else {
1195
			$intent_id = $order->get_meta( '_stripe_setup_intent' );
1196
		}
1197
1198
		if ( $intent_id ) {
1199
			return WC_Stripe_API::request( array(), "setup_intents/$intent_id", 'GET' );
1200
		}
1201
1202
		return false;
1203
	}
1204
1205
	/**
1206
	 * Locks an order for payment intent processing for 5 minutes.
1207
	 *
1208
	 * @since 4.2
1209
	 * @param WC_Order $order  The order that is being paid.
1210
	 * @param stdClass $intent The intent that is being processed.
1211
	 * @return bool            A flag that indicates whether the order is already locked.
1212
	 */
1213
	public function lock_order_payment( $order, $intent = null ) {
1214
		$order_id       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1215
		$transient_name = 'wc_stripe_processing_intent_' . $order_id;
1216
		$processing     = get_transient( $transient_name );
1217
1218
		// Block the process if the same intent is already being handled.
1219
		if ( "-1" === $processing || ( isset( $intent->id ) && $processing === $intent->id ) ) {
1220
			return true;
1221
		}
1222
1223
		// Save the new intent as a transient, eventually overwriting another one.
1224
		set_transient( $transient_name, empty( $intent ) ? '-1' : $intent->id, 5 * MINUTE_IN_SECONDS );
1225
1226
		return false;
1227
	}
1228
1229
	/**
1230
	 * Unlocks an order for processing by payment intents.
1231
	 *
1232
	 * @since 4.2
1233
	 * @param WC_Order $order The order that is being unlocked.
1234
	 */
1235
	public function unlock_order_payment( $order ) {
1236
		$order_id = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1237
		delete_transient( 'wc_stripe_processing_intent_' . $order_id );
1238
	}
1239
1240
	/**
1241
	 * Given a response from Stripe, check if it's a card error where authentication is required
1242
	 * to complete the payment.
1243
	 *
1244
	 * @param object $response The response from Stripe.
1245
	 * @return boolean Whether or not it's a 'authentication_required' error
1246
	 */
1247
	public function is_authentication_required_for_payment( $response ) {
1248
		return ( ! empty( $response->error ) && 'authentication_required' === $response->error->code )
1249
			|| ( ! empty( $response->last_payment_error ) && 'authentication_required' === $response->last_payment_error->code );
1250
	}
1251
1252
	/**
1253
	 * Creates a SetupIntent for future payments, and saves it to the order.
1254
	 *
1255
	 * @param WC_Order $order           The ID of the (free/pre- order).
1256
	 * @param object   $prepared_source The source, entered/chosen by the customer.
1257
	 * @return string                   The client secret of the intent, used for confirmation in JS.
1258
	 */
1259
	public function setup_intent( $order, $prepared_source ) {
1260
		$order_id     = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1261
		$setup_intent = WC_Stripe_API::request( array(
1262
			'payment_method' => $prepared_source->source,
1263
			'customer'       => $prepared_source->customer,
1264
			'confirm'        => 'true',
1265
		), 'setup_intents' );
1266
1267
		if ( is_wp_error( $setup_intent ) ) {
1268
			WC_Stripe_Logger::log( "Unable to create SetupIntent for Order #$order_id: " . print_r( $setup_intent, true ) );
1269
		} elseif ( 'requires_action' === $setup_intent->status ) {
1270
			if ( WC_Stripe_Helper::is_wc_lt( '3.0' ) ) {
1271
				update_post_meta( $order_id, '_stripe_setup_intent', $setup_intent->id );
1272
			} else {
1273
				$order->update_meta_data( '_stripe_setup_intent', $setup_intent->id );
1274
				$order->save();
1275
			}
1276
1277
			return $setup_intent->client_secret;
1278
		}
1279
	}
1280
1281
	/**
1282
	 * Create and confirm a new PaymentIntent.
1283
	 *
1284
	 * @param WC_Order $order           The order that is being paid for.
1285
	 * @param object   $prepared_source The source that is used for the payment.
1286
	 * @param float    $amount          The amount to charge. If not specified, it will be read from the order.
1287
	 * @return object                   An intent or an error.
1288
	 */
1289
	public function create_and_confirm_intent_for_off_session( $order, $prepared_source, $amount = NULL ) {
1290
		// The request for a charge contains metadata for the intent.
1291
		$full_request = $this->generate_payment_request( $order, $prepared_source );
1292
1293
		$request = array(
1294
			'amount'               => $amount ? WC_Stripe_Helper::get_stripe_amount( $amount, $full_request['currency'] ) : $full_request['amount'],
1295
			'currency'             => $full_request['currency'],
1296
			'description'          => $full_request['description'],
1297
			'metadata'             => $full_request['metadata'],
1298
			'payment_method_types' => array(
1299
				'card',
1300
			),
1301
			'off_session'          => 'true',
1302
			'confirm'              => 'true',
1303
			'confirmation_method'  => 'automatic',
1304
		);
1305
1306
		if ( isset( $full_request['statement_descriptor'] ) ) {
1307
			$request['statement_descriptor'] = $full_request['statement_descriptor'];
1308
		}
1309
1310
		if ( isset( $full_request['customer'] ) ) {
1311
			$request['customer'] = $full_request['customer'];
1312
		}
1313
1314
		if ( isset( $full_request['source'] ) ) {
1315
			$request['source'] = $full_request['source'];
1316
		}
1317
1318
		$intent = WC_Stripe_API::request( $request, 'payment_intents' );
1319
		$is_authentication_required = $this->is_authentication_required_for_payment( $intent );
1320
1321
		if ( ! empty( $intent->error ) && ! $is_authentication_required ) {
1322
			return $intent;
1323
		}
1324
1325
		$intent_id      = ( ! empty( $intent->error )
1326
			? $intent->error->payment_intent->id
1327
			: $intent->id
1328
		);
1329
		$payment_intent = ( ! empty( $intent->error )
1330
			? $intent->error->payment_intent
1331
			: $intent
1332
		);
1333
		$order_id       = WC_Stripe_Helper::is_wc_lt( '3.0' ) ? $order->id : $order->get_id();
1334
		WC_Stripe_Logger::log( "Stripe PaymentIntent $intent_id initiated for order $order_id" );
1335
1336
		// Save the intent ID to the order.
1337
		$this->save_intent_to_order( $order, $payment_intent );
1338
1339
		return $intent;
1340
	}
1341
}
1342