Skip to content

Commit

Permalink
Handling charge expiration webhooks (#3651)
Browse files Browse the repository at this point in the history
* Handling charge expiration webhooks

* Readme and changelog entries

* Adding specific unit tests

* Fix tests

* Fix tests

* Updating changelog and readme entries version
  • Loading branch information
wjrosa authored Dec 12, 2024
1 parent 3c6eb4b commit 1977aff
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 9 deletions.
3 changes: 3 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
*** Changelog ***

= 9.1.0 - xxxx-xx-xx =
* Add - Correctly handles charge expired webhook events, setting the order status to failed and adding a note.

= 9.0.0 - xxxx-xx-xx =
* Fix - Allow account creation on checkout, if enabled, when purchasing subscriptions using ECE.
* Fix - Fix 404 that happens when using ECE and 3D Secure auth is triggered.
Expand Down
8 changes: 7 additions & 1 deletion includes/class-wc-stripe-webhook-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ public function process_webhook_charge_succeeded( $notification ) {
*
* @since 4.0.0
* @since 4.1.5 Can handle any fail payments from any methods.
* @since 9.0.0 Can handle payment expiration.
* @param object $notification
*/
public function process_webhook_charge_failed( $notification ) {
Expand All @@ -536,7 +537,11 @@ public function process_webhook_charge_failed( $notification ) {
return;
}

$message = __( 'This payment failed to clear.', 'woocommerce-gateway-stripe' );
if ( 'charge.expired' === $notification->type ) {
$message = __( 'This payment has expired.', 'woocommerce-gateway-stripe' );
} else {
$message = __( 'This payment failed to clear.', 'woocommerce-gateway-stripe' );
}
if ( ! $order->get_meta( '_stripe_status_final', false ) ) {
$order->update_status( 'failed', $message );
} else {
Expand Down Expand Up @@ -1160,6 +1165,7 @@ public function process_webhook( $request_body ) {
break;

case 'charge.failed':
case 'charge.expired':
$this->process_webhook_charge_failed( $notification );
break;

Expand Down
3 changes: 3 additions & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o

== Changelog ==

= 9.1.0 - xxxx-xx-xx =
* Add - Correctly handles charge expired webhook events, setting the order status to failed and adding a note.

= 9.0.0 - xxxx-xx-xx =
* Fix - Allow account creation on checkout, if enabled, when purchasing subscriptions using ECE.
* Fix - Fix 404 that happens when using ECE and 3D Secure auth is triggered.
Expand Down
113 changes: 105 additions & 8 deletions tests/phpunit/test-wc-stripe-webhook-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ class WC_Stripe_Webhook_Handler_Test extends WP_UnitTestCase {
* Mock card payment intent template.
*/
const MOCK_PAYMENT_INTENT = [
'id' => 'pi_mock',
'object' => 'payment_intent',
'status' => 'succeeded',
'charges' => [
'id' => 'pi_mock',
'object' => 'payment_intent',
'status' => 'succeeded',
'charges' => [
'total_count' => 1,
'data' => [
[
Expand Down Expand Up @@ -96,7 +96,7 @@ public function test_process_deferred_webhook_invalid_args() {
$this->mock_webhook_handler->process_deferred_webhook( 'payment_intent.succeeded', $data );

// No payment intent.
$order = WC_Helper_Order::create_order();
$order = WC_Helper_Order::create_order();
$data['order_id'] = $order->get_id();

$this->expectExceptionMessage( "Missing required data. 'intent_id' is missing for the deferred 'payment_intent.succeeded' event." );
Expand All @@ -110,7 +110,7 @@ public function test_process_deferred_webhook() {
$order = WC_Helper_Order::create_order();
$intent_id = 'pi_mock_1234';
$data = [
'order_id' => $order->get_id(),
'order_id' => $order->get_id(),
'intent_id' => $intent_id,
];

Expand All @@ -134,7 +134,7 @@ function( $passed_order ) use ( $order ) {
public function test_mismatch_intent_id_process_deferred_webhook() {
$order = WC_Helper_Order::create_order();
$data = [
'order_id' => $order->get_id(),
'order_id' => $order->get_id(),
'intent_id' => 'pi_wrong_id',
];

Expand Down Expand Up @@ -168,7 +168,7 @@ function( $passed_order ) use ( $order ) {
public function test_process_of_successful_payment_intent_deferred_webhook() {
$order = WC_Helper_Order::create_order();
$data = [
'order_id' => $order->get_id(),
'order_id' => $order->get_id(),
'intent_id' => self::MOCK_PAYMENT_INTENT['id'],
];

Expand Down Expand Up @@ -198,4 +198,101 @@ function( $passed_order ) use ( $order ) {

$this->mock_webhook_handler->process_deferred_webhook( 'payment_intent.succeeded', $data );
}

/**
* Test for `process_webhook_charge_failed`.
*
* @param string $order_status The order status.
* @param bool $order_status_final Whether the order status is final.
* @param string $charge_id The charge ID.
* @param array $event The event type.
* @param string $expected_status The expected order status.
* @param string $expected_note The expected order note.
* @return void
* @dataProvider provide_test_process_webhook_charge_failed
*/
public function test_process_webhook_charge_failed(
$order_status,
$order_status_final,
$charge_id,
$event,
$expected_status,
$expected_note
) {
$order = WC_Helper_Order::create_order();
$order->set_status( $order_status );
$order->set_transaction_id( $charge_id );
if ( $order_status_final ) {
$order->update_meta_data( '_stripe_status_final', true );
}
$order->save();

$notification = (object) [
'type' => $event,
'data' => (object) [
'object' => (object) [
'id' => 'ch_fQpkNKxmUrZ8t4CT7EHGS3Rg',
],
],
];

$this->mock_webhook_handler->process_webhook_charge_failed( $notification );

if ( $charge_id ) { // Order not found charge ID.
$final_order = wc_get_order( $order->get_id() );
$this->assertEquals( $expected_status, $final_order->get_status() );

if ( $expected_note ) {
$notes = wc_get_order_notes(
[
'order_id' => $final_order->get_id(),
'limit' => 1,
]
);
$this->assertSame( $expected_note, $notes[0]->content );
}
}
}

/**
* Provider for `test_process_webhook_charge_failed`.
*
* @return array
*/
public function provide_test_process_webhook_charge_failed() {
return [
'order already failed' => [
'order status' => 'failed',
'order status final' => false,
'charge id' => 'ch_fQpkNKxmUrZ8t4CT7EHGS3Rg',
'event' => 'charge.failed',
'expected status' => 'failed',
'expected note' => '',
],
'charge failed event, order already with the final status' => [
'order status' => 'on-hold',
'order status final' => true,
'charge id' => 'ch_fQpkNKxmUrZ8t4CT7EHGS3Rg',
'event' => 'charge.failed',
'expected status' => 'on-hold',
'expected note' => 'This payment failed to clear.',
],
'charge failed event' => [
'order status' => 'on-hold',
'order status final' => false,
'charge id' => 'ch_fQpkNKxmUrZ8t4CT7EHGS3Rg',
'event' => 'charge.failed',
'expected status' => 'failed',
'expected note' => 'This payment failed to clear. Order status changed from On hold to Failed.',
],
'charge expired event' => [
'order status' => 'on-hold',
'order status final' => false,
'charge id' => 'ch_fQpkNKxmUrZ8t4CT7EHGS3Rg',
'event' => 'charge.expired',
'expected status' => 'failed',
'expected note' => 'This payment has expired. Order status changed from On hold to Failed.',
],
];
}
}

0 comments on commit 1977aff

Please sign in to comment.