Completed
Pull Request — master (#1224)
by
unknown
02:13
created

WC_Stripe_Webhook_Handler::process_setup_intent()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 36

Duplication

Lines 8
Ratio 22.22 %

Importance

Changes 0
Metric Value
cc 8
nc 7
nop 1
dl 8
loc 36
rs 8.0995
c 0
b 0
f 0
1
<?php
2
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * Class WC_Stripe_Webhook_Handler.
8
 *
9
 * Handles webhooks from Stripe on sources that are not immediately chargeable.
10
 * @since 4.0.0
11
 */
12
class WC_Stripe_Webhook_Handler extends WC_Stripe_Payment_Gateway {
13
	/**
14
	 * Delay of retries.
15
	 *
16
	 * @var int
17
	 */
18
	public $retry_interval;
19
20
	/**
21
	 * Is test mode active?
22
	 *
23
	 * @var bool
24
	 */
25
	public $testmode;
26
27
	/**
28
	 * The secret to use when verifying webhooks.
29
	 *
30
	 * @var string
31
	 */
32
	protected $secret;
33
34
	/**
35
	 * Constructor.
36
	 *
37
	 * @since 4.0.0
38
	 * @version 4.0.0
39
	 */
40
	public function __construct() {
41
		$this->retry_interval = 2;
42
		$stripe_settings      = get_option( 'woocommerce_stripe_settings', array() );
43
		$this->testmode       = ( ! empty( $stripe_settings['testmode'] ) && 'yes' === $stripe_settings['testmode'] ) ? true : false;
44
		$secret_key           = ( $this->testmode ? 'test_' : '' ) . 'webhook_secret';
45
		$this->secret         = ! empty( $stripe_settings[ $secret_key ] ) ? $stripe_settings[ $secret_key ] : false;
46
47
		add_action( 'woocommerce_api_wc_stripe', array( $this, 'check_for_webhook' ) );
48
49
		// Get/set the time we began monitoring the health of webhooks by fetching it.
50
		// This should be roughly the same as the activation time of the version of the
51
		// plugin when this code first appears.
52
		WC_Stripe_Webhook_State::get_monitoring_began_at();
53
	}
54
55
	/**
56
	 * Check incoming requests for Stripe Webhook data and process them.
57
	 *
58
	 * @since 4.0.0
59
	 * @version 4.0.0
60
	 */
61
	public function check_for_webhook() {
62
		if ( ( 'POST' !== $_SERVER['REQUEST_METHOD'] )
63
			|| ! isset( $_GET['wc-api'] )
64
			|| ( 'wc_stripe' !== $_GET['wc-api'] )
65
		) {
66
			return;
67
		}
68
69
		$request_body    = file_get_contents( 'php://input' );
70
		$request_headers = array_change_key_case( $this->get_request_headers(), CASE_UPPER );
71
72
		// Validate it to make sure it is legit.
73
		$validation_result = $this->validate_request( $request_headers, $request_body );
74
		if ( WC_Stripe_Webhook_State::VALIDATION_SUCCEEDED === $validation_result ) {
75
			$this->process_webhook( $request_body );
76
77
			$notification = json_decode( $request_body );
78
			WC_Stripe_Webhook_State::set_last_webhook_success_at( $notification->created );
79
80
			status_header( 200 );
81
			exit;
82
		} else {
83
			WC_Stripe_Logger::log( 'Incoming webhook failed validation: ' . print_r( $request_body, true ) );
84
			WC_Stripe_Webhook_State::set_last_webhook_failure_at( current_time( 'timestamp', true ) );
85
			WC_Stripe_Webhook_State::set_last_error_reason( $validation_result );
86
87
			status_header( 400 );
88
			exit;
89
		}
90
	}
91
92
	/**
93
	 * Verify the incoming webhook notification to make sure it is legit.
94
	 *
95
	 * @since 4.0.0
96
	 * @version 4.0.0
97
	 * @param string $request_headers The request headers from Stripe.
98
	 * @param string $request_body The request body from Stripe.
99
	 * @return string The validation result (e.g. self::VALIDATION_SUCCEEDED )
100
	 */
101
	public function validate_request( $request_headers = null, $request_body = null ) {
102
		if ( null === $request_headers ) {
103
			return WC_Stripe_Webhook_State::VALIDATION_FAILED_HEADERS_NULL;
104
		}
105
		if ( null === $request_body ) {
106
			return WC_Stripe_Webhook_State::VALIDATION_FAILED_BODY_NULL;
107
		}
108
109
		if ( ! empty( $request_headers['USER-AGENT'] ) && ! preg_match( '/Stripe/', $request_headers['USER-AGENT'] ) ) {
110
			return WC_Stripe_Webhook_State::VALIDATION_FAILED_USER_AGENT_INVALID;
111
		}
112
113
		if ( ! empty( $this->secret ) ) {
114
			// Check for a valid signature.
115
			$signature_format = '/^t=(?P<timestamp>\d+)(?P<signatures>(,v\d+=[a-z0-9]+){1,2})$/';
116
			if ( empty( $request_headers['STRIPE-SIGNATURE'] ) || ! preg_match( $signature_format, $request_headers['STRIPE-SIGNATURE'], $matches ) ) {
117
				return WC_Stripe_Webhook_State::VALIDATION_FAILED_SIGNATURE_INVALID;
118
			}
119
120
			// Verify the timestamp.
121
			$timestamp = intval( $matches['timestamp'] );
122
			if ( abs( $timestamp - time() ) > 5 * MINUTE_IN_SECONDS ) {
123
				return WC_Stripe_Webhook_State::VALIDATION_FAILED_TIMESTAMP_MISMATCH;
124
			}
125
126
			// Generate the expected signature.
127
			$signed_payload     = $timestamp . '.' . $request_body;
128
			$expected_signature = hash_hmac( 'sha256', $signed_payload, $this->secret );
129
130
			// Check if the expected signature is present.
131
			if ( ! preg_match( '/,v\d+=' . preg_quote( $expected_signature, '/' ) . '/', $matches['signatures'] ) ) {
132
				return WC_Stripe_Webhook_State::VALIDATION_FAILED_SIGNATURE_MISMATCH;
133
			}
134
		}
135
136
		return WC_Stripe_Webhook_State::VALIDATION_SUCCEEDED;
137
	}
138
139
	/**
140
	 * Gets the incoming request headers. Some servers are not using
141
	 * Apache and "getallheaders()" will not work so we may need to
142
	 * build our own headers.
143
	 *
144
	 * @since 4.0.0
145
	 * @version 4.0.0
146
	 */
147
	public function get_request_headers() {
148
		if ( ! function_exists( 'getallheaders' ) ) {
149
			$headers = array();
150
151
			foreach ( $_SERVER as $name => $value ) {
152
				if ( 'HTTP_' === substr( $name, 0, 5 ) ) {
153
					$headers[ str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', substr( $name, 5 ) ) ) ) ) ] = $value;
154
				}
155
			}
156
157
			return $headers;
158
		} else {
159
			return getallheaders();
160
		}
161
	}
162
163
	/**
164
	 * Process webhook payments.
165
	 * This is where we charge the source.
166
	 *
167
	 * @since 4.0.0
168
	 * @version 4.0.0
169
	 * @param object $notification
170
	 * @param bool $retry
171
	 */
172
	public function process_webhook_payment( $notification, $retry = true ) {
173
		// The following 3 payment methods are synchronous so does not need to be handle via webhook.
174
		if ( 'card' === $notification->data->object->type || 'sepa_debit' === $notification->data->object->type || 'three_d_secure' === $notification->data->object->type ) {
175
			return;
176
		}
177
178
		$order = WC_Stripe_Helper::get_order_by_source_id( $notification->data->object->id );
179
180
		if ( ! $order ) {
181
			WC_Stripe_Logger::log( 'Could not find order via source ID: ' . $notification->data->object->id );
182
			return;
183
		}
184
185
		$order_id  = $order->get_id();
186
		$source_id = $notification->data->object->id;
187
188
		$is_pending_receiver = ( 'receiver' === $notification->data->object->flow );
189
190
		try {
191
			if ( $order->has_status( array( 'processing', 'completed' ) ) ) {
192
				return;
193
			}
194
195
			if ( $order->has_status( 'on-hold' ) && ! $is_pending_receiver ) {
196
				return;
197
			}
198
199
			// Result from Stripe API request.
200
			$response = null;
201
202
			// This will throw exception if not valid.
203
			$this->validate_minimum_order_amount( $order );
204
205
			WC_Stripe_Logger::log( "Info: (Webhook) Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
206
207
			// Prep source object.
208
			$source_object           = new stdClass();
209
			$source_object->token_id = '';
210
			$source_object->customer = $this->get_stripe_customer_id( $order );
211
			$source_object->source   = $source_id;
212
213
			// Make the request.
214
			$response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source_object ), 'charges', 'POST', true );
215
			$headers  = $response['headers'];
216
			$response = $response['body'];
217
218 View Code Duplication
			if ( ! empty( $response->error ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
219
				// Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without.
220
				if ( $this->is_no_such_customer_error( $response->error ) ) {
221
					delete_user_option( $order->get_customer_id(), '_stripe_customer_id' );
222
					$order->delete_meta_data( '_stripe_customer_id' );
223
					$order->save();
224
				}
225
226
				if ( $this->is_no_such_token_error( $response->error ) && $prepared_source->token_id ) {
227
					// Source param wrong? The CARD may have been deleted on stripe's end. Remove token and show message.
228
					$wc_token = WC_Payment_Tokens::get( $prepared_source->token_id );
0 ignored issues
show
Bug introduced by
The variable $prepared_source does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
229
					$wc_token->delete();
230
					$localized_message = __( 'This card is no longer available and has been removed.', 'woocommerce-gateway-stripe' );
231
					$order->add_order_note( $localized_message );
232
					throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
233
				}
234
235
				// We want to retry.
236
				if ( $this->is_retryable_error( $response->error ) ) {
237
					if ( $retry ) {
238
						// Don't do anymore retries after this.
239
						if ( 5 <= $this->retry_interval ) {
240
241
							return $this->process_webhook_payment( $notification, false );
242
						}
243
244
						sleep( $this->retry_interval );
245
246
						$this->retry_interval++;
247
						return $this->process_webhook_payment( $notification, true );
248
					} else {
249
						$localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
250
						$order->add_order_note( $localized_message );
251
						throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
252
					}
253
				}
254
255
				$localized_messages = WC_Stripe_Helper::get_localized_messages();
256
257
				if ( 'card_error' === $response->error->type ) {
258
					$localized_message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
259
				} else {
260
					$localized_message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
261
				}
262
263
				$order->add_order_note( $localized_message );
264
265
				throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
266
			}
267
268
			// To prevent double processing the order on WC side.
269
			if ( ! $this->is_original_request( $headers ) ) {
270
				return;
271
			}
272
273
			do_action( 'wc_gateway_stripe_process_webhook_payment', $response, $order );
274
275
			$this->process_response( $response, $order );
276
277
		} catch ( WC_Stripe_Exception $e ) {
278
			WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
279
280
			do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification, $e );
281
282
			$statuses = array( 'pending', 'failed' );
283
284
			if ( $order->has_status( $statuses ) ) {
285
				$this->send_failed_order_email( $order_id );
286
			}
287
		}
288
	}
289
290
	/**
291
	 * Process webhook disputes that is created.
292
	 * This is trigger when a fraud is detected or customer processes chargeback.
293
	 * We want to put the order into on-hold and add an order note.
294
	 *
295
	 * @since 4.0.0
296
	 * @param object $notification
297
	 */
298
	public function process_webhook_dispute( $notification ) {
299
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge );
300
301
		if ( ! $order ) {
302
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->charge );
303
			return;
304
		}
305
306
		/* translators: 1) The URL to the order. */
307
		$order->update_status( 'on-hold', sprintf( __( 'A dispute was created for this order. Response is needed. Please go to your <a href="%s" title="Stripe Dashboard" target="_blank">Stripe Dashboard</a> to review this dispute.', 'woocommerce-gateway-stripe' ), $this->get_transaction_url( $order ) ) );
308
309
		do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
310
311
		$order_id = $order->get_id();
312
		$this->send_failed_order_email( $order_id );
313
	}
