Completed
Push — master ( d5bb38...49ed8e )
by Marcin
01:38
created

WC_Stripe_Webhook_Handler::get_refund_amount()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 3
Ratio 23.08 %

Importance

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