process_webhook_charge_failed()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 17
rs 9.7
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
50
	/**
51
	 * Check incoming requests for Stripe Webhook data and process them.
52
	 *
53
	 * @since 4.0.0
54
	 * @version 4.0.0
55
	 */
56
	public function check_for_webhook() {
57
		if ( ( 'POST' !== $_SERVER['REQUEST_METHOD'] )
58
			|| ! isset( $_GET['wc-api'] )
59
			|| ( 'wc_stripe' !== $_GET['wc-api'] )
60
		) {
61
			return;
62
		}
63
64
		$request_body    = file_get_contents( 'php://input' );
65
		$request_headers = array_change_key_case( $this->get_request_headers(), CASE_UPPER );
66
67
		// Validate it to make sure it is legit.
68
		if ( $this->is_valid_request( $request_headers, $request_body ) ) {
69
			$this->process_webhook( $request_body );
70
			status_header( 200 );
71
			exit;
72
		} else {
73
			WC_Stripe_Logger::log( 'Incoming webhook failed validation: ' . print_r( $request_body, true ) );
74
			// A webhook endpoint must return a 2xx HTTP status code.
75
			// @see https://stripe.com/docs/webhooks/build#return-a-2xx-status-code-quickly
76
			status_header( 204 );
77
			exit;
78
		}
79
	}
80
81
	/**
82
	 * Verify the incoming webhook notification to make sure it is legit.
83
	 *
84
	 * @since 4.0.0
85
	 * @version 4.0.0
86
	 * @param string $request_headers The request headers from Stripe.
87
	 * @param string $request_body The request body from Stripe.
88
	 * @return bool
89
	 */
90
	public function is_valid_request( $request_headers = null, $request_body = null ) {
91
		if ( null === $request_headers || null === $request_body ) {
92
			return false;
93
		}
94
95
		if ( ! empty( $request_headers['USER-AGENT'] ) && ! preg_match( '/Stripe/', $request_headers['USER-AGENT'] ) ) {
96
			return false;
97
		}
98
99
		if ( ! empty( $this->secret ) ) {
100
			// Check for a valid signature.
101
			$signature_format = '/^t=(?P<timestamp>\d+)(?P<signatures>(,v\d+=[a-z0-9]+){1,2})$/';
102
			if ( empty( $request_headers['STRIPE-SIGNATURE'] ) || ! preg_match( $signature_format, $request_headers['STRIPE-SIGNATURE'], $matches ) ) {
103
				return false;
104
			}
105
106
			// Verify the timestamp.
107
			$timestamp = intval( $matches['timestamp'] );
108
			if ( abs( $timestamp - time() ) > 5 * MINUTE_IN_SECONDS ) {
109
				return;
110
			}
111
112
			// Generate the expected signature.
113
			$signed_payload     = $timestamp . '.' . $request_body;
114
			$expected_signature = hash_hmac( 'sha256', $signed_payload, $this->secret );
115
116
			// Check if the expected signature is present.
117
			if ( ! preg_match( '/,v\d+=' . preg_quote( $expected_signature, '/' ) . '/', $matches['signatures'] ) ) {
118
				return false;
119
			}
120
		}
121
122
		return true;
123
	}
124
125
	/**
126
	 * Gets the incoming request headers. Some servers are not using
127
	 * Apache and "getallheaders()" will not work so we may need to
128
	 * build our own headers.
129
	 *
130
	 * @since 4.0.0
131
	 * @version 4.0.0
132
	 */
133
	public function get_request_headers() {
134
		if ( ! function_exists( 'getallheaders' ) ) {
135
			$headers = array();
136
137
			foreach ( $_SERVER as $name => $value ) {
138
				if ( 'HTTP_' === substr( $name, 0, 5 ) ) {
139
					$headers[ str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', substr( $name, 5 ) ) ) ) ) ] = $value;
140
				}
141
			}
142
143
			return $headers;
144
		} else {
145
			return getallheaders();
146
		}
147
	}
148
149
	/**
150
	 * Process webhook payments.
151
	 * This is where we charge the source.
152
	 *
153
	 * @since 4.0.0
154
	 * @version 4.0.0
155
	 * @param object $notification
156
	 * @param bool $retry
157
	 */
158
	public function process_webhook_payment( $notification, $retry = true ) {
159
		// The following 3 payment methods are synchronous so does not need to be handle via webhook.
160
		if ( 'card' === $notification->data->object->type || 'sepa_debit' === $notification->data->object->type || 'three_d_secure' === $notification->data->object->type ) {
161
			return;
162
		}
163
164
		$order = WC_Stripe_Helper::get_order_by_source_id( $notification->data->object->id );
165
166
		if ( ! $order ) {
167
			WC_Stripe_Logger::log( 'Could not find order via source ID: ' . $notification->data->object->id );
168
			return;
169
		}
170
171
		$order_id  = $order->get_id();
172
		$source_id = $notification->data->object->id;
173
174
		$is_pending_receiver = ( 'receiver' === $notification->data->object->flow );
175
176
		try {
177
			if ( $order->has_status( array( 'processing', 'completed' ) ) ) {
178
				return;
179
			}
180
181
			if ( $order->has_status( 'on-hold' ) && ! $is_pending_receiver ) {
182
				return;
183
			}
184
185
			// Result from Stripe API request.
186
			$response = null;
187
188
			// This will throw exception if not valid.
189
			$this->validate_minimum_order_amount( $order );
190
191
			WC_Stripe_Logger::log( "Info: (Webhook) Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
192
193
			// Prep source object.
194
			$source_object           = new stdClass();
195
			$source_object->token_id = '';
196
			$source_object->customer = $this->get_stripe_customer_id( $order );
197
			$source_object->source   = $source_id;
198
199
			// Make the request.
200
			$response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source_object ), 'charges', 'POST', true );
201
			$headers  = $response['headers'];
202
			$response = $response['body'];
203
204 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...
205
				// Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without.
206
				if ( $this->is_no_such_customer_error( $response->error ) ) {
207
					delete_user_option( $order->get_customer_id(), '_stripe_customer_id' );
208
					$order->delete_meta_data( '_stripe_customer_id' );
209
					$order->save();
210
				}
211
212
				if ( $this->is_no_such_token_error( $response->error ) && $prepared_source->token_id ) {
213
					// Source param wrong? The CARD may have been deleted on stripe's end. Remove token and show message.
214
					$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...
215
					$wc_token->delete();
216
					$localized_message = __( 'This card is no longer available and has been removed.', 'woocommerce-gateway-stripe' );
217
					$order->add_order_note( $localized_message );
218
					throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
219
				}
220
221
				// We want to retry.
222
				if ( $this->is_retryable_error( $response->error ) ) {
223
					if ( $retry ) {
224
						// Don't do anymore retries after this.
225
						if ( 5 <= $this->retry_interval ) {
226
227
							return $this->process_webhook_payment( $notification, false );
228
						}
229
230
						sleep( $this->retry_interval );
231
232
						$this->retry_interval++;
233
						return $this->process_webhook_payment( $notification, true );
234
					} else {
235
						$localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
236
						$order->add_order_note( $localized_message );
237
						throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
238
					}
239
				}
240
241
				$localized_messages = WC_Stripe_Helper::get_localized_messages();
242
243
				if ( 'card_error' === $response->error->type ) {
244
					$localized_message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
245
				} else {
246
					$localized_message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
247
				}
248
249
				$order->add_order_note( $localized_message );
250
251
				throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
252
			}
253
254
			// To prevent double processing the order on WC side.
255
			if ( ! $this->is_original_request( $headers ) ) {
256
				return;
257
			}
258
259
			do_action( 'wc_gateway_stripe_process_webhook_payment', $response, $order );
260
261
			$this->process_response( $response, $order );
262
263
		} catch ( WC_Stripe_Exception $e ) {
264
			WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
265
266
			do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification, $e );
267
268
			$statuses = array( 'pending', 'failed' );
269
270
			if ( $order->has_status( $statuses ) ) {
271
				$this->send_failed_order_email( $order_id );
272
			}
273
		}
274
	}
275
276
	/**
277
	 * Process webhook disputes that is created.
278
	 * This is trigger when a fraud is detected or customer processes chargeback.
279
	 * We want to put the order into on-hold and add an order note.
280
	 *
281
	 * @since 4.0.0
282
	 * @param object $notification
283
	 */
284
	public function process_webhook_dispute( $notification ) {
285
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge );
286
287
		if ( ! $order ) {
288
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->charge );
289
			return;
290
		}
291
292
		/* translators: 1) The URL to the order. */
293
		$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 ) ) );
294
295
		do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
296
297
		$order_id = $order->get_id();
298
		$this->send_failed_order_email( $order_id );
299
	}
300
301
	/**
302
	 * Process webhook capture. This is used for an authorized only
303
	 * transaction that is later captured via Stripe not WC.
304
	 *
305
	 * @since 4.0.0
306
	 * @version 4.0.0
307
	 * @param object $notification
308
	 */
309
	public function process_webhook_capture( $notification ) {
310
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
311
312
		if ( ! $order ) {
313
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
314
			return;
315
		}
316
317
		if ( 'stripe' === $order->get_payment_method() ) {
318
			$charge   = $order->get_transaction_id();
319
			$captured = $order->get_meta( '_stripe_charge_captured', true );
320
321
			if ( $charge && 'no' === $captured ) {
322
				$order->update_meta_data( '_stripe_charge_captured', 'yes' );
323
324
				// Store other data such as fees
325
				$order->set_transaction_id( $notification->data->object->id );
326
327
				if ( isset( $notification->data->object->balance_transaction ) ) {
328
					$this->update_fees( $order, $notification->data->object->balance_transaction );
329
				}
330
331
				// Check and see if capture is partial.
332
				if ( $this->is_partial_capture( $notification ) ) {
333
					$partial_amount = $this->get_partial_amount_to_charge( $notification );
334
					$order->set_total( $partial_amount );
335
					$this->update_fees( $order, $notification->data->object->refunds->data[0]->balance_transaction );
336
					/* translators: partial captured amount */
337
					$order->add_order_note( sprintf( __( 'This charge was partially captured via Stripe Dashboard in the amount of: %s', 'woocommerce-gateway-stripe' ), $partial_amount ) );
338
				} else {
339
					$order->payment_complete( $notification->data->object->id );
340
341
					/* translators: transaction id */
342
					$order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $notification->data->object->id ) );
343
				}
344
345
				if ( is_callable( array( $order, 'save' ) ) ) {
346
					$order->save();
347
				}
348
			}
349
		}
350
	}
351
352
	/**
353
	 * Process webhook charge succeeded. This is used for payment methods
354
	 * that takes time to clear which is asynchronous. e.g. SEPA, SOFORT.
355
	 *
356
	 * @since 4.0.0
357
	 * @version 4.0.0
358
	 * @param object $notification
359
	 */
360
	public function process_webhook_charge_succeeded( $notification ) {
361
		// Ignore the notification for charges, created through PaymentIntents.
362
		if ( isset( $notification->data->object->payment_intent ) && $notification->data->object->payment_intent ) {
363
			return;
364
		}
365
366
		// The following payment methods are synchronous so does not need to be handle via webhook.
367
		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 ) ) {
368
			return;
369
		}
370
371
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
372
373
		if ( ! $order ) {
374
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
375
			return;
376
		}
377
378
		if ( ! $order->has_status( 'on-hold' ) ) {
379
			return;
380
		}
381
382
		// Store other data such as fees
383
		$order->set_transaction_id( $notification->data->object->id );
384
385
		if ( isset( $notification->data->object->balance_transaction ) ) {
386
			$this->update_fees( $order, $notification->data->object->balance_transaction );
387
		}
388
389
		$order->payment_complete( $notification->data->object->id );
390
391
		/* translators: transaction id */
392
		$order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $notification->data->object->id ) );
393
394
		if ( is_callable( array( $order, 'save' ) ) ) {
395
			$order->save();
396
		}
397
	}
398
399
	/**
400
	 * Process webhook charge failed.
401
	 *
402
	 * @since 4.0.0
403
	 * @since 4.1.5 Can handle any fail payments from any methods.
404
	 * @param object $notification
405
	 */
406
	public function process_webhook_charge_failed( $notification ) {
407
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
408
409
		if ( ! $order ) {
410
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
411
			return;
412
		}
413
414
		// If order status is already in failed status don't continue.
415
		if ( $order->has_status( 'failed' ) ) {
416
			return;
417
		}
418
419
		$order->update_status( 'failed', __( 'This payment failed to clear.', 'woocommerce-gateway-stripe' ) );
420
421
		do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
422
	}
423
424
	/**
425
	 * Process webhook source canceled. This is used for payment methods
426
	 * that redirects and awaits payments from customer.
427
	 *
428
	 * @since 4.0.0
429
	 * @since 4.1.15 Add check to make sure order is processed by Stripe.
430
	 * @param object $notification
431
	 */
432
	public function process_webhook_source_canceled( $notification ) {
433
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
434
435
		// If can't find order by charge ID, try source ID.
436
		if ( ! $order ) {
437
			$order = WC_Stripe_Helper::get_order_by_source_id( $notification->data->object->id );
438
439
			if ( ! $order ) {
440
				WC_Stripe_Logger::log( 'Could not find order via charge/source ID: ' . $notification->data->object->id );
441
				return;
442
			}
443
		}
444
445
		// Don't proceed if payment method isn't Stripe.
446
		if ( 'stripe' !== $order->get_payment_method() ) {
447
			WC_Stripe_Logger::log( 'Canceled webhook abort: Order was not processed by Stripe: ' . $order->get_id() );
448
			return;
449
		}
450
451
		if ( ! $order->has_status( 'cancelled' ) ) {
452
			$order->update_status( 'cancelled', __( 'This payment has cancelled.', 'woocommerce-gateway-stripe' ) );
453
		}
454
455
		do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
456
	}
457
458
	/**
459
	 * Process webhook refund.
460
	 *
461
	 * @since 4.0.0
462
	 * @version 4.0.0
463
	 * @param object $notification
464
	 */
465
	public function process_webhook_refund( $notification ) {
466
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
467
468
		if ( ! $order ) {
469
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
470
			return;
471
		}
472
473
		$order_id = $order->get_id();
474
475
		if ( 'stripe' === $order->get_payment_method() ) {
476
			$charge    = $order->get_transaction_id();
477
			$captured  = $order->get_meta( '_stripe_charge_captured', true );
478
			$refund_id = $order->get_meta( '_stripe_refund_id', true );
479
480
			// If the refund ID matches, don't continue to prevent double refunding.
481
			if ( $notification->data->object->refunds->data[0]->id === $refund_id ) {
482
				return;
483
			}
484
485
			// Only refund captured charge.
486
			if ( $charge ) {
487
				$reason = ( isset( $captured ) && 'yes' === $captured ) ? __( 'Refunded via Stripe Dashboard', 'woocommerce-gateway-stripe' ) : __( 'Pre-Authorization Released via Stripe Dashboard', 'woocommerce-gateway-stripe' );
488
489
				// Create the refund.
490
				$refund = wc_create_refund(
491
					array(
492
						'order_id' => $order_id,
493
						'amount'   => $this->get_refund_amount( $notification ),
494
						'reason'   => $reason,
495
					)
496
				);
497
498
				if ( is_wp_error( $refund ) ) {
499
					WC_Stripe_Logger::log( $refund->get_error_message() );
500
				}
501
502
				$order->update_meta_data( '_stripe_refund_id', $notification->data->object->refunds->data[0]->id );
503
504
				$amount = wc_price( $notification->data->object->refunds->data[0]->amount / 100 );
505
506
				if ( in_array( strtolower( $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
507
					$amount = wc_price( $notification->data->object->refunds->data[0]->amount );
508
				}
509
510
				if ( isset( $notification->data->object->refunds->data[0]->balance_transaction ) ) {
511
					$this->update_fees( $order, $notification->data->object->refunds->data[0]->balance_transaction );
512
				}
513
514
				/* translators: 1) dollar amount 2) transaction id 3) refund message */
515
				$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' );
516
517
				$order->add_order_note( $refund_message );
518
			}
519
		}
520
	}
521
522
	/**
523
	 * Process webhook reviews that are opened. i.e Radar.
524
	 *
525
	 * @since 4.0.6
526
	 * @param object $notification
527
	 */
528 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...
529
		if ( isset( $notification->data->object->payment_intent ) ) {
530
			$order = WC_Stripe_Helper::get_order_by_intent_id( $notification->data->object->payment_intent );
531
532
			if ( ! $order ) {
533
				WC_Stripe_Logger::log( '[Review Opened] Could not find order via intent ID: ' . $notification->data->object->payment_intent );
534
				return;
535
			}
536
		} else {
537
			$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge );
538
539
			if ( ! $order ) {
540
				WC_Stripe_Logger::log( '[Review Opened] Could not find order via charge ID: ' . $notification->data->object->charge );
541
				return;
542
			}
543
		}
544
545
		/* translators: 1) The URL to the order. 2) The reason type. */
546
		$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 );
547
548
		if ( apply_filters( 'wc_stripe_webhook_review_change_order_status', true, $order, $notification ) ) {
549
			$order->update_status( 'on-hold', $message );
550
		} else {
551
			$order->add_order_note( $message );
552
		}
553
	}
554
555
	/**
556
	 * Process webhook reviews that are closed. i.e Radar.
557
	 *
558
	 * @since 4.0.6
559
	 * @param object $notification
560
	 */
561 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...
562
		if ( isset( $notification->data->object->payment_intent ) ) {
563
			$order = WC_Stripe_Helper::get_order_by_intent_id( $notification->data->object->payment_intent );
564
565
			if ( ! $order ) {
566
				WC_Stripe_Logger::log( '[Review Closed] Could not find order via intent ID: ' . $notification->data->object->payment_intent );
567
				return;
568
			}
569
		} else {
570
			$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge );
571
572
			if ( ! $order ) {
573
				WC_Stripe_Logger::log( '[Review Closed] Could not find order via charge ID: ' . $notification->data->object->charge );
574
				return;
575
			}
576
		}
577
578
		/* translators: 1) The reason type. */
579
		$message = sprintf( __( 'The opened review for this order is now closed. Reason: (%s)', 'woocommerce-gateway-stripe' ), $notification->data->object->reason );
580
581
		if ( $order->has_status( 'on-hold' ) ) {
582
			if ( apply_filters( 'wc_stripe_webhook_review_change_order_status', true, $order, $notification ) ) {
583
				$order->update_status( 'processing', $message );
584
			} else {
585
				$order->add_order_note( $message );
586
			}
587
		} else {
588
			$order->add_order_note( $message );
589
		}
590
	}
591
592
	/**
593
	 * Checks if capture is partial.
594
	 *
595
	 * @since 4.0.0
596
	 * @version 4.0.0
597
	 * @param object $notification
598
	 */
599
	public function is_partial_capture( $notification ) {
600
		return 0 < $notification->data->object->amount_refunded;
601
	}
602
603
	/**
604
	 * Gets the amount refunded.
605
	 *
606
	 * @since 4.0.0
607
	 * @version 4.0.0
608
	 * @param object $notification
609
	 */
610
	public function get_refund_amount( $notification ) {
611
		if ( $this->is_partial_capture( $notification ) ) {
612
			$amount = $notification->data->object->refunds->data[0]->amount / 100;
613
614 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...
615
				$amount = $notification->data->object->refunds->data[0]->amount;
616
			}
617
618
			return $amount;
619
		}
620
621
		return false;
622
	}
623
624
	/**
625
	 * Gets the amount we actually charge.
626
	 *
627
	 * @since 4.0.0
628
	 * @version 4.0.0
629
	 * @param object $notification
630
	 */
631
	public function get_partial_amount_to_charge( $notification ) {
632
		if ( $this->is_partial_capture( $notification ) ) {
633
			$amount = ( $notification->data->object->amount - $notification->data->object->amount_refunded ) / 100;
634
635 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...
636
				$amount = ( $notification->data->object->amount - $notification->data->object->amount_refunded );
637
			}
638
639
			return $amount;
640
		}
641
642
		return false;
643
	}
644
645
	public function process_payment_intent_success( $notification ) {
646
		$intent = $notification->data->object;
647
		$order = WC_Stripe_Helper::get_order_by_intent_id( $intent->id );
648
649
		if ( ! $order ) {
650
			WC_Stripe_Logger::log( 'Could not find order via intent ID: ' . $intent->id );
651
			return;
652
		}
653
654
		if ( ! $order->has_status( array( 'pending', 'failed' ) ) ) {
655
			return;
656
		}
657
658
		if ( $this->lock_order_payment( $order, $intent ) ) {
659
			return;
660
		}
661
662
		$order_id = $order->get_id();
663
		if ( 'payment_intent.succeeded' === $notification->type || 'payment_intent.amount_capturable_updated' === $notification->type ) {
664
			$charge = end( $intent->charges->data );
665
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
666
667
			do_action( 'wc_gateway_stripe_process_payment', $charge, $order );
668
669
			// Process valid response.
670
			$this->process_response( $charge, $order );
671
672 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...
673
			$error_message = $intent->last_payment_error ? $intent->last_payment_error->message : "";
674
675
			/* translators: 1) The error message that was received from Stripe. */
676
			$order->update_status( 'failed', sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $error_message ) );
677
678
			do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
679
680
			$this->send_failed_order_email( $order_id );
681
		}
682
683
		$this->unlock_order_payment( $order );
684
	}
685
686
	public function process_setup_intent( $notification ) {
687
		$intent = $notification->data->object;
688
		$order = WC_Stripe_Helper::get_order_by_setup_intent_id( $intent->id );
689
690
		if ( ! $order ) {
691
			WC_Stripe_Logger::log( 'Could not find order via setup intent ID: ' . $intent->id );
692
			return;
693
		}
694
695
		if ( ! $order->has_status( array( 'pending', 'failed' ) ) ) {
696
			return;
697
		}
698
699
		if ( $this->lock_order_payment( $order, $intent ) ) {
700
			return;
701
		}
702
703
		$order_id = $order->get_id();
704
		if ( 'setup_intent.succeeded' === $notification->type ) {
705
			WC_Stripe_Logger::log( "Stripe SetupIntent $intent->id succeeded for order $order_id" );
706
			if ( WC_Stripe_Helper::is_pre_orders_exists() && WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) {
707
				WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
708
			} else {
709
				$order->payment_complete();
710
			}
711 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...
712
			$error_message = $intent->last_setup_error ? $intent->last_setup_error->message : "";
713
714
			/* translators: 1) The error message that was received from Stripe. */
715
			$order->update_status( 'failed', sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $error_message ) );
716
717
			$this->send_failed_order_email( $order_id );
718
		}
719
720
		$this->unlock_order_payment( $order );
721
	}
722
723
	/**
724
	 * Processes the incoming webhook.
725
	 *
726
	 * @since 4.0.0
727
	 * @version 4.0.0
728
	 * @param string $request_body
729
	 */
730
	public function process_webhook( $request_body ) {
731
		$notification = json_decode( $request_body );
732
733
		switch ( $notification->type ) {
734
			case 'source.chargeable':
735
				$this->process_webhook_payment( $notification );
736
				break;
737
738
			case 'source.canceled':
739
				$this->process_webhook_source_canceled( $notification );
740
				break;
741
742
			case 'charge.succeeded':
743
				$this->process_webhook_charge_succeeded( $notification );
744
				break;
745
746
			case 'charge.failed':
747
				$this->process_webhook_charge_failed( $notification );
748
				break;
749
750
			case 'charge.captured':
751
				$this->process_webhook_capture( $notification );
752
				break;
753
754
			case 'charge.dispute.created':
755
				$this->process_webhook_dispute( $notification );
756
				break;
757
758
			case 'charge.refunded':
759
				$this->process_webhook_refund( $notification );
760
				break;
761
762
			case 'review.opened':
763
				$this->process_review_opened( $notification );
764
				break;
765
766
			case 'review.closed':
767
				$this->process_review_closed( $notification );
768
				break;
769
770
			case 'payment_intent.succeeded':
771
			case 'payment_intent.payment_failed':
772
			case 'payment_intent.amount_capturable_updated':
773
				$this->process_payment_intent_success( $notification );
774
				break;
775
776
			case 'setup_intent.succeeded':
777
			case 'setup_intent.setup_failed':
778
				$this->process_setup_intent( $notification );
779
780
		}
781
	}
782
}
783
784
new WC_Stripe_Webhook_Handler();
785