314
315
	/**
316
	 * Process webhook capture. This is used for an authorized only
317
	 * transaction that is later captured via Stripe not WC.
318
	 *
319
	 * @since 4.0.0
320
	 * @version 4.0.0
321
	 * @param object $notification
322
	 */
323
	public function process_webhook_capture( $notification ) {
324
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
325
326
		if ( ! $order ) {
327
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
328
			return;
329
		}
330
331
		if ( 'stripe' === $order->get_payment_method() ) {
332
			$charge   = $order->get_transaction_id();
333
			$captured = $order->get_meta( '_stripe_charge_captured', true );
334
335
			if ( $charge && 'no' === $captured ) {
336
				$order->update_meta_data( '_stripe_charge_captured', 'yes' );
337
338
				// Store other data such as fees
339
				$order->set_transaction_id( $notification->data->object->id );
340
341
				if ( isset( $notification->data->object->balance_transaction ) ) {
342
					$this->update_fees( $order, $notification->data->object->balance_transaction );
343
				}
344
345
				// Check and see if capture is partial.
346
				if ( $this->is_partial_capture( $notification ) ) {
347
					$partial_amount = $this->get_partial_amount_to_charge( $notification );
348
					$order->set_total( $partial_amount );
349
					$this->update_fees( $order, $notification->data->object->refunds->data[0]->balance_transaction );
350
					/* translators: partial captured amount */
351
					$order->add_order_note( sprintf( __( 'This charge was partially captured via Stripe Dashboard in the amount of: %s', 'woocommerce-gateway-stripe' ), $partial_amount ) );
352
				} else {
353
					$order->payment_complete( $notification->data->object->id );
354
355
					/* translators: transaction id */
356
					$order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $notification->data->object->id ) );
357
				}
358
359
				if ( is_callable( array( $order, 'save' ) ) ) {
360
					$order->save();
361
				}
362
			}
