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

WC_Stripe_Webhook_Handler   F

Complexity

Total Complexity 158

Size/Duplication

Total Lines 780
Duplicated Lines 15.26 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
dl 119
loc 780
rs 1.82
c 0
b 0
f 0
wmc 158
lcom 1
cbo 5

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 5
A check_for_webhook() 0 22 5
B is_valid_request() 0 34 10
A get_request_headers() 0 15 4
F process_webhook_payment() 54 122 24
A process_webhook_dispute() 0 16 3
D process_webhook_capture() 0 44 14
C process_webhook_charge_succeeded() 0 40 13
A process_webhook_charge_failed() 0 19 4
A process_webhook_source_canceled() 0 25 5
F process_webhook_refund() 0 56 19
A process_review_opened() 21 26 5
B process_review_closed() 20 30 6
A is_partial_capture() 0 3 1
A get_refund_amount() 3 13 3
A get_partial_amount_to_charge() 3 13 3
B process_payment_intent_success() 10 40 9
B process_setup_intent() 8 36 10
C process_webhook() 0 52 15

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WC_Stripe_Webhook_Handler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WC_Stripe_Webhook_Handler, and based on these observations, apply Extract Interface, too.

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