Completed
Pull Request — master (#1478)
by
unknown
10:44
created

WC_Stripe_Webhook_Handler   F

Complexity

Total Complexity 149

Size/Duplication

Total Lines 841
Duplicated Lines 17 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
dl 143
loc 841
rs 1.759
c 0
b 0
f 0
wmc 149
lcom 1
cbo 5

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 5
A check_for_webhook() 0 24 5
B is_valid_request() 0 34 10
A get_request_headers() 0 15 4
F process_webhook_payment() 49 117 21
A process_webhook_dispute() 0 23 3
B process_webhook_dispute_closed() 0 30 7
B process_webhook_capture() 0 42 8
B process_webhook_charge_succeeded() 0 38 11
A process_webhook_charge_failed() 0 22 4
B process_webhook_source_canceled() 0 28 6
C process_webhook_refund() 0 56 12
B process_review_opened() 28 28 6
B process_review_closed() 30 30 7
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() 16 46 8
B process_setup_intent() 14 42 9
C process_webhook() 0 56 16

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
			// 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 false;
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 dispute that is created.
278
	 * This is triggered when 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
		$order->update_meta_data( '_stripe_status_before_hold', $order->get_status() );
293
294
		/* translators: 1) The URL to the order. */
295
		$message = 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 ) );
296
		if ( ! $order->get_meta( '_stripe_status_final', false ) ) {
297
			$order->update_status( 'on-hold', $message );
298
		} else {
299
			$order->add_order_note( $message );
300
		}
301
302
		do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
303
304
		$order_id = $order->get_id();
305
		$this->send_failed_order_email( $order_id );
306
	}
307
308
	/**
309
	 * Process webhook dispute that is closed.
310
	 *
311
	 * @since 4.4.1
312
	 * @param object $notification
313
	 */
314
	public function process_webhook_dispute_closed( $notification ) {
315
		$order  = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge );
316
		$status = $notification->data->object->status;
317
318
		if ( ! $order ) {
319
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->charge );
320
			return;
321
		}
322
323
		if ( 'lost' === $status ) {
324
			$message = __( 'The dispute was lost or accepted.', 'woocommerce-gateway-stripe' );
325
		} elseif ( 'won' === $status ) {
326
			$message = __( 'The dispute was resolved in your favor.', 'woocommerce-gateway-stripe' );
327
		} elseif ( 'warning_closed' === $status ) {
328
			$message = __( 'The inquiry or retrieval was closed.', 'woocommerce-gateway-stripe' );
329
		} else {
330
			return;
331
		}
332
333
		if ( apply_filters( 'wc_stripe_webhook_dispute_change_order_status', true, $order, $notification ) ) {
334
			// Mark final so that order status is not overridden by out-of-sequence events.
335
			$order->update_meta_data( '_stripe_status_final', true );
336
337
			// Fail order if dispute is lost, or else revert to pre-dispute status.
338
			$order_status = 'lost' === $status ? 'failed' : $order->get_meta( '_stripe_status_before_hold', 'processing' );
339
			$order->update_status( $order_status, $message );
340
		} else {
341
			$order->add_order_note( $message );
342
		}
343
	}
344
345
	/**
346
	 * Process webhook capture. This is used for an authorized only
347
	 * transaction that is later captured via Stripe not WC.
348
	 *
349
	 * @since 4.0.0
350
	 * @version 4.0.0
351
	 * @param object $notification
352
	 */
