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