Skip to content

Commit 96946ae

Browse files
authored
Add hook to allow for custom webhook post-processing (#4466)
* Add hook to allow for custom webhook post-processing * Moving action inside specific methods when processing async events * Adding a try...catch block * Changelog and readme entries * Update includes/class-wc-stripe-webhook-handler.php * Introducing a protected variable to store the order being processed * Passing the whole notification object to deferred events * Fix tests * Moving action after the deferred webhook switch * Add try/catch to main hook; catch Throwable instead of Exception * Refactor action triggers into helper function and add explicit docs for null orders * Update changelog.txt * Readme entry update * Update includes/class-wc-stripe-webhook-handler.php * Renaming the action to wc_stripe_webhook_received * Changelog and readme entries * Fix changelog entry location
1 parent 90f82d3 commit 96946ae

File tree

4 files changed

+163
-20
lines changed

4 files changed

+163
-20
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Tweak - Use more specific selector in express checkout e2e tests
1111
* Tweak - Small improvements to e2e tests
1212
* Fix - Fix unnecessary Stripe API calls when rendering subscription details
13+
* Add - Adds a new action (`wc_stripe_webhook_received`) to allow additional actions to be taken for webhook notifications from Stripe
1314

1415
= 9.8.1 - 2025-08-15 =
1516
* Fix - Remove connection type requirement from PMC sync migration attempt

includes/class-wc-stripe-webhook-handler.php

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ class WC_Stripe_Webhook_Handler extends WC_Stripe_Payment_Gateway {
4949
*/
5050
protected $deferred_webhook_action = 'wc_stripe_deferred_webhook';
5151

52+
/**
53+
* The order object being processed.
54+
*
55+
* @var WC_Order|null
56+
*/
57+
protected $resolved_order = null;
58+
5259
/**
5360
* Constructor.
5461
*
@@ -71,7 +78,7 @@ public function __construct() {
7178
// plugin when this code first appears.
7279
WC_Stripe_Webhook_State::get_monitoring_began_at();
7380

74-
add_action( $this->deferred_webhook_action, [ $this, 'process_deferred_webhook' ], 10, 2 );
81+
add_action( $this->deferred_webhook_action, [ $this, 'process_deferred_webhook' ], 10, 3 );
7582
}
7683

7784
/**
@@ -269,6 +276,9 @@ public function process_webhook_payment( $notification, $retry = true ) {
269276
return;
270277
}
271278

279+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
280+
$this->resolved_order = $order;
281+
272282
$order_id = $order->get_id();
273283

274284
$is_pending_receiver = ( 'receiver' === $notification->data->object->flow );
@@ -402,6 +412,9 @@ public function process_webhook_dispute( $notification ) {
402412
return;
403413
}
404414

415+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
416+
$this->resolved_order = $order;
417+
405418
$this->set_stripe_order_status_before_hold( $order, $order->get_status() );
406419

407420
$needs_response = in_array( $notification->data->object->status, [ 'needs_response', 'warning_needs_response' ], true );
@@ -444,6 +457,9 @@ public function process_webhook_dispute_closed( $notification ) {
444457
return;
445458
}
446459

460+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
461+
$this->resolved_order = $order;
462+
447463
if ( 'lost' === $status ) {
448464
$message = __( 'The dispute was lost or accepted.', 'woocommerce-gateway-stripe' );
449465
} elseif ( 'won' === $status ) {
@@ -495,6 +511,9 @@ public function process_webhook_capture( $notification ) {
495511
return;
496512
}
497513

514+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
515+
$this->resolved_order = $order;
516+
498517
if ( WC_Stripe_Helper::payment_method_allows_manual_capture( $order->get_payment_method() ) ) {
499518
$charge = $order->get_transaction_id();
500519
$captured = $order->get_meta( '_stripe_charge_captured', true );
@@ -567,6 +586,9 @@ public function process_webhook_charge_succeeded( $notification ) {
567586
return;
568587
}
569588

589+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
590+
$this->resolved_order = $order;
591+
570592
if ( ! $order->has_status( OrderStatus::ON_HOLD ) ) {
571593
return;
572594
}
@@ -625,6 +647,9 @@ public function process_webhook_charge_failed( $notification ) {
625647
return;
626648
}
627649

650+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
651+
$this->resolved_order = $order;
652+
628653
// If order status is already in failed status don't continue.
629654
if ( $order->has_status( OrderStatus::FAILED ) ) {
630655
return;
@@ -665,6 +690,9 @@ public function process_webhook_source_canceled( $notification ) {
665690
}
666691
}
667692

693+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
694+
$this->resolved_order = $order;
695+
668696
// Don't proceed if payment method isn't Stripe.
669697
if ( 'stripe' !== $order->get_payment_method() ) {
670698
WC_Stripe_Logger::log( 'Canceled webhook abort: Order was not processed by Stripe: ' . $order->get_id() );
@@ -697,6 +725,9 @@ public function process_webhook_refund( $notification ) {
697725
$order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id );
698726
}
699727

728+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
729+
$this->resolved_order = $order;
730+
700731
if ( ! $order ) {
701732
WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id );
702733
return;
@@ -785,6 +816,9 @@ public function process_webhook_refund_updated( $notification ) {
785816
return;
786817
}
787818

819+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
820+
$this->resolved_order = $order;
821+
788822
$order_id = $order->get_id();
789823

790824
if ( 'stripe' === substr( (string) $order->get_payment_method(), 0, 6 ) ) {
@@ -888,6 +922,9 @@ public function process_review_opened( $notification ) {
888922
}
889923
}
890924

925+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
926+
$this->resolved_order = $order;
927+
891928
$this->set_stripe_order_status_before_hold( $order, $order->get_status() );
892929

893930
$message = sprintf(
@@ -929,6 +966,9 @@ public function process_review_closed( $notification ) {
929966
}
930967
}
931968

969+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
970+
$this->resolved_order = $order;
971+
932972
/* translators: 1) The reason type. */
933973
$message = sprintf( __( 'The opened review for this order is now closed. Reason: (%s)', 'woocommerce-gateway-stripe' ), $notification->data->object->reason );
934974

@@ -1047,6 +1087,9 @@ public function process_payment_intent( $notification ) {
10471087
return;
10481088
}
10491089

1090+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
1091+
$this->resolved_order = $order;
1092+
10501093
if ( $this->lock_order_payment( $order, $intent ) ) {
10511094
return;
10521095
}
@@ -1082,7 +1125,7 @@ public function process_payment_intent( $notification ) {
10821125
$process_webhook_async = apply_filters( 'wc_stripe_process_payment_intent_webhook_async', true, $order, $intent, $notification );
10831126
$is_awaiting_action = $order->get_meta( '_stripe_upe_waiting_for_redirect' ) ?? false;
10841127

1085-
// Process the webhook now if it's for a voucher or wallet payment , or if filtered to process immediately and order is not awaiting action.
1128+
// Process the webhook now if it's for a voucher or wallet payment, or if filtered to process immediately and order is not awaiting action.
10861129
if ( $is_voucher_payment || $is_wallet_payment || ( ! $process_webhook_async && ! $is_awaiting_action ) ) {
10871130
$charge = $this->get_latest_charge_from_intent( $intent );
10881131

@@ -1096,6 +1139,8 @@ public function process_payment_intent( $notification ) {
10961139

10971140
$charge->is_webhook_response = true;
10981141
$this->process_response( $charge, $order );
1142+
1143+
$this->run_webhook_received_action( (string) $notification->type, $notification, $this->resolved_order );
10991144
} else {
11001145
WC_Stripe_Logger::log( "Processing $notification->type ($intent->id) asynchronously for order $order_id." );
11011146

@@ -1112,7 +1157,6 @@ public function process_payment_intent( $notification ) {
11121157
do_action( 'wc_gateway_stripe_process_payment_intent_incomplete', $order );
11131158
}
11141159
}
1115-
11161160
break;
11171161
default:
11181162
if ( $is_voucher_payment && 'payment_intent.payment_failed' === $notification->type ) {
@@ -1153,6 +1197,9 @@ public function process_setup_intent( $notification ) {
11531197
return;
11541198
}
11551199

1200+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
1201+
$this->resolved_order = $order;
1202+
11561203
$allowed_payment_processing_statuses = [ OrderStatus::PENDING, OrderStatus::FAILED ];
11571204

11581205
$allowed_payment_processing_statuses = apply_filters_deprecated(
@@ -1227,8 +1274,9 @@ protected function defer_webhook_processing( $webhook_notification, $additional_
12271274
time() + $this->deferred_webhook_delay,
12281275
$this->deferred_webhook_action,
12291276
[
1230-
'type' => $webhook_notification->type,
1231-
'data' => $additional_data,
1277+
'type' => $webhook_notification->type,
1278+
'data' => $additional_data,
1279+
'notification' => $webhook_notification,
12321280
]
12331281
);
12341282
}
@@ -1240,8 +1288,9 @@ protected function defer_webhook_processing( $webhook_notification, $additional_
12401288
*
12411289
* @param string $webhook_type The webhook event name/type.
12421290
* @param array $additional_data Additional data passed to the scheduled job.
1291+
* @param stdClass $notification The webhook notification payload.
12431292
*/
1244-
public function process_deferred_webhook( $webhook_type, $additional_data ) {
1293+
public function process_deferred_webhook( $webhook_type, $additional_data, $notification = null ) {
12451294
try {
12461295
switch ( $webhook_type ) {
12471296
case 'payment_intent.succeeded':
@@ -1253,6 +1302,9 @@ public function process_deferred_webhook( $webhook_type, $additional_data ) {
12531302
throw new Exception( "Missing required data. 'order_id' is invalid or not found for the deferred '{$webhook_type}' event." );
12541303
}
12551304

1305+
// Set the order being processed for the `wc_stripe_webhook_received` action later.
1306+
$this->resolved_order = $order;
1307+
12561308
if ( empty( $intent_id ) ) {
12571309
throw new Exception( "Missing required data. 'intent_id' is missing for the deferred '{$webhook_type}' event." );
12581310
}
@@ -1269,6 +1321,8 @@ public function process_deferred_webhook( $webhook_type, $additional_data ) {
12691321
throw new Exception( "Unsupported webhook type: {$webhook_type}" );
12701322
break;
12711323
}
1324+
1325+
$this->run_webhook_received_action( (string) $webhook_type, $notification, $this->resolved_order );
12721326
} catch ( Exception $e ) {
12731327
WC_Stripe_Logger::log( 'Error processing deferred webhook: ' . $e->getMessage() );
12741328

@@ -1334,6 +1388,8 @@ public function process_account_updated( $notification ) {
13341388
public function process_webhook( $request_body ) {
13351389
$notification = json_decode( $request_body );
13361390

1391+
$this->resolved_order = null;
1392+
13371393
switch ( $notification->type ) {
13381394
case 'account.updated':
13391395
$this->process_account_updated( $notification );
@@ -1397,8 +1453,41 @@ public function process_webhook( $request_body ) {
13971453
$this->process_setup_intent( $notification );
13981454

13991455
}
1456+
1457+
// These events might be processed async. Skip the action trigger for them here. The trigger will be called inside the specific methods.
1458+
if ( 'payment_intent.succeeded' === $notification->type || 'payment_intent.amount_capturable_updated' === $notification->type ) {
1459+
return;
1460+
}
1461+
1462+
$this->run_webhook_received_action( $notification->type, $notification, $this->resolved_order );
14001463
}
14011464

1465+
/**
1466+
* Helper function to run the `wc_stripe_webhook_received` action consistently.
1467+
*
1468+
* @param string $webhook_type The type of webhook that was processed.
1469+
* @param object $notification The webhook data sent from Stripe.
1470+
* @param WC_Order|null $order The order being processed by the webhook.
1471+
*/
1472+
private function run_webhook_received_action( string $webhook_type, object $notification, ?WC_Order $order = null ): void {
1473+
try {
1474+
/**
1475+
* Fires after a webhook has been processed, but before we respond to Stripe.
1476+
* This allows for custom processing of the webhook after it has been processed.
1477+
* Note that the $order parameter may be null in various cases, especially when processing
1478+
* webhooks unrelated to orders, such as account updates.
1479+
*
1480+
* @since 9.8.0
1481+
*
1482+
* @param string $webhook_type The type of webhook that was processed.
1483+
* @param object $notification The webhook data sent from Stripe.
1484+
* @param WC_Order|null $order The order being processed by the webhook.
1485+
*/
1486+
do_action( 'wc_stripe_webhook_received', $webhook_type, $notification, $this->resolved_order );
1487+
} catch ( Throwable $e ) {
1488+
WC_Stripe_Logger::error( 'Error in wc_stripe_webhook_received action: ' . $e->getMessage(), [ 'error' => $e ] );
1489+
}
1490+
}
14021491
/**
14031492
* Fetches an order from a payment intent.
14041493
*

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
120120
* Tweak - Use more specific selector in express checkout e2e tests
121121
* Tweak - Small improvements to e2e tests
122122
* Fix - Fix unnecessary Stripe API calls when rendering subscription details
123+
* Add - Adds a new action (`wc_stripe_webhook_received`) to allow additional actions to be taken for webhook notifications from Stripe
123124

124125
[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).

0 commit comments

Comments
 (0)