353
	public function process_webhook_capture( $notification ) {
354
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
355
356
		if ( ! $order ) {
357
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
358
			return;
359
		}
360
361
		if ( 'stripe' === $order->get_payment_method() ) {
362
			$charge   = $order->get_transaction_id();
363
			$captured = $order->get_meta( '_stripe_charge_captured', true );
364
365
			if ( $charge && 'no' === $captured ) {
366
				$order->update_meta_data( '_stripe_charge_captured', 'yes' );
367
368
				// Store other data such as fees
369
				$order->set_transaction_id( $notification->data->object->id );
370
371
				if ( isset( $notification->data->object->balance_transaction ) ) {
372
					$this->update_fees( $order, $notification->data->object->balance_transaction );
373
				}
374
375
				// Check and see if capture is partial.
376
				if ( $this->is_partial_capture( $notification ) ) {
377
					$partial_amount = $this->get_partial_amount_to_charge( $notification );
378
					$order->set_total( $partial_amount );
379
					$this->update_fees( $order, $notification->data->object->refunds->data[0]->balance_transaction );
380
					/* translators: partial captured amount */
381
					$order->add_order_note( sprintf( __( 'This charge was partially captured via Stripe Dashboard in the amount of: %s', 'woocommerce-gateway-stripe' ), $partial_amount ) );
382
				} else {
383
					$order->payment_complete( $notification->data->object->id );
384
385
					/* translators: transaction id */
386
					$order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $notification->data->object->id ) );
387
				}
388
389
				if ( is_callable( array( $order, 'save' ) ) ) {
390
					$order->save();
391
				}
392
			}
393
		}
394
	}
395
396
	/**
397
	 * Process webhook charge succeeded. This is used for payment methods
398
	 * that takes time to clear which is asynchronous. e.g. SEPA, SOFORT.
399
	 *
400
	 * @since 4.0.0
401
	 * @version 4.0.0
402
	 * @param object $notification
403
	 */
404
	public function process_webhook_charge_succeeded( $notification ) {
405
		// Ignore the notification for charges, created through PaymentIntents.
406
		if ( isset( $notification->data->object->payment_intent ) && $notification->data->object->payment_intent ) {
407
			return;
408
		}
409
410
		// The following payment methods are synchronous so does not need to be handle via webhook.
411
		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 ) ) {
412
			return;
413
		}
414
415
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
416
417
		if ( ! $order ) {
418
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
419
			return;
420
		}
421
422
		if ( ! $order->has_status( 'on-hold' ) ) {
423
			return;
424
		}
425
426
		// Store other data such as fees
427
		$order->set_transaction_id( $notification->data->object->id );
428
429
		if ( isset( $notification->data->object->balance_transaction ) ) {
430
			$this->update_fees( $order, $notification->data->object->balance_transaction );
431
		}
432
433
		$order->payment_complete( $notification->data->object->id );
434
435
		/* translators: transaction id */
436
		$order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $notification->data->object->id ) );
437
438
		if ( is_callable( array( $order, 'save' ) ) ) {
439
			$order->save();
440
		}
441
	}
442
443
	/**
444
	 * Process webhook charge failed.
445
	 *
446
	 * @since 4.0.0
447
	 * @since 4.1.5 Can handle any fail payments from any methods.
448
	 * @param object $notification
449
	 */
450
	public function process_webhook_charge_failed( $notification ) {
451
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
452
453
		if ( ! $order ) {
454
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
455
			return;
456
		}
457
458
		// If order status is already in failed status don't continue.
459
		if ( $order->has_status( 'failed' ) ) {
460
			return;
461
		}
462
463
		$message = __( 'This payment failed to clear.', 'woocommerce-gateway-stripe' );
464
		if ( ! $order->get_meta( '_stripe_status_final', false ) ) {
465
			$order->update_status( 'failed', $message );
466
		} else {
467
			$order->add_order_note( $message );
468
		}
469
470
		do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
471
	}
472
473
	/**
474
	 * Process webhook source canceled. This is used for payment methods
475
	 * that redirects and awaits payments from customer.
476
	 *
477
	 * @since 4.0.0
478
	 * @since 4.1.15 Add check to make sure order is processed by Stripe.
479
	 * @param object $notification
480
	 */
481
	public function process_webhook_source_canceled( $notification ) {
482
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
483
484
		// If can't find order by charge ID, try source ID.
485
		if ( ! $order ) {
486
			$order = WC_Stripe_Helper::get_order_by_source_id( $notification->data->object->id );
487
488
			if ( ! $order ) {
489
				WC_Stripe_Logger::log( 'Could not find order via charge/source ID: ' . $notification->data->object->id );
490
				return;
491
			}
492
		}
493
494
		// Don't proceed if payment method isn't Stripe.
495
		if ( 'stripe' !== $order->get_payment_method() ) {
496
			WC_Stripe_Logger::log( 'Canceled webhook abort: Order was not processed by Stripe: ' . $order->get_id() );
497
			return;
498
		}
499
500
		$message = __( 'This payment was cancelled.', 'woocommerce-gateway-stripe' );
501
		if ( ! $order->has_status( 'cancelled' ) && ! $order->get_meta( '_stripe_status_final', false ) ) {
502
			$order->update_status( 'cancelled', $message );
503
		} else {
504
			$order->add_order_note( $message );
505
		}
506
507
		do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
508
	}
509
510
	/**
511
	 * Process webhook refund.
512
	 *
513
	 * @since 4.0.0
514
	 * @version 4.0.0
515
	 * @param object $notification
516
	 */
517
	public function process_webhook_refund( $notification ) {
518
		$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
519
520
		if ( ! $order ) {
521
			WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
522
			return;
523
		}
524
525
		$order_id = $order->get_id();
526
527
		if ( 'stripe' === $order->get_payment_method() ) {
528
			$charge    = $order->get_transaction_id();
529
			$captured  = $order->get_meta( '_stripe_charge_captured', true );
530
			$refund_id = $order->get_meta( '_stripe_refund_id', true );
531
532
			// If the refund ID matches, don't continue to prevent double refunding.
533
			if ( $notification->data->object->refunds->data[0]->id === $refund_id ) {
534
				return;
535
			}
536
537
			// Only refund captured charge.
538
			if ( $charge ) {
539
				$reason = ( isset( $captured ) && 'yes' === $captured ) ? __( 'Refunded via Stripe Dashboard', 'woocommerce-gateway-stripe' ) : __( 'Pre-Authorization Released via Stripe Dashboard', 'woocommerce-gateway-stripe' );
540
541
				// Create the refund.
542
				$refund = wc_create_refund(
543
					array(
544
						'order_id' => $order_id,
545
						'amount'   => $this->get_refund_amount( $notification ),
546
						'reason'   => $reason,
547
					)
548
				);
549
550
				if ( is_wp_error( $refund ) ) {
551
					WC_Stripe_Logger::log( $refund->get_error_message() );
552
				}
553
554
				$order->update_meta_data( '_stripe_refund_id', $notification->data->object->refunds->data[0]->id );
555
556
				$amount = wc_price( $notification->data->object->refunds->data[0]->amount / 100 );
557
558
				if ( in_array( strtolower( $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) {
559
					$amount = wc_price( $notification->data->object->refunds->data[0]->amount );
560
				}
561
562
				if ( isset( $notification->data->object->refunds->data[0]->balance_transaction ) ) {
563
					$this->update_fees( $order, $notification->data->object->refunds->data[0]->balance_transaction );
564
				}
565
566
				/* translators: 1) dollar amount 2) transaction id 3) refund message */
567
				$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' );
568
569
				$order->add_order_note( $refund_message );
570
			}
571
		}
572
	}
573
574
	/**
575
	 * Process webhook reviews that are opened. i.e Radar.
576
	 *
577
	 * @since 4.0.6
578
	 * @param object $notification
579
	 */
580 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...
581
		if ( isset( $notification->data->object->payment_intent ) ) {
582
			$order = WC_Stripe_Helper::get_order_by_intent_id( $notification->data->object->payment_intent );
583
584
			if ( ! $order ) {
585
				WC_Stripe_Logger::log( '[Review Opened] Could not find order via intent ID: ' . $notification->data->object->payment_intent );
586
				return;
587
			}
588
		} else {
589
			$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge );
590
591
			if ( ! $order ) {
592
				WC_Stripe_Logger::log( '[Review Opened] Could not find order via charge ID: ' . $notification->data->object->charge );
593
				return;
594
			}
595
		}
596
597
		$order->update_meta_data( '_stripe_status_before_hold', $order->get_status() );
598
599
		/* translators: 1) The URL to the order. 2) The reason type. */
600
		$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 );
601
602
		if ( apply_filters( 'wc_stripe_webhook_review_change_order_status', true, $order, $notification ) && ! $order->get_meta( '_stripe_status_final', false ) ) {
603
			$order->update_status( 'on-hold', $message );
604
		} else {
605
			$order->add_order_note( $message );
606
		}
607
	}
608
609
	/**
610
	 * Process webhook reviews that are closed. i.e Radar.
611
	 *
612
	 * @since 4.0.6
613
	 * @param object $notification
614
	 */
615 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...
616
		if ( isset( $notification->data->object->payment_intent ) ) {
617
			$order = WC_Stripe_Helper::get_order_by_intent_id( $notification->data->object->payment_intent );
618
619
			if ( ! $order ) {
620
				WC_Stripe_Logger::log( '[Review Closed] Could not find order via intent ID: ' . $notification->data->object->payment_intent );
621
				return;
622
			}
623
		} else {
624
			$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge );
625
626
			if ( ! $order ) {
627
				WC_Stripe_Logger::log( '[Review Closed] Could not find order via charge ID: ' . $notification->data->object->charge );
628
				return;
629
			}
630
		}
631
632
		/* translators: 1) The reason type. */
633
		$message = sprintf( __( 'The opened review for this order is now closed. Reason: (%s)', 'woocommerce-gateway-stripe' ), $notification->data->object->reason );
634
635
		if (
636
			$order->has_status( 'on-hold' ) &&
637
			apply_filters( 'wc_stripe_webhook_review_change_order_status', true, $order, $notification ) &&
638
			! $order->get_meta( '_stripe_status_final', false )
639
		) {
640
			$order->update_status( $order->get_meta( '_stripe_status_before_hold', 'processing' ), $message );
641
		} else {
642
			$order->add_order_note( $message );
643
		}
644
	}
645
646
	/**
647
	 * Checks if capture is partial.
648
	 *
649
	 * @since 4.0.0
650
	 * @version 4.0.0
651
	 * @param object $notification
652
	 */
653
	public function is_partial_capture( $notification ) {
654
		return 0 < $notification->data->object->amount_refunded;
655
	}
656
657
	/**
658
	 * Gets the amount refunded.
659
	 *
660
	 * @since 4.0.0
661
	 * @version 4.0.0
662
	 * @param object $notification
663
	 */
664
	public function get_refund_amount( $notification ) {
665
		if ( $this->is_partial_capture( $notification ) ) {
666
			$amount = $notification->data->object->refunds->data[0]->amount / 100;
667
668 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...
669
				$amount = $notification->data->object->refunds->data[0]->amount;
670
			}
671
672
			return $amount;
673
		}
674
675
		return false;
676
	}
677
678
	/**
679
	 * Gets the amount we actually charge.
680
	 *
681
	 * @since 4.0.0
682
	 * @version 4.0.0
683
	 * @param object $notification
684
	 */
685
	public function get_partial_amount_to_charge( $notification ) {
686
		if ( $this->is_partial_capture( $notification ) ) {
687
			$amount = ( $notification->data->object->amount - $notification->data->object->amount_refunded ) / 100;
688
689 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...
690
				$amount = ( $notification->data->object->amount - $notification->data->object->amount_refunded );
691
			}
692
693
			return $amount;
694
		}
695
696
		return false;
697
	}
698
699
	public function process_payment_intent_success( $notification ) {
700
		$intent = $notification->data->object;
701
		$order = WC_Stripe_Helper::get_order_by_intent_id( $intent->id );
702
703
		if ( ! $order ) {
704
			WC_Stripe_Logger::log( 'Could not find order via intent ID: ' . $intent->id );
705
			return;
706
		}
707
708
		if ( ! $order->has_status( array( 'pending', 'failed' ) ) ) {
709
			return;
710
		}
711
712
		if ( $this->lock_order_payment( $order, $intent ) ) {
713
			return;
714
		}
715
716
		$order_id = $order->get_id();
717
		if ( 'payment_intent.succeeded' === $notification->type || 'payment_intent.amount_capturable_updated' === $notification->type ) {
718
			$charge = end( $intent->charges->data );
719
			WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" );
720
721
			do_action( 'wc_gateway_stripe_process_payment', $charge, $order );
722
723
			// Process valid response.
724
			$this->process_response( $charge, $order );
725
726 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...
727
			$error_message = $intent->last_payment_error ? $intent->last_payment_error->message : "";
728
729
			/* translators: 1) The error message that was received from Stripe. */
730
			$message = sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $error_message );
731
732
			if ( ! $order->get_meta( '_stripe_status_final', false ) ) {
733
				$order->update_status( 'failed', $message );
734
			} else {
735
				$order->add_order_note( $message );
736
			}
737
738
			do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification );
739
740
			$this->send_failed_order_email( $order_id );
741
		}
742
743
		$this->unlock_order_payment( $order );
744
	}
745
746
	public function process_setup_intent( $notification ) {
747
		$intent = $notification->data->object;
748
		$order = WC_Stripe_Helper::get_order_by_setup_intent_id( $intent->id );
749
750
		if ( ! $order ) {
751
			WC_Stripe_Logger::log( 'Could not find order via setup intent ID: ' . $intent->id );
752
			return;
753
		}
754
755
		if ( ! $order->has_status( array( 'pending', 'failed' ) ) ) {
756
			return;
757
		}
758
759
		if ( $this->lock_order_payment( $order, $intent ) ) {
760
			return;
761
		}
762
763
		$order_id = $order->get_id();
764
		if ( 'setup_intent.succeeded' === $notification->type ) {
765
			WC_Stripe_Logger::log( "Stripe SetupIntent $intent->id succeeded for order $order_id" );
766
			if ( WC_Stripe_Helper::is_pre_orders_exists() && WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) {
767
				WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
768
			} else {
769
				$order->payment_complete();
770
			}
771 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...
772
			$error_message = $intent->last_setup_error ? $intent->last_setup_error->message : "";
773
774
			/* translators: 1) The error message that was received from Stripe. */
775
			$message = sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $error_message );
776
777
			if ( ! $order->get_meta( '_stripe_status_final', false ) ) {
778
				$order->update_status( 'failed', $message );
779
			} else {
780
				$order->add_order_note( $message );
781
			}
782
783
			$this->send_failed_order_email( $order_id );
784
		}
785
786
		$this->unlock_order_payment( $order );
787
	}
788
789
	/**
790
	 * Processes the incoming webhook.
791
	 *
792
	 * @since 4.0.0
793
	 * @version 4.0.0
794
	 * @param string $request_body
795
	 */
796
	public function process_webhook( $request_body ) {
797
		$notification = json_decode( $request_body );
798
799
		switch ( $notification->type ) {
800
			case 'source.chargeable':
801
				$this->process_webhook_payment( $notification );
802
				break;
803
804
			case 'source.canceled':
805
				$this->process_webhook_source_canceled( $notification );
806
				break;
807
808
			case 'charge.succeeded':
809
				$this->process_webhook_charge_succeeded( $notification );
810
				break;
811
812
			case 'charge.failed':
813
				$this->process_webhook_charge_failed( $notification );
814
				break;
815
816
			case 'charge.captured':
817
				$this->process_webhook_capture( $notification );
818
				break;
819
820
			case 'charge.dispute.created':
821
				$this->process_webhook_dispute( $notification );
822
				break;
823
824
			case 'charge.dispute.closed':
825
				$this->process_webhook_dispute_closed( $notification );
826
				break;
827
828
			case 'charge.refunded':
829
				$this->process_webhook_refund( $notification );
830
				break;
831
832
			case 'review.opened':
833
				$this->process_review_opened( $notification );
834
				break;
835
836
			case 'review.closed':
837
				$this->process_review_closed( $notification );
838
				break;
839
840
			case 'payment_intent.succeeded':
841
			case 'payment_intent.payment_failed':
842
			case 'payment_intent.amount_capturable_updated':
843
				$this->process_payment_intent_success( $notification );
844
				break;
845
846
			case 'setup_intent.succeeded':
847
			case 'setup_intent.setup_failed':
848
				$this->process_setup_intent( $notification );
849
850
		}
851
	}
852
}
853
854
new WC_Stripe_Webhook_Handler();
855