From d199760629fe4d6f0aa795f0e2c673a71f0456c3 Mon Sep 17 00:00:00 2001 From: Paul Dechov Date: Mon, 12 Jul 2021 23:05:01 -0400 Subject: [PATCH] Fix failed free trial checkout when card requires authentication (#2326) Treat setup_intent as array instead of object when retrieving next_action --- includes/class-wc-payment-gateway-wcpay.php | 3 +- ...-payment-gateway-wcpay-process-payment.php | 120 +++++++++++++++++- ...ay-wcpay-subscriptions-process-payment.php | 1 + 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index 1ab937fd139..20772512719 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -957,6 +957,7 @@ public function process_payment_for_order( $cart, $payment_information, $additio $charge_id = $intent->get_charge_id(); $client_secret = $intent->get_client_secret(); $currency = $intent->get_currency(); + $next_action = $intent->get_next_action(); if ( 'requires_action' === $status && $payment_information->is_merchant_initiated() ) { @@ -975,6 +976,7 @@ public function process_payment_for_order( $cart, $payment_information, $additio $charge_id = ''; $client_secret = $intent['client_secret']; $currency = $order->get_currency(); + $next_action = $intent['next_action']; } if ( ! empty( $intent ) ) { @@ -998,7 +1000,6 @@ public function process_payment_for_order( $cart, $payment_information, $additio } if ( 'requires_action' === $status ) { - $next_action = $intent->get_next_action(); if ( isset( $next_action['type'] ) && 'redirect_to_url' === $next_action['type'] && ! empty( $next_action['redirect_to_url']['url'] ) ) { $response = [ 'result' => 'success', diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php index be46d5c35af..e91d61512d5 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php @@ -74,7 +74,7 @@ public function setUp() { // Note that we cannot use createStub here since it's not defined in PHPUnit 6.5. $this->mock_api_client = $this->getMockBuilder( 'WC_Payments_API_Client' ) ->disableOriginalConstructor() - ->setMethods( [ 'create_and_confirm_intention', 'get_payment_method', 'is_server_connected' ] ) + ->setMethods( [ 'create_and_confirm_intention', 'create_and_confirm_setup_intent', 'get_payment_method', 'is_server_connected' ] ) ->getMock(); // Arrange: Create new WC_Payments_Account instance to use later. @@ -703,6 +703,124 @@ function( $note ) use ( $intent_id, $total ) { ); } + /** + * Test processing free order with the status "requires_action". + * This is the status returned when the saved card setup requires + * further authentication with 3DS. + */ + public function test_setup_intent_status_requires_action() { + // Arrange: Reusable data. + $intent_id = 'pi_123'; + $customer_id = 'cu_123'; + $status = 'requires_action'; + $secret = 'client_secret_123'; + $order_id = 123; + $total = 0; + $currency = 'USD'; + + // Arrange: Create an order to test with. + $mock_order = $this->createMock( 'WC_Order' ); + + // Arrange: Set a good return value for order ID. + $mock_order + ->method( 'get_id' ) + ->willReturn( $order_id ); + + // Arrange: Set a good return value for order total. + $mock_order + ->method( 'get_total' ) + ->willReturn( $total ); + + // Arrange: Set currency for order total. + $mock_order + ->method( 'get_currency' ) + ->willReturn( $currency ); + + // Arrange: Set a WP_User object as a return value of order's get_user. + $mock_order + ->method( 'get_user' ) + ->willReturn( wp_get_current_user() ); + + // Arrange: Set a good return value for customer ID. + $this->mock_customer_service->expects( $this->once() ) + ->method( 'create_customer_for_user' ) + ->willReturn( $customer_id ); + + // Arrange: Create a mock cart. + $mock_cart = $this->createMock( 'WC_Cart' ); + + // Arrange: Return a 'requires_action' response from create_and_confirm_setup_intent(). + $intent = [ + 'id' => $intent_id, + 'status' => $status, + 'client_secret' => $secret, + 'next_action' => [], + ]; + $this->mock_api_client + ->expects( $this->any() ) + ->method( 'create_and_confirm_setup_intent' ) + ->will( + $this->returnValue( $intent ) + ); + + // Assert: Order charge id meta data was updated with `update_meta_data()`. + // Assert: Order does not have intention status meta data. + // Assert: Order has correct intent ID. + // This test is a little brittle because we don't really care about the order + // in which the different calls are made, but it's not possible to write it + // otherwise for now. + // There's an issue open for that here: + // https://github.com/sebastianbergmann/phpunit/issues/4026. + $mock_order + ->expects( $this->exactly( 8 ) ) + ->method( 'update_meta_data' ) + ->withConsecutive( + [ '_payment_method_id', 'pm_mock' ], + [ '_stripe_customer_id', $customer_id ], + [ '_intent_id', $intent_id ], + [ '_charge_id', '' ], + [ '_intention_status', $status ], + [ '_payment_method_id', 'pm_mock' ], + [ '_stripe_customer_id', $customer_id ], + [ WC_Payments_Utils::ORDER_INTENT_CURRENCY_META_KEY, 'USD' ] + ); + + // Assert: Order status was not updated. + $mock_order + ->expects( $this->never() ) + ->method( 'set_status' ); + + // Assert: No order note added because payment not needed. + $mock_order + ->expects( $this->never() ) + ->method( 'add_order_note' ); + + // Assert: Order has correct transaction ID set. + $mock_order + ->expects( $this->exactly( 1 ) ) + ->method( 'set_transaction_id' ) + ->with( $intent_id ); + + // Assert: empty_cart() was not called. + $mock_cart + ->expects( $this->never() ) + ->method( 'empty_cart' ); + + // Act: prepare payment information. + $payment_information = WCPay\Payment_Information::from_payment_request( $_POST, $mock_order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $payment_information->must_save_payment_method(); + + // Act: process payment. + $result = $this->mock_wcpay_gateway->process_payment_for_order( $mock_cart, $payment_information ); + + // Assert: Returning correct array. + $this->assertEquals( 'success', $result['result'] ); + $this->assertEquals( + '#wcpay-confirm-si:' . $order_id . ':' . $secret . ':' . wp_create_nonce( 'wcpay_update_order_status_nonce' ), + $result['redirect'] + ); + } + public function test_saved_card_at_checkout() { $order = WC_Helper_Order::create_order(); diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php index 7bf97d39d4c..65811b7a7a5 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php @@ -68,6 +68,7 @@ class WC_Payment_Gateway_WCPay_Subscriptions_Process_Payment_Test extends WP_Uni 'id' => self::SETUP_INTENT_ID, 'status' => 'succeeded', 'client_secret' => 'test_client_secret', + 'next_action' => [], ]; /**