Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling charge expiration webhooks #3651

Merged
merged 8 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
wjrosa marked this conversation as resolved.
Show resolved Hide resolved

= 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.',
],
];
}
}
Loading