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
|
|
|
* Constructor. |
29
|
|
|
* |
30
|
|
|
* @since 4.0.0 |
31
|
|
|
* @version 4.0.0 |
32
|
|
|
*/ |
33
|
|
|
public function __construct() { |
34
|
|
|
$this->retry_interval = 2; |
35
|
|
|
$stripe_settings = get_option( 'woocommerce_stripe_settings', array() ); |
36
|
|
|
$this->testmode = ( ! empty( $stripe_settings['testmode'] ) && 'yes' === $stripe_settings['testmode'] ) ? true : false; |
37
|
|
|
add_action( 'woocommerce_api_wc_stripe', array( $this, 'check_for_webhook' ) ); |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Check incoming requests for Stripe Webhook data and process them. |
42
|
|
|
* |
43
|
|
|
* @since 4.0.0 |
44
|
|
|
* @version 4.0.0 |
45
|
|
|
*/ |
46
|
|
|
public function check_for_webhook() { |
47
|
|
|
if ( ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) |
48
|
|
|
|| ! isset( $_GET['wc-api'] ) |
49
|
|
|
|| ( 'wc_stripe' !== $_GET['wc-api'] ) |
50
|
|
|
) { |
51
|
|
|
return; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
$request_body = file_get_contents( 'php://input' ); |
55
|
|
|
$request_headers = array_change_key_case( $this->get_request_headers(), CASE_UPPER ); |
56
|
|
|
|
57
|
|
|
// Validate it to make sure it is legit. |
58
|
|
|
if ( $this->is_valid_request( $request_headers, $request_body ) ) { |
59
|
|
|
$this->process_webhook( $request_body ); |
60
|
|
|
status_header( 200 ); |
61
|
|
|
exit; |
62
|
|
|
} else { |
63
|
|
|
WC_Stripe_Logger::log( 'Incoming webhook failed validation: ' . print_r( $request_body, true ) ); |
64
|
|
|
status_header( 400 ); |
65
|
|
|
exit; |
66
|
|
|
} |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Verify the incoming webhook notification to make sure it is legit. |
71
|
|
|
* |
72
|
|
|
* @since 4.0.0 |
73
|
|
|
* @version 4.0.0 |
74
|
|
|
* @todo Implement proper webhook signature validation. Ref https://stripe.com/docs/webhooks#signatures |
75
|
|
|
* @param string $request_headers The request headers from Stripe. |
76
|
|
|
* @param string $request_body The request body from Stripe. |
77
|
|
|
* @return bool |
78
|
|
|
*/ |
79
|
|
|
public function is_valid_request( $request_headers = null, $request_body = null ) { |
80
|
|
|
if ( null === $request_headers || null === $request_body ) { |
81
|
|
|
return false; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
if ( ! empty( $request_headers['USER-AGENT'] ) && ! preg_match( '/Stripe/', $request_headers['USER-AGENT'] ) ) { |
85
|
|
|
return false; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
return true; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Gets the incoming request headers. Some servers are not using |
93
|
|
|
* Apache and "getallheaders()" will not work so we may need to |
94
|
|
|
* build our own headers. |
95
|
|
|
* |
96
|
|
|
* @since 4.0.0 |
97
|
|
|
* @version 4.0.0 |
98
|
|
|
*/ |
99
|
|
|
public function get_request_headers() { |
100
|
|
|
if ( ! function_exists( 'getallheaders' ) ) { |
101
|
|
|
$headers = []; |
102
|
|
|
foreach ( $_SERVER as $name => $value ) { |
103
|
|
|
if ( 'HTTP_' === substr( $name, 0, 5 ) ) { |
104
|
|
|
$headers[ str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', substr( $name, 5 ) ) ) ) ) ] = $value; |
105
|
|
|
} |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
return $headers; |
109
|
|
|
} else { |
110
|
|
|
return getallheaders(); |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Process webhook payments. |
116
|
|
|
* This is where we charge the source. |
117
|
|
|
* |
118
|
|
|
* @since 4.0.0 |
119
|
|
|
* @version 4.0.0 |
120
|
|
|
* @param object $notification |
121
|
|
|
* @param bool $retry |
122
|
|
|
*/ |
123
|
|
|
public function process_webhook_payment( $notification, $retry = true ) { |
124
|
|
|
// The following 2 payment methods are synchronous so does not need to be handle via webhook. |
125
|
|
|
if ( 'card' === $notification->data->object->type || 'sepa_debit' === $notification->data->object->type ) { |
126
|
|
|
return; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
$order = WC_Stripe_Helper::get_order_by_source_id( $notification->data->object->id ); |
130
|
|
|
|
131
|
|
|
if ( ! $order ) { |
132
|
|
|
WC_Stripe_Logger::log( 'Could not find order via source ID: ' . $notification->data->object->id ); |
133
|
|
|
return; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id(); |
137
|
|
|
$source_id = $notification->data->object->id; |
138
|
|
|
|
139
|
|
|
$is_pending_receiver = ( 'receiver' === $notification->data->object->flow ); |
140
|
|
|
|
141
|
|
|
try { |
142
|
|
|
if ( 'processing' === $order->get_status() || 'completed' === $order->get_status() ) { |
143
|
|
|
return; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
if ( 'on-hold' === $order->get_status() && ! $is_pending_receiver ) { |
147
|
|
|
return; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
// Result from Stripe API request. |
151
|
|
|
$response = null; |
152
|
|
|
|
153
|
|
|
// This will throw exception if not valid. |
154
|
|
|
$this->validate_minimum_order_amount( $order ); |
155
|
|
|
|
156
|
|
|
WC_Stripe_Logger::log( "Info: (Webhook) Begin processing payment for order $order_id for the amount of {$order->get_total()}" ); |
157
|
|
|
|
158
|
|
|
// Prep source object. |
159
|
|
|
$source_object = new stdClass(); |
160
|
|
|
$source_object->token_id = ''; |
161
|
|
|
$source_object->customer = $this->get_stripe_customer_id( $order ); |
162
|
|
|
$source_object->source = $source_id; |
163
|
|
|
|
164
|
|
|
// Make the request. |
165
|
|
|
$response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source_object ) ); |
166
|
|
|
|
167
|
|
View Code Duplication |
if ( ! empty( $response->error ) ) { |
|
|
|
|
168
|
|
|
// If it is an API error such connection or server, let's retry. |
169
|
|
|
if ( 'api_connection_error' === $response->error->type || 'api_error' === $response->error->type ) { |
170
|
|
|
if ( $retry ) { |
171
|
|
|
sleep( 5 ); |
172
|
|
|
return $this->process_webhook_payment( $notification, false ); |
173
|
|
|
} else { |
174
|
|
|
$localized_message = 'API connection error and retries exhausted.'; |
175
|
|
|
$order->add_order_note( $localized_message ); |
176
|
|
|
throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message ); |
177
|
|
|
} |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
// We want to retry. |
181
|
|
|
if ( $this->is_retryable_error( $response->error ) ) { |
182
|
|
|
if ( $retry ) { |
183
|
|
|
// Don't do anymore retries after this. |
184
|
|
|
if ( 5 <= $this->retry_interval ) { |
185
|
|
|
|
186
|
|
|
return $this->process_webhook_payment( $notification, false ); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
sleep( $this->retry_interval ); |
190
|
|
|
|
191
|
|
|
$this->retry_interval++; |
192
|
|
|
return $this->process_webhook_payment( $notification, true ); |
193
|
|
|
} else { |
194
|
|
|
$localized_message = __( 'On going requests error and retries exhausted.', 'woocommerce-gateway-stripe' ); |
195
|
|
|
$order->add_order_note( $localized_message ); |
196
|
|
|
throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message ); |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
// Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without. |
201
|
|
|
if ( preg_match( '/No such customer/i', $response->error->message ) && $retry ) { |
202
|
|
|
if ( WC_Stripe_Helper::is_pre_30() ) { |
203
|
|
|
delete_user_meta( $order->customer_user, '_stripe_customer_id' ); |
204
|
|
|
delete_post_meta( $order_id, '_stripe_customer_id' ); |
205
|
|
|
} else { |
206
|
|
|
delete_user_meta( $order->get_customer_id(), '_stripe_customer_id' ); |
207
|
|
|
$order->delete_meta_data( '_stripe_customer_id' ); |
208
|
|
|
$order->save(); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
return $this->process_webhook_payment( $notification, false ); |
212
|
|
|
|
213
|
|
|
} elseif ( preg_match( '/No such token/i', $response->error->message ) && $source_object->token_id ) { |
214
|
|
|
// Source param wrong? The CARD may have been deleted on stripe's end. Remove token and show message. |
215
|
|
|
$wc_token = WC_Payment_Tokens::get( $source_object->token_id ); |
216
|
|
|
$wc_token->delete(); |
217
|
|
|
$message = __( 'This card is no longer available and has been removed.', 'woocommerce-gateway-stripe' ); |
218
|
|
|
$order->add_order_note( $message ); |
219
|
|
|
throw new WC_Stripe_Exception( print_r( $response, true ), $message ); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
$localized_messages = WC_Stripe_Helper::get_localized_messages(); |
223
|
|
|
|
224
|
|
|
if ( 'card_error' === $response->error->type ) { |
225
|
|
|
$localized_message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message; |
226
|
|
|
} else { |
227
|
|
|
$localized_message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
$order->add_order_note( $localized_message ); |
231
|
|
|
|
232
|
|
|
throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message ); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
do_action( 'wc_gateway_stripe_process_webhook_payment', $response, $order ); |
236
|
|
|
|
237
|
|
|
$this->process_response( $response, $order ); |
238
|
|
|
|
239
|
|
|
} catch ( WC_Stripe_Exception $e ) { |
240
|
|
|
WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() ); |
241
|
|
|
|
242
|
|
|
do_action( 'wc_gateway_stripe_process_webhook_payment_error', $e, $order ); |
243
|
|
|
|
244
|
|
|
$statuses = array( 'pending', 'failed' ); |
245
|
|
|
|
246
|
|
|
if ( $order->has_status( $statuses ) ) { |
247
|
|
|
$this->send_failed_order_email( $order_id ); |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Process webhook disputes that is created. |
254
|
|
|
* This is trigger when a fraud is detected or customer processes chargeback. |
255
|
|
|
* We want to put the order into on-hold and add an order note. |
256
|
|
|
* |
257
|
|
|
* @since 4.0.0 |
258
|
|
|
* @param object $notification |
259
|
|
|
*/ |
260
|
|
|
public function process_webhook_dispute( $notification ) { |
261
|
|
|
$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge ); |
262
|
|
|
|
263
|
|
|
if ( ! $order ) { |
264
|
|
|
WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->charge ); |
265
|
|
|
return; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/* translators: 1) The URL to the order. */ |
269
|
|
|
$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 ) ) ); |
270
|
|
|
|
271
|
|
|
do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification ); |
272
|
|
|
|
273
|
|
|
$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id(); |
274
|
|
|
$this->send_failed_order_email( $order_id ); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Process webhook capture. This is used for an authorized only |
279
|
|
|
* transaction that is later captured via Stripe not WC. |
280
|
|
|
* |
281
|
|
|
* @since 4.0.0 |
282
|
|
|
* @version 4.0.0 |
283
|
|
|
* @param object $notification |
284
|
|
|
*/ |
285
|
|
|
public function process_webhook_capture( $notification ) { |
286
|
|
|
$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id ); |
287
|
|
|
|
288
|
|
|
if ( ! $order ) { |
289
|
|
|
WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id ); |
290
|
|
|
return; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id(); |
294
|
|
|
|
295
|
|
|
if ( 'stripe' === ( WC_Stripe_Helper::is_pre_30() ? $order->payment_method : $order->get_payment_method() ) ) { |
296
|
|
|
$charge = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_transaction_id', true ) : $order->get_transaction_id(); |
297
|
|
|
$captured = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_stripe_charge_captured', true ) : $order->get_meta( '_stripe_charge_captured', true ); |
298
|
|
|
|
299
|
|
|
if ( $charge && 'no' === $captured ) { |
300
|
|
|
WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_stripe_charge_captured', 'yes' ) : $order->update_meta_data( '_stripe_charge_captured', 'yes' ); |
301
|
|
|
|
302
|
|
|
// Store other data such as fees |
303
|
|
|
WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_transaction_id', $notification->data->object->id ) : $order->set_transaction_id( $notification->data->object->id ); |
304
|
|
|
|
305
|
|
|
if ( isset( $notification->data->object->balance_transaction ) ) { |
306
|
|
|
$this->update_fees( $order, $notification->data->object->balance_transaction ); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
if ( is_callable( array( $order, 'save' ) ) ) { |
310
|
|
|
$order->save(); |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/* translators: transaction id */ |
314
|
|
|
$order->update_status( $order->needs_processing() ? 'processing' : 'completed', sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $notification->data->object->id ) ); |
315
|
|
|
|
316
|
|
|
// Check and see if capture is partial. |
317
|
|
|
if ( $this->is_partial_capture( $notification ) ) { |
318
|
|
|
$order->set_total( $this->get_partial_amount_to_charge( $notification ) ); |
319
|
|
|
$order->add_order_note( __( 'This charge was partially captured via Stripe Dashboard', 'woocommerce-gateway-stripe' ) ); |
320
|
|
|
$order->save(); |
321
|
|
|
} |
322
|
|
|
} |
323
|
|
|
} |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* Process webhook charge succeeded. This is used for payment methods |
328
|
|
|
* that takes time to clear which is asynchronous. e.g. SEPA, SOFORT. |
329
|
|
|
* |
330
|
|
|
* @since 4.0.0 |
331
|
|
|
* @version 4.0.0 |
332
|
|
|
* @param object $notification |
333
|
|
|
*/ |
334
|
|
|
public function process_webhook_charge_succeeded( $notification ) { |
335
|
|
|
// The following payment methods are synchronous so does not need to be handle via webhook. |
336
|
|
|
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 ) ) { |
337
|
|
|
return; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id ); |
341
|
|
|
|
342
|
|
|
if ( ! $order ) { |
343
|
|
|
WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id ); |
344
|
|
|
return; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id(); |
348
|
|
|
|
349
|
|
|
if ( 'on-hold' !== $order->get_status() ) { |
350
|
|
|
return; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
// Store other data such as fees |
354
|
|
|
WC_Stripe_Helper::is_pre_30() ? update_post_meta( $order_id, '_transaction_id', $notification->data->object->id ) : $order->set_transaction_id( $notification->data->object->id ); |
355
|
|
|
|
356
|
|
|
if ( isset( $notification->data->object->balance_transaction ) ) { |
357
|
|
|
$this->update_fees( $order, $notification->data->object->balance_transaction ); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
if ( is_callable( array( $order, 'save' ) ) ) { |
361
|
|
|
$order->save(); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
/* translators: transaction id */ |
365
|
|
|
$order->update_status( $order->needs_processing() ? 'processing' : 'completed', sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $notification->data->object->id ) ); |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* Process webhook charge failed. This is used for payment methods |
370
|
|
|
* that takes time to clear which is asynchronous. e.g. SEPA, SOFORT. |
371
|
|
|
* |
372
|
|
|
* @since 4.0.0 |
373
|
|
|
* @version 4.0.0 |
374
|
|
|
* @param object $notification |
375
|
|
|
*/ |
376
|
|
View Code Duplication |
public function process_webhook_charge_failed( $notification ) { |
|
|
|
|
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_pre_30() ? $order->id : $order->get_id(); |
|
|
|
|
385
|
|
|
|
386
|
|
|
if ( 'on-hold' !== $order->get_status() ) { |
387
|
|
|
return; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
$order->update_status( 'failed', __( 'This payment failed to clear.', 'woocommerce-gateway-stripe' ) ); |
391
|
|
|
|
392
|
|
|
do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification ); |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
/** |
396
|
|
|
* Process webhook source canceled. This is used for payment methods |
397
|
|
|
* that redirects and awaits payments from customer. |
398
|
|
|
* |
399
|
|
|
* @since 4.0.0 |
400
|
|
|
* @version 4.0.0 |
401
|
|
|
* @param object $notification |
402
|
|
|
*/ |
403
|
|
View Code Duplication |
public function process_webhook_source_canceled( $notification ) { |
|
|
|
|
404
|
|
|
$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id ); |
405
|
|
|
|
406
|
|
|
if ( ! $order ) { |
407
|
|
|
WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id ); |
408
|
|
|
return; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id(); |
|
|
|
|
412
|
|
|
|
413
|
|
|
if ( 'on-hold' !== $order->get_status() || 'cancelled' !== $order->get_status() ) { |
414
|
|
|
return; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
$order->update_status( 'cancelled', __( 'This payment has cancelled.', 'woocommerce-gateway-stripe' ) ); |
418
|
|
|
|
419
|
|
|
do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification ); |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* Process webhook refund. |
424
|
|
|
* Note currently only support 1 time refund. |
425
|
|
|
* |
426
|
|
|
* @since 4.0.0 |
427
|
|
|
* @version 4.0.0 |
428
|
|
|
* @param object $notification |
429
|
|
|
*/ |
430
|
|
|
public function process_webhook_refund( $notification ) { |
431
|
|
|
$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id ); |
432
|
|
|
|
433
|
|
|
if ( ! $order ) { |
434
|
|
|
WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id ); |
435
|
|
|
return; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
$order_id = WC_Stripe_Helper::is_pre_30() ? $order->id : $order->get_id(); |
439
|
|
|
|
440
|
|
|
if ( 'stripe' === ( WC_Stripe_Helper::is_pre_30() ? $order->payment_method : $order->get_payment_method() ) ) { |
441
|
|
|
$charge = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_transaction_id', true ) : $order->get_transaction_id(); |
442
|
|
|
$captured = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_stripe_charge_captured', true ) : $order->get_meta( '_stripe_charge_captured', true ); |
443
|
|
|
$refund_id = WC_Stripe_Helper::is_pre_30() ? get_post_meta( $order_id, '_stripe_refund_id', true ) : $order->get_meta( '_stripe_refund_id', true ); |
444
|
|
|
|
445
|
|
|
// If the refund ID matches, don't continue to prevent double refunding. |
446
|
|
|
if ( $notification->data->object->refunds->data[0]->id === $refund_id ) { |
447
|
|
|
return; |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
// Only refund captured charge. |
451
|
|
|
if ( $charge ) { |
452
|
|
|
$reason = ( isset( $captured ) && 'yes' === $captured ) ? __( 'Refunded via Stripe Dashboard', 'woocommerce-gateway-stripe' ) : __( 'Pre-Authorization Released via Stripe Dashboard', 'woocommerce-gateway-stripe' ); |
453
|
|
|
|
454
|
|
|
// Create the refund. |
455
|
|
|
$refund = wc_create_refund( array( |
456
|
|
|
'order_id' => $order_id, |
457
|
|
|
'amount' => $this->get_refund_amount( $notification ), |
458
|
|
|
'reason' => $reason, |
459
|
|
|
) ); |
460
|
|
|
|
461
|
|
|
if ( is_wp_error( $refund ) ) { |
462
|
|
|
WC_Stripe_Logger::log( $refund->get_error_message() ); |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
$order->add_order_note( $reason ); |
466
|
|
|
} |
467
|
|
|
} |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
/** |
471
|
|
|
* Process webhook reviews that are opened. i.e Radar. |
472
|
|
|
* |
473
|
|
|
* @since 4.0.6 |
474
|
|
|
* @param object $notification |
475
|
|
|
*/ |
476
|
|
|
public function process_review_opened( $notification ) { |
477
|
|
|
$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge ); |
478
|
|
|
|
479
|
|
|
if ( ! $order ) { |
480
|
|
|
WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->charge ); |
481
|
|
|
return; |
482
|
|
|
} |
483
|
|
|
|
484
|
|
|
/* translators: 1) The URL to the order. 2) The reason type. */ |
485
|
|
|
$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 ); |
486
|
|
|
|
487
|
|
View Code Duplication |
if ( apply_filters( 'wc_stripe_webhook_review_change_order_status', true, $order, $notification ) ) { |
|
|
|
|
488
|
|
|
$order->update_status( 'on-hold', $message ); |
489
|
|
|
} else { |
490
|
|
|
$order->add_order_note( $message ); |
491
|
|
|
} |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
/** |
495
|
|
|
* Process webhook reviews that are closed. i.e Radar. |
496
|
|
|
* |
497
|
|
|
* @since 4.0.6 |
498
|
|
|
* @param object $notification |
499
|
|
|
*/ |
500
|
|
|
public function process_review_closed( $notification ) { |
501
|
|
|
$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge ); |
502
|
|
|
|
503
|
|
|
if ( ! $order ) { |
504
|
|
|
WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->charge ); |
505
|
|
|
return; |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
/* translators: 1) The reason type. */ |
509
|
|
|
$message = sprintf( __( 'The opened review for this order is now closed. Reason: (%s)', 'woocommerce-gateway-stripe' ), $notification->data->object->reason ); |
510
|
|
|
|
511
|
|
|
if ( 'on-hold' === $order->get_status() ) { |
512
|
|
View Code Duplication |
if ( apply_filters( 'wc_stripe_webhook_review_change_order_status', true, $order, $notification ) ) { |
|
|
|
|
513
|
|
|
$order->update_status( 'processing', $message ); |
514
|
|
|
} else { |
515
|
|
|
$order->add_order_note( $message ); |
516
|
|
|
} |
517
|
|
|
} else { |
518
|
|
|
$order->add_order_note( $message ); |
519
|
|
|
} |
520
|
|
|
} |
521
|
|
|
|
522
|
|
|
/** |
523
|
|
|
* Checks if capture is partial. |
524
|
|
|
* |
525
|
|
|
* @since 4.0.0 |
526
|
|
|
* @version 4.0.0 |
527
|
|
|
* @param object $notification |
528
|
|
|
*/ |
529
|
|
|
public function is_partial_capture( $notification ) { |
530
|
|
|
return 0 < $notification->data->object->amount_refunded; |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
/** |
534
|
|
|
* Gets the amount refunded. |
535
|
|
|
* |
536
|
|
|
* @since 4.0.0 |
537
|
|
|
* @version 4.0.0 |
538
|
|
|
* @param object $notification |
539
|
|
|
*/ |
540
|
|
|
public function get_refund_amount( $notification ) { |
541
|
|
|
if ( $this->is_partial_capture( $notification ) ) { |
542
|
|
|
$amount = $notification->data->object->amount_refunded / 100; |
543
|
|
|
|
544
|
|
|
if ( in_array( strtolower( $notification->data->object->currency ), WC_Stripe_Helper::no_decimal_currencies() ) ) { |
545
|
|
|
$amount = $notification->data->object->amount_refunded; |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
return $amount; |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
return false; |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
/** |
555
|
|
|
* Gets the amount we actually charge. |
556
|
|
|
* |
557
|
|
|
* @since 4.0.0 |
558
|
|
|
* @version 4.0.0 |
559
|
|
|
* @param object $notification |
560
|
|
|
*/ |
561
|
|
|
public function get_partial_amount_to_charge( $notification ) { |
562
|
|
|
if ( $this->is_partial_capture( $notification ) ) { |
563
|
|
|
$amount = ( $notification->data->object->amount - $notification->data->object->amount_refunded ) / 100; |
564
|
|
|
|
565
|
|
|
if ( in_array( strtolower( $notification->data->object->currency ), WC_Stripe_Helper::no_decimal_currencies() ) ) { |
566
|
|
|
$amount = ( $notification->data->object->amount - $notification->data->object->amount_refunded ); |
567
|
|
|
} |
568
|
|
|
|
569
|
|
|
return $amount; |
570
|
|
|
} |
571
|
|
|
|
572
|
|
|
return false; |
573
|
|
|
} |
574
|
|
|
|
575
|
|
|
/** |
576
|
|
|
* Processes the incoming webhook. |
577
|
|
|
* |
578
|
|
|
* @since 4.0.0 |
579
|
|
|
* @version 4.0.0 |
580
|
|
|
* @param string $request_body |
581
|
|
|
*/ |
582
|
|
|
public function process_webhook( $request_body ) { |
583
|
|
|
$notification = json_decode( $request_body ); |
584
|
|
|
|
585
|
|
|
switch ( $notification->type ) { |
586
|
|
|
case 'source.chargeable': |
587
|
|
|
$this->process_webhook_payment( $notification ); |
588
|
|
|
break; |
589
|
|
|
|
590
|
|
|
case 'source.canceled': |
591
|
|
|
$this->process_webhook_source_canceled( $notification ); |
592
|
|
|
break; |
593
|
|
|
|
594
|
|
|
case 'charge.succeeded': |
595
|
|
|
$this->process_webhook_charge_succeeded( $notification ); |
596
|
|
|
break; |
597
|
|
|
|
598
|
|
|
case 'charge.failed': |
599
|
|
|
$this->process_webhook_charge_failed( $notification ); |
600
|
|
|
break; |
601
|
|
|
|
602
|
|
|
case 'charge.captured': |
603
|
|
|
$this->process_webhook_capture( $notification ); |
604
|
|
|
break; |
605
|
|
|
|
606
|
|
|
case 'charge.dispute.created': |
607
|
|
|
$this->process_webhook_dispute( $notification ); |
608
|
|
|
break; |
609
|
|
|
|
610
|
|
|
case 'charge.refunded': |
611
|
|
|
$this->process_webhook_refund( $notification ); |
612
|
|
|
break; |
613
|
|
|
|
614
|
|
|
case 'review.opened': |
615
|
|
|
$this->process_review_opened( $notification ); |
616
|
|
|
break; |
617
|
|
|
|
618
|
|
|
case 'review.closed': |
619
|
|
|
$this->process_review_closed( $notification ); |
620
|
|
|
break; |
621
|
|
|
|
622
|
|
|
} |
623
|
|
|
} |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
new WC_Stripe_Webhook_Handler(); |
627
|
|
|
|
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.