PluginProbe ʕ •ᴥ•ʔ
Razorpay for WooCommerce / 4.8.6
Razorpay for WooCommerce v4.8.6
4.8.6 4.8.5 4.8.4 trunk 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.5.0 1.5.1 1.5.2 1.5.3 1.6.0 1.6.0-beta 1.6.1 1.6.2 1.6.3 1.6.5 2.0.0 2.0.1 2.1.0 2.2.0 2.3.0 2.3.1 2.3.2 2.4.0 2.4.1 2.4.2 2.4.3 2.5.0 2.6.0 2.7.0 2.7.1 2.7.2 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 3.0.0 3.0.1 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 3.3.0 3.4.0 3.4.1 3.5.0 3.5.1 3.6.0 3.7.0 3.7.1 3.7.2 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.0.1 4.1.0 4.2.0 4.3.0 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4.0 4.4.1 4.4.2 4.4.3 4.5.0 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.5.6 4.5.7 4.5.8 4.5.9 4.6.0 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.6.6 4.6.7 4.6.8 4.6.9 4.7.0 4.7.1 4.7.2 4.7.3 4.7.4 4.7.5 4.7.6 4.7.7 4.7.8 4.7.9 4.8.0 4.8.1 4.8.2 4.8.3
woo-razorpay / includes / razorpay-webhook.php
woo-razorpay / includes Last commit date
Errors 8 years ago api 2 months ago cron 2 years ago support 5 months ago debug.php 4 years ago plugin-instrumentation.php 3 months ago razorpay-affordability-widget.php 4 weeks ago razorpay-route-actions.php 2 years ago razorpay-route.php 4 weeks ago razorpay-webhook.php 5 months ago state-map.php 4 years ago utils.php 3 years ago
razorpay-webhook.php
711 lines
1 <?php
2
3 require_once __DIR__ . '/../woo-razorpay.php';
4 require_once __DIR__ . '/../razorpay-sdk/Razorpay.php';
5 require_once ABSPATH . '/wp-admin/includes/upgrade.php';
6
7 use Razorpay\Api\Api;
8 use Razorpay\Api\Errors;
9 use Automattic\WooCommerce\Utilities\OrderUtil;
10
11 class RZP_Webhook
12 {
13 /**
14 * Instance of the razorpay payments class
15 * @var WC_Razorpay
16 */
17 protected $razorpay;
18
19 /**
20 * API client instance to communicate with Razorpay API
21 * @var Razorpay\Api\Api
22 */
23 protected $api;
24
25 /**
26 * Event constants
27 */
28 const PAYMENT_AUTHORIZED = 'payment.authorized';
29 const PAYMENT_FAILED = 'payment.failed';
30 const PAYMENT_PENDING = 'payment.pending';
31 const SUBSCRIPTION_CANCELLED = 'subscription.cancelled';
32 const REFUNDED_CREATED = 'refund.created';
33 const VIRTUAL_ACCOUNT_CREDITED = 'virtual_account.credited';
34 const SUBSCRIPTION_PAUSED = 'subscription.paused';
35 const SUBSCRIPTION_RESUMED = 'subscription.resumed';
36 const SUBSCRIPTION_CHARGED = 'subscription.charged';
37
38 protected $eventsArray = [
39 self::PAYMENT_AUTHORIZED,
40 self::VIRTUAL_ACCOUNT_CREDITED,
41 self::REFUNDED_CREATED,
42 self::PAYMENT_FAILED,
43 self::PAYMENT_PENDING,
44 self::SUBSCRIPTION_CANCELLED,
45 self::SUBSCRIPTION_PAUSED,
46 self::SUBSCRIPTION_RESUMED,
47 self::SUBSCRIPTION_CHARGED,
48 ];
49
50 protected $subscriptionEvents = [
51 self::SUBSCRIPTION_CANCELLED,
52 self::SUBSCRIPTION_PAUSED,
53 self::SUBSCRIPTION_RESUMED,
54 self::SUBSCRIPTION_CHARGED,
55 ];
56
57 public function __construct()
58 {
59 $this->razorpay = new WC_Razorpay(false);
60
61 $this->api = $this->razorpay->getRazorpayApiInstance();
62 }
63
64 /**
65 * Process a Razorpay Webhook. We exit in the following cases:
66 * - Successful processed
67 * - Exception while fetching the payment
68 *
69 * It passes on the webhook in the following cases:
70 * - invoice_id set in payment.authorized
71 * - order refunded
72 * - Invalid JSON
73 * - Signature mismatch
74 * - Secret isn't setup
75 * - Event not recognized
76 *
77 * @return void|WP_Error
78 * @throws Exception
79 */
80 public function process()
81 {
82 $post = file_get_contents('php://input');
83
84 $data = json_decode($post, true);
85
86 if (json_last_error() !== 0) {
87 return;
88 }
89
90 // Skip the webhook if not the valid data and event
91 if ($this->shouldConsumeWebhook($data) === false) {
92
93 rzpLogDebug("Invalid webhook trigger: " . json_encode($data));
94 return;
95 }
96
97 if (empty($data['event']) === false) {
98
99 $orderId = $data['payload']['payment']['entity']['notes']['woocommerce_order_id'];
100 $razorpayOrderId = $data['payload']['payment']['entity']['order_id'];
101
102 if (in_array($data['event'], $this->subscriptionEvents) === true)
103 {
104 $orderId = $data['payload']['subscription']['entity']['notes']['woocommerce_order_id'];
105 $razorpayOrderId = ($data['event'] == self::SUBSCRIPTION_CHARGED) ? $razorpayOrderId : "No payment id in subscription event";
106 }
107
108 if (isset($_SERVER['HTTP_X_RAZORPAY_SIGNATURE']) === true) {
109
110 $razorpayWebhookSecret = (empty($this->razorpay->getSetting('webhook_secret')) === false) ? $this->razorpay->getSetting('webhook_secret') : get_option('webhook_secret');
111
112 //
113 // If the webhook secret isn't set on wordpress, return
114 //
115 if (empty($razorpayWebhookSecret) === true) {
116 $razorpayWebhookSecret = get_option('rzp_webhook_secret');
117 if (empty($razorpayWebhookSecret) === false) {
118 $this->razorpay->update_option('webhook_secret', $razorpayWebhookSecret);
119 } else {
120 rzpLogInfo("Woocommerce orderId: $orderId webhook process exited due to secret not available");
121
122 return;
123 }
124 }
125
126 try
127 {
128 $this->api->utility->verifyWebhookSignature($post,
129 $_SERVER['HTTP_X_RAZORPAY_SIGNATURE'],
130 $razorpayWebhookSecret);
131 } catch (Errors\SignatureVerificationError $e) {
132 $log = array(
133 'message' => $e->getMessage(),
134 'data' => $data,
135 'event' => 'razorpay.wc.signature.verify_failed',
136 );
137
138 rzpLogError(json_encode($log));
139
140 $trackObject = $this->razorpay->newTrackPluginInstrumentation();
141 $properties = [
142 'error' => $e->getMessage(),
143 'log' => $log
144 ];
145 $trackObject->rzpTrackDataLake('razorpay.webhook.signature.verification.failed', $properties);
146
147 return;
148 }
149
150 rzpLogInfo("Woocommerce orderId: $orderId webhook process intitiated for event: ". $data['event']);
151
152 switch ($data['event']) {
153 case self::PAYMENT_AUTHORIZED:
154 $webhookFilteredData = [
155 'invoice_id' => $data['payload']['payment']['entity']['invoice_id'],
156 'woocommerce_order_id' => $data['payload']['payment']['entity']['notes']['woocommerce_order_id'],
157 'razorpay_payment_id' => $data['payload']['payment']['entity']['id'],
158 'event' => $data['event']
159 ];
160 $this->saveWebhookEvent($webhookFilteredData, $data['payload']['payment']['entity']['order_id']);
161 return;
162
163 case self::VIRTUAL_ACCOUNT_CREDITED:
164 return $this->virtualAccountCredited($data);
165
166 case self::PAYMENT_FAILED:
167 return $this->paymentFailed($data);
168
169 case self::PAYMENT_PENDING:
170 return $this->paymentPending($data);
171
172 case self::SUBSCRIPTION_CANCELLED:
173 return $this->subscriptionCancelled($data);
174
175 case self::REFUNDED_CREATED:
176 return $this->refundedCreated($data);
177
178 case self::SUBSCRIPTION_PAUSED:
179 return $this->subscriptionPaused($data);
180
181 case self::SUBSCRIPTION_RESUMED:
182 return $this->subscriptionResumed($data);
183
184 case self::SUBSCRIPTION_CHARGED:
185 return $this->subscriptionCharged($data);
186
187 default:
188 return;
189 }
190 }
191 }
192 }
193
194 /**
195 * saves triggered webhook event in rzp_webhook_data table
196 * @param array $data Webook event Data
197 */
198 protected function saveWebhookEvent($data, $rzpOrderId)
199 {
200 global $wpdb;
201
202 try
203 {
204 $tableName = $wpdb->prefix . 'rzp_webhook_requests';
205
206 $integration = "woocommerce";
207
208 $webhookEvents = $wpdb->get_results("SELECT rzp_webhook_data FROM $tableName where order_id=" . $data['woocommerce_order_id'] . " AND rzp_order_id='" . $rzpOrderId . "';");
209
210 $rzpWebhookData = (array) json_decode($webhookEvents['rzp_webhook_data']);
211
212 $rzpWebhookData[] = $data;
213
214 $wpdb->update(
215 $tableName,
216 array(
217 'rzp_webhook_data' => json_encode($rzpWebhookData),
218 'rzp_webhook_notified_at' => time()
219 ),
220 array(
221 'integration' => $integration,
222 'order_id' => $data['woocommerce_order_id'],
223 'rzp_order_id' => $rzpOrderId
224 )
225 );
226 rzpLogInfo("webhook event saved for order:" . $data['woocommerce_order_id']);
227 }
228 catch (Exception $e)
229 {
230 rzpLogError("Insert webhook event failed. " . $e->getMessage());
231
232 $trackObject = $this->razorpay->newTrackPluginInstrumentation();
233 $properties = [
234 'error' => $e->getMessage()
235 ];
236 $trackObject->rzpTrackDataLake('razorpay.webhook.save.event.failed', $properties);
237 }
238 }
239
240 /**
241 * Does nothing for the main payments flow currently
242 * @param array $data Webook Data
243 */
244 protected function paymentFailed(array $data)
245 {
246 return;
247 }
248
249 /**
250 * Does nothing for the main payments flow currently
251 * @param array $data Webook Data
252 */
253 protected function subscriptionCancelled(array $data)
254 {
255 return;
256 }
257
258 /**
259 * Does nothing for the main payments flow currently
260 * @param array $data Webook Data
261 */
262 protected function subscriptionPaused(array $data)
263 {
264 return;
265 }
266
267 /**
268 * Does nothing for the main payments flow currently
269 * @param array $data Webook Data
270 */
271 protected function subscriptionResumed(array $data)
272 {
273 return;
274 }
275
276 /**
277 * Handling next subscription charged webhook
278 * @param array $data Webook Data
279 */
280 protected function subscriptionCharged(array $data)
281 {
282 return;
283 }
284
285 /**
286 * Handling the payment authorized webhook
287 *
288 * @param array $data Webook Data
289 */
290 public function paymentAuthorized(array $data)
291 {
292 // We don't process subscription/invoice payments here
293 if (isset($data['invoice_id']) === true) {
294 rzpLogInfo("We don't process subscription/invoice payments here");
295 return;
296 }
297
298 if (empty($data['woocommerce_order_id'])) {
299 rzpLogInfo("woocommerce_order_id not found in data:" . json_encode($data));
300 return;
301 }
302
303 $orderId = $data['woocommerce_order_id'];
304
305 rzpLogInfo("Woocommerce orderId: $orderId, webhook process initiated for payment authorized event by cron");
306
307
308 $order = $this->checkIsObject($orderId);
309 if ($order === false)
310 {
311 return;
312 }
313
314 $orderStatus = $order->get_status();
315 rzpLogInfo("Woocommerce orderId: $orderId order status: $orderStatus");
316
317 // If it is already marked as paid, ignore the event
318 if ($orderStatus != 'draft' and
319 ($order->needs_payment() === false and
320 ($orderStatus === 'cancelled') === false)) {
321 rzpLogInfo("Woocommerce orderId: $orderId webhook process exited with need payment status :" . $order->needs_payment());
322
323 return;
324 }
325
326 if ($orderStatus == 'checkout-draft' || $orderStatus == 'draft') {
327 updateOrderStatus($orderId, 'wc-pending');
328 }
329
330 $razorpayPaymentId = $data['razorpay_payment_id'];
331
332 $payment = $this->getPaymentEntity($razorpayPaymentId, $data);
333
334 if ($payment === false)
335 {
336 return;
337 }
338
339 $amount = $this->getOrderAmountAsInteger($order);
340
341 $success = false;
342 $errorMessage = 'The payment has failed.';
343
344 if ($payment['status'] === 'captured') {
345 $success = true;
346 } else if (($payment['status'] === 'authorized') and
347 ($this->razorpay->getSetting('payment_action') === WC_Razorpay::CAPTURE)) {
348 //
349 // If the payment is only authorized, we capture it
350 // If the merchant has enabled auto capture
351 //
352 try
353 {
354 $payment->capture(array('amount' => $amount));
355
356 $success = true;
357 } catch (Exception $e) {
358 //
359 // Capture will fail if the payment is already captured
360 //
361 $log = array(
362 'message' => $e->getMessage(),
363 'payment_id' => $razorpayPaymentId,
364 'event' => $data['event'],
365 );
366
367 rzpLogError(json_encode($log));
368
369 //
370 // We re-fetch the payment entity and check if the payment is captured now
371 //
372 $payment = $this->getPaymentEntity($razorpayPaymentId, $data);
373
374 if ($payment === false)
375 {
376 return;
377 }
378
379 if ($payment['status'] === 'captured') {
380 $success = true;
381 }
382 }
383 }
384
385 $this->razorpay->updateOrder($order, $success, $errorMessage, $razorpayPaymentId, null, true);
386
387 rzpLogInfo("Woocommerce orderId: $orderId webhook process finished the updateOrder function");
388 }
389
390 /**
391 * Handling the payment pending webhook to handle COD orders
392 *
393 * @param array $data Webook Data
394 */
395 protected function paymentPending(array $data)
396 {
397 // We don't process subscription/invoice payments here
398 if (isset($data['payload']['payment']['entity']['invoice_id']) === true) {
399 return;
400 }
401
402 if (isset($data['payload']['payment']['entity']['method']) != 'cod') {
403 return;
404 }
405
406 //
407 // Order entity should be sent as part of the webhook payload
408 //
409 $orderId = $data['payload']['payment']['entity']['notes']['woocommerce_order_id'];
410
411 rzpLogInfo("Woocommerce orderId: $orderId webhook process intitiated for COD method payment pending event");
412
413 if (!empty($orderId)) {
414 $order = $this->checkIsObject($orderId);
415
416 if ($order === false)
417 {
418 return;
419 }
420 }
421
422 $orderStatus = $order->get_status();
423 rzpLogInfo("Woocommerce orderId: $orderId order status: $orderStatus");
424
425 // If it is already marked as paid, ignore the event
426 if ($orderStatus != 'draft' && $order->needs_payment() === false) {
427 rzpLogInfo("Woocommerce orderId: $orderId webhook process exited with need payment status :" . $order->needs_payment());
428
429 return;
430 }
431
432 if ($orderStatus == 'checkout-draft' || $orderStatus == 'draft') {
433 updateOrderStatus($orderId, 'wc-pending');
434 }
435
436 $razorpayPaymentId = $data['payload']['payment']['entity']['id'];
437
438 $payment = $this->getPaymentEntity($razorpayPaymentId, $data);
439
440 if ($payment === false)
441 {
442 return;
443 }
444
445 $success = false;
446 $errorMessage = 'The payment has failed.';
447
448 if ($payment['status'] === 'pending' && $data['payload']['payment']['entity']['method'] == 'cod' && !empty($razorpayPaymentId)) {
449 $success = true;
450
451 $this->razorpay->updateOrder($order, $success, $errorMessage, $razorpayPaymentId, null, true);
452 rzpLogInfo("Woocommerce orderId: $orderId webhook process finished the update order function for COD");
453 }
454
455 // Graceful exit since payment is now processed.
456 exit;
457 }
458
459 /**
460 * Handling the virtual account credited webhook
461 *
462 * @param array $data Webook Data
463 */
464 protected function virtualAccountCredited(array $data)
465 {
466 // We don't process subscription/invoice payments here
467 if (isset($data['payload']['payment']['entity']['invoice_id']) === true) {
468 return;
469 }
470
471 //
472 // Order entity should be sent as part of the webhook payload
473 //
474 $orderId = $data['payload']['payment']['entity']['notes']['woocommerce_order_id'];
475
476 if (!empty($orderId)) {
477 $order = $this->checkIsObject($orderId);
478
479 if ($order === false)
480 {
481 return;
482 }
483 }
484 // If it is already marked as paid, ignore the event
485 if ($order->needs_payment() === false) {
486 return;
487 }
488
489 $razorpayPaymentId = $data['payload']['payment']['entity']['id'];
490 $virtualAccountId = $data['payload']['virtual_account']['entity']['id'];
491 $amountPaid = (int) $data['payload']['virtual_account']['entity']['amount_paid'];
492
493 $payment = $this->getPaymentEntity($razorpayPaymentId, $data);
494
495 if ($payment === false)
496 {
497 return;
498 }
499
500 $amount = $this->getOrderAmountAsInteger($order);
501
502 $success = false;
503 $errorMessage = 'The payment has failed.';
504
505 if ($payment['status'] === 'captured' and $amountPaid === $amount) {
506 $success = true;
507 } else if (($payment['status'] === 'authorized') and $amountPaid === $amount and
508 ($this->razorpay->getSetting('payment_action') === WC_Razorpay::CAPTURE)) {
509 //
510 // If the payment is only authorized, we capture it
511 // If the merchant has enabled auto capture
512 //
513 try
514 {
515 $payment->capture(array('amount' => $amount));
516
517 $success = true;
518 } catch (Exception $e) {
519 //
520 // Capture will fail if the payment is already captured
521 //
522 $log = array(
523 'message' => $e->getMessage(),
524 'payment_id' => $razorpayPaymentId,
525 'event' => $data['event'],
526 );
527
528 error_log(json_encode($log));
529
530 //
531 // We re-fetch the payment entity and check if the payment is captured now
532 //
533 $payment = $this->getPaymentEntity($razorpayPaymentId, $data);
534
535 if ($payment === false)
536 {
537 return;
538 }
539
540 if ($payment['status'] === 'captured') {
541 $success = true;
542 }
543 }
544 }
545
546 $this->razorpay->updateOrder($order, $success, $errorMessage, $razorpayPaymentId, $virtualAccountId, true);
547
548 // Graceful exit since payment is now processed.
549 exit;
550 }
551
552 protected function getPaymentEntity($razorpayPaymentId, $data)
553 {
554 try
555 {
556 $payment = $this->api->payment->fetch($razorpayPaymentId);
557 } catch (Exception $e) {
558 $log = array(
559 'message' => $e->getMessage(),
560 'payment_id' => $razorpayPaymentId,
561 'event' => $data['event'],
562 );
563
564 rzpLogError(json_encode($log));
565
566 return false;
567 }
568
569 return $payment;
570 }
571
572 /**
573 * Returns boolean false incase not proper webhook data
574 */
575 protected function shouldConsumeWebhook($data)
576 {
577 if ((isset($data['event']) === true) and
578 (in_array($data['event'], $this->eventsArray) === true) and
579 (isset($data['payload']['payment']['entity']['notes']['woocommerce_order_id']) === true or isset($data['payload']['subscription']['entity']['notes']['woocommerce_order_id']) === true)) {
580 return true;
581 }
582
583 return false;
584 }
585
586 /**
587 * Returns the order amount, rounded as integer
588 * @param WC_Order $order WooCommerce Order instance
589 * @return int Order Amount
590 */
591 public function getOrderAmountAsInteger($order)
592 {
593 if (version_compare(WOOCOMMERCE_VERSION, '3.0.0', '>=')) {
594 return (int) round($order->get_total() * 100);
595 }
596
597 return (int) round($order->order_total * 100);
598 }
599
600 /**
601 * Process Order Refund through Webhook
602 * @param array $data
603 * @return void|WP_Error
604 * @throws Exception
605 */
606 public function refundedCreated(array $data)
607 {
608 // We don't process subscription/invoice payments here
609 if (isset($data['payload']['payment']['entity']['invoice_id']) === true) {
610 return;
611 }
612
613 //Avoid to recreate refund, If already refund saved and initiated from woocommerce website.
614 if (isset($data['payload']['refund']['entity']['notes']['refund_from_website']) === true) {
615 return;
616 }
617
618 $razorpayPaymentId = $data['payload']['refund']['entity']['payment_id'];
619
620 $refundId = $data['payload']['refund']['entity']['id'];
621
622 $payment = $this->getPaymentEntity($razorpayPaymentId, $data);
623
624 if ($payment === false)
625 {
626 return;
627 }
628
629 //
630 // Order entity should be sent as part of the webhook payload
631 //
632 $orderId = $payment['notes']['woocommerce_order_id'];
633
634 if (!empty($orderId)) {
635 $order = $this->checkIsObject($orderId);
636
637 if ($order === false)
638 {
639 return;
640 }
641 }
642
643 // If it is already marked as unpaid, ignore the event
644 if ($order->needs_payment() === true) {
645 return;
646 }
647
648 // If it's something else such as a WC_Order_Refund, we don't want that.
649 if (!is_a($order, 'WC_Order')) {
650 $log = array(
651 'Error' => 'Provided ID is not a WC Order',
652 );
653
654 error_log(json_encode($log));
655 }
656
657 if ('refunded' == $order->get_status()) {
658 $log = array(
659 'Error' => 'Order has been already refunded for Order Id -' . $orderId,
660 );
661
662 error_log(json_encode($log));
663 }
664
665 $refundAmount = round(($data['payload']['refund']['entity']['amount'] / 100), 2);
666
667 $refundReason = $data['payload']['refund']['entity']['notes']['comment'];
668
669 try
670 {
671 wc_create_refund(array(
672 'amount' => $refundAmount,
673 'reason' => $refundReason,
674 'order_id' => $orderId,
675 'refund_id' => $refundId,
676 'line_items' => array(),
677 'refund_payment' => false,
678 ));
679
680 $order->add_order_note(__('Refund Id: ' . $refundId, 'woocommerce'));
681
682 } catch (Exception $e) {
683 //
684 // Capture will fail if the payment is already captured
685 //
686 $log = array(
687 'message' => $e->getMessage(),
688 'payment_id' => $razorpayPaymentId,
689 'event' => $data['event'],
690 );
691
692 error_log(json_encode($log));
693
694 }
695
696 // Graceful exit since payment is now refunded.
697 exit();
698 }
699
700 public function checkIsObject($orderId)
701 {
702 $order = wc_get_order($orderId);
703 if (is_object($order)) {
704 return wc_get_order($orderId);
705 } else {
706 rzpLogInfo("Woocommerce order Object does not exist");
707 return false;
708 }
709 }
710 }
711