363
		}
364
	}
365
366
	/**
367
	 * Process webhook charge succeeded. This is used for payment methods
368
	 * that takes time to clear which is asynchronous. e.g. SEPA, SOFORT.
369
	 *
370
	 * @since 4.0.0
371
	 * @version 4.0.0
372
	 * @param object $notification
373
	 */
374
	public function process_webhook_charge_succeeded( $notification ) {
375
		// Ignore the notification for charges, created through PaymentIntents.
376
		if ( isset( $notification->data->object->payment_intent ) && $notification->data->object->payment_intent ) {
377
			return;
378
		}
379
380
		// The following payment methods are synchronous so does not need to be handle via webhook.
381
		if ( ( isset( $notification->data->object->source->type ) && 'card' === $notification->data->object->source->type ) || ( isset( $notification->data->object->source->type ) && 'three_d_secure' === $notification->data->object->source->type ) ) {
382
			return;
383
		}
384
385
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
386
387
		if ( ! $order ) {
388
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
389
			return;
390
		}
391
392
		if ( ! $order->has_status( 'on-hold' ) ) {
393
			return;
394
		}
395
396
		// Store other data such as fees
397
		$order->set_transaction_id( $notification->data->object->id );
398
399
		if ( isset( $notification->data->object->balance_transaction ) ) {
400
			$this->update_fees( $order, $notification->data->object->balance_transaction );
401
		}
402
403
		$order->payment_complete( $notification->data->object->id );
404
405
		/* translators: transaction id */
406
		$order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $notification->data->object->id ) );
407
408
		if ( is_callable( array( $order, 'save' ) ) ) {
409
			$order->save();
410
		}
411
	}
412
413
	/**
414
	 * Process webhook charge failed.
415
	 *
416
	 * @since 4.0.0
417
	 * @since 4.1.5 Can handle any fail payments from any methods.
418
	 * @param object $notification
419
	 */
420
	public function process_webhook_charge_failed( $notification ) {
421
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
422
423
		if ( ! $order ) {
424
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
425
			return;
426
		}
427
428
		// If order status is already in failed status don't continue.
429
		if ( $order->has_status( 'failed' ) ) {
430
			return;
431
		}
432
433
		$order->update_status( 'failed', __( 'This payment failed to clear.', 'woocommerce-gateway-stripe' ) );
434
435
		do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
436
	}
437
438
	/**
439
	 * Process webhook source canceled. This is used for payment methods
440
	 * that redirects and awaits payments from customer.
441
	 *
442
	 * @since 4.0.0
443
	 * @since 4.1.15 Add check to make sure order is processed by Stripe.
444
	 * @param object $notification
445
	 */
446
	public function process_webhook_source_canceled( $notification ) {
447
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
448
449
		// If can't find order by charge ID, try source ID.
450
		if ( ! $order ) {
451
			$order = WC_Stripe_Helper::get_order_by_source_id( $notification->data->object->id );
452
453
			if ( ! $order ) {
454
				WC_Stripe_Logger::log( 'Could not find order via charge/source ID: ' . $notification->data->object->id );
455
				return;
456
			}
457
		}
458
459
		// Don't proceed if payment method isn't Stripe.
460
		if ( 'stripe' !== $order->get_payment_method() ) {
461
			WC_Stripe_Logger::log( 'Canceled webhook abort: Order was not processed by Stripe: ' . $order->get_id() );
462
			return;
463
		}
464
465
		if ( ! $order->has_status( 'cancelled' ) ) {
466
			$order->update_status( 'cancelled', __( 'This payment has cancelled.', 'woocommerce-gateway-stripe' ) );
467
		}
468
469
		do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
470
	}
471
472
	/**
473
	 * Process webhook refund.
474
	 *
475
	 * @since 4.0.0
476
	 * @version 4.0.0
477
	 * @param object $notification
478
	 */
479
	public function process_webhook_refund( $notification ) {
480
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
481
482
		if ( ! $order ) {
483
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
484
			return;
485
		}
486
487
		$order_id = $order->get_id();
488
489
		if ( 'stripe' === $order->get_payment_method() ) {
490
			$charge    = $order->get_transaction_id();
491
			$captured  = $order->get_meta( '_stripe_charge_captured', true );
492
			$refund_id = $order->get_meta( '_stripe_refund_id', true );
493
494
			// If the refund ID matches, don't continue to prevent double refunding.
495
			if ( $notification->data->object->refunds->data[0]->id === $refund_id ) {
496
				return;
497
			}
498
499
			// Only refund captured charge.
500
			if ( $charge ) {
501
				$reason = ( isset( $captured ) && 'yes' === $captured ) ? __( 'Refunded via Stripe Dashboard', 'woocommerce-gateway-stripe' ) : __( 'Pre-Authorization Released via Stripe Dashboard', 'woocommerce-gateway-stripe' );
502
503
				// Create the refund.
504
				$refund = wc_create_refund(
505
					array(
506
						'order_id' => $order_id,
507
						'amount'   => $this->get_refund_amount( $notification ),
508
						'reason'   => $reason,
509
					)
510
				);
511
512
				if ( is_wp_error( $refund ) ) {
513
					WC_Stripe_Logger::log( $refund->get_error_message() );
514
				}
515
516
				$order->update_meta_data( '_stripe_refund_id', $notification->data->object->refunds->data[0]->id );
517
518
				$amount = wc_price( $notification->data->object->refunds->data[0]->amount / 100 );
519
520
				if ( in_array( strtolower( $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
521
					$amount = wc_price( $notification->data->object->refunds->data[0]->amount );
522
				}
523
524
				if ( isset( $notification->data->object->refunds->data[0]->balance_transaction ) ) {
525
					$this->update_fees( $order, $notification->data->object->refunds->data[0]->balance_transaction );
526
				}
527
528
				/* translators: 1) dollar amount 2) transaction id 3) refund message */
529
				$refund_message = ( isset( $captured ) && 'yes' === $captured ) ? sprintf( __( 'Refunded %1$s - Refund ID: %2$s - %3$s', 'woocommerce-gateway-stripe' ), $amount, $notification->data->object->refunds->data[0]->id, $reason ) : __( 'Pre-Authorization Released via Stripe Dashboard', 'woocommerce-gateway-stripe' );
530
531
				$order->add_order_note( $refund_message );
532
			}
533
		}
534
	}
535
536
	/**
537
	 * Process webhook reviews that are opened. i.e Radar.
538
	 *
539
	 * @since 4.0.6
540
	 * @param object $notification
541
	 */
542 View Code Duplication
	public function process_review_opened( $notification ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
543
		if ( isset( $notification->data->object->payment_intent ) ) {
544
			$order = WC_Stripe_Helper::get_order_by_intent_id( $notification->data->object->payment_intent );
545
546
			if ( ! $order ) {
547
				WC_Stripe_Logger::log( '[Review Opened] Could not find order via intent ID: ' . $notification->data->object->payment_intent );
548
				return;
549
			}
550
		} else {
551
			$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge );
552
553
			if ( ! $order ) {
554
				WC_Stripe_Logger::log( '[Review Opened] Could not find order via charge ID: ' . $notification->data->object->charge );
555
				return;
556
			}
557
		}
558
559
		/* translators: 1) The URL to the order. 2) The reason type. */
560
		$message = sprintf( __( 'A review has been opened for this order. Action is needed. Please go to your <a href="%1$s" title="Stripe Dashboard" target="_blank">Stripe Dashboard</a> to review the issue. Reason: (%2$s)', 'woocommerce-gateway-stripe' ), $this->get_transaction_url( $order ), $notification->data->object->reason );
561
562
		if ( apply_filters( 'wc_stripe_webhook_review_change_order_status', true, $order, $notification ) ) {
563
			$order->update_status( 'on-hold', $message );
564
		} else {
565
			$order->add_order_note( $message );
566
		}
567
	}
568
569
	/**
570
	 * Process webhook reviews that are closed. i.e Radar.
571
	 *
572
	 * @since 4.0.6
573
	 * @param object $notification
574
	 */
575 View Code Duplication
	public function process_review_closed( $notification ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
576
		if ( isset( $notification->data->object->payment_intent ) ) {
577
			$order = WC_Stripe_Helper::get_order_by_intent_id( $notification->data->object->payment_intent );
578
579
			if ( ! $order ) {
580
				WC_Stripe_Logger::log( '[Review Closed] Could not find order via intent ID: ' . $notification->data->object->payment_intent );
581
				return;
582
			}
583
		} else {
584
			$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge );
585
586
			if ( ! $order ) {
587
				WC_Stripe_Logger::log( '[Review Closed] Could not find order via charge ID: ' . $notification->data->object->charge );
588
				return;
589
			}
590
		}
591
592
		/* translators: 1) The reason type. */
593
		$message = sprintf( __( 'The opened review for this order is now closed. Reason: (%s)', 'woocommerce-gateway-stripe' ), $notification->data->object->reason );
594
595
		if ( $order->has_status( 'on-hold' ) ) {
596
			if ( apply_filters( 'wc_stripe_webhook_review_change_order_status', true, $order, $notification ) ) {
597
				$order->update_status( 'processing', $message );
598
			} else {
599
				$order->add_order_note( $message );
600
			}
601
		} else {
602
			$order->add_order_note( $message );
603
		}
604
	}
605
606
	/**
607
	 * Checks if capture is partial.
608
	 *
609
	 * @since 4.0.0
610
	 * @version 4.0.0
611
	 * @param object $notification
612
	 */
613
	public function is_partial_capture( $notification ) {
614
		return 0 < $notification->data->object->amount_refunded;
615
	}
616
617
	/**
618
	 * Gets the amount refunded.
619
	 *
620
	 * @since 4.0.0
621
	 * @version 4.0.0
622
	 * @param object $notification
623
	 */
624
	public function get_refund_amount( $notification ) {
625
		if ( $this->is_partial_capture( $notification ) ) {
626
			$amount = $notification->data->object->refunds->data[0]->amount / 100;
627
628 View Code Duplication
			if ( in_array( strtolower( $notification->data->object->currency ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
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...
629
				$amount = $notification->data->object->refunds->data[0]->amount;
630
			}
631
632
			return $amount;
633
		}
634
635
		return false;
636
	}
637
638
	/**
639
	 * Gets the amount we actually charge.
640
	 *
641
	 * @since 4.0.0
642
	 * @version 4.0.0
643
	 * @param object $notification
644
	 */
645
	public function get_partial_amount_to_charge( $notification ) {
646
		if ( $this->is_partial_capture( $notification ) ) {
647
			$amount = ( $notification->data->object->amount - $notification->data->object->amount_refunded ) / 100;
648
649 View Code Duplication
			if ( in_array( strtolower( $notification->data->object->currency ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
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...
650
				$amount = ( $notification->data->object->amount - $notification->data->object->amount_refunded );
651
			}
652
653
			return $amount;
654
		}
655
656
		return false;
657
	}
658
659
	public function process_payment_intent_success( $notification ) {
660
		$intent = $notification->data->object;
661
		$order = WC_Stripe_Helper::get_order_by_intent_id( $intent->id );
662
663
		if ( ! $order ) {
664
			WC_Stripe_Logger::log( 'Could not find order via intent ID: ' . $intent->id );
665
			return;
666
		}
667
668
		if ( ! $order->has_status( array( 'pending', 'failed' ) ) ) {
669
			return;
670
		}
671
672
		if ( $this->lock_order_payment( $order, $intent ) ) {
673
			return;
674
		}
675
676
		$order_id = $order->get_id();
677
		if ( 'payment_intent.succeeded' === $notification->type || 'payment_intent.amount_capturable_updated' === $notification->type ) {
678
			$charge = end( $intent->charges->data );
679
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
680
681
			do_action( 'wc_gateway_stripe_process_payment', $charge, $order );
682
683
			// Process valid response.
684
			$this->process_response( $charge, $order );
685
686 View Code Duplication
		} else {
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...
687
			$error_message = $intent->last_payment_error ? $intent->last_payment_error->message : "";
688
689
			/* translators: 1) The error message that was received from Stripe. */
690
			$order->update_status( 'failed', sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $error_message ) );
691
692
			do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
693
694
			$this->send_failed_order_email( $order_id );
695
		}
696
697
		$this->unlock_order_payment( $order );
698
	}
699
700
	public function process_setup_intent( $notification ) {
701
		$intent = $notification->data->object;
702
		$order = WC_Stripe_Helper::get_order_by_setup_intent_id( $intent->id );
703
704
		if ( ! $order ) {
705
			WC_Stripe_Logger::log( 'Could not find order via setup intent ID: ' . $intent->id );
706
			return;
707
		}
708
709
		if ( ! $order->has_status( array( 'pending', 'failed' ) ) ) {
710
			return;
711
		}
712
713
		if ( $this->lock_order_payment( $order, $intent ) ) {
714
			return;
715
		}
716
717
		$order_id = $order->get_id();
718
		if ( 'setup_intent.succeeded' === $notification->type ) {
719
			WC_Stripe_Logger::log( "Stripe SetupIntent $intent->id succeeded for order $order_id" );
720
			if ( WC_Stripe_Helper::is_pre_orders_exists() && WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) {
721
				WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
722
			} else {
723
				$order->payment_complete();
724
			}
725 View Code Duplication
		} else {
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...
726
			$error_message = $intent->last_setup_error ? $intent->last_setup_error->message : "";
727
728
			/* translators: 1) The error message that was received from Stripe. */
729
			$order->update_status( 'failed', sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $error_message ) );
730
731
			$this->send_failed_order_email( $order_id );
732
		}
733
734
		$this->unlock_order_payment( $order );
735
	}
736
737
	/**
738
	 * Processes the incoming webhook.
739
	 *
740
	 * @since 4.0.0
741
	 * @version 4.0.0
742
	 * @param string $request_body
743
	 */
744
	public function process_webhook( $request_body ) {
745
		$notification = json_decode( $request_body );
746
747
		switch ( $notification->type ) {
748
			case 'source.chargeable':
749
				$this->process_webhook_payment( $notification );
750
				break;
751
752
			case 'source.canceled':
753
				$this->process_webhook_source_canceled( $notification );
754
				break;
755
756
			case 'charge.succeeded':
757
				$this->process_webhook_charge_succeeded( $notification );
758
				break;
759
760
			case 'charge.failed':
761
				$this->process_webhook_charge_failed( $notification );
762
				break;
763
764
			case 'charge.captured':
765
				$this->process_webhook_capture( $notification );
766
				break;
767
768
			case 'charge.dispute.created':
769
				$this->process_webhook_dispute( $notification );
770
				break;
771
772
			case 'charge.refunded':
773
				$this->process_webhook_refund( $notification );
774
				break;
775
776
			case 'review.opened':
777
				$this->process_review_opened( $notification );
778
				break;
779
780
			case 'review.closed':
781
				$this->process_review_closed( $notification );
782
				break;
783
784
			case 'payment_intent.succeeded':
785
			case 'payment_intent.payment_failed':
786
			case 'payment_intent.amount_capturable_updated':
787
				$this->process_payment_intent_success( $notification );
788
				break;
789
790
			case 'setup_intent.succeeded':
791
			case 'setup_intent.setup_failed':
792
				$this->process_setup_intent( $notification );
793
794
		}
795
	}
796
}
797
798
new WC_Stripe_Webhook_Handler();
799