diff --git a/includes/abstracts/abstract-wc-stripe-payment-gateway.php b/includes/abstracts/abstract-wc-stripe-payment-gateway.php index 6e96d1c820..e9e257f86d 100644 --- a/includes/abstracts/abstract-wc-stripe-payment-gateway.php +++ b/includes/abstracts/abstract-wc-stripe-payment-gateway.php @@ -1743,7 +1743,7 @@ private function get_intent( $intent_type, $intent_id ) { throw new Exception( "Failed to get intent of type $intent_type. Type is not allowed" ); } - $response = WC_Stripe_API::request( [], "$intent_type/$intent_id?expand[]=payment_method", 'GET' ); + $response = WC_Stripe_API::request( [ 'stripe_expand' => [ 'payment_method' ] ], "$intent_type/$intent_id", 'GET' ); if ( $response && isset( $response->{ 'error' } ) ) { $error_response_message = print_r( $response, true ); diff --git a/includes/class-wc-stripe-api.php b/includes/class-wc-stripe-api.php index 4aaa84d30e..9a14606fad 100644 --- a/includes/class-wc-stripe-api.php +++ b/includes/class-wc-stripe-api.php @@ -202,7 +202,7 @@ public static function get_idempotency_key( $api, $method, $request ) { * * @since 3.1.0 * @version 4.0.6 - * @param array $request + * @param array $request Note that there is special handling for the `stripe_expand` parameter, which is expected to be an array of field names to expand. * @param string $api * @param string $method * @param bool $with_headers To get the response with headers. @@ -217,6 +217,11 @@ public static function request( $request, $api = 'charges', $method = 'POST', $w $headers['Idempotency-Key'] = $idempotency_key; } + [ + 'request' => $request, + 'expand_params' => $expand_params, + ] = self::extract_expand_params( $request ); + $request = apply_filters_deprecated( 'woocommerce_stripe_request_body', [ $request, $api ], @@ -230,13 +235,23 @@ public static function request( $request, $api = 'charges', $method = 'POST', $w * * @since 9.7.0 * - * @param array $request The default request body we will send to the Stripe API. - * @param string $api The Stripe API endpoint. + * @param array $request The default request body we will send to the Stripe API. + * @param string $api The Stripe API endpoint. + * @param string[] $expand_params The parameters to send as `expand[]` URL parameters in the request. */ - $request = apply_filters( 'wc_stripe_request_body', $request, $api ); + $request = apply_filters( 'wc_stripe_request_body', $request, $api, $expand_params ); + + $debug_params = [ + 'request' => $request, + ]; + if ( is_array( $expand_params ) ) { + $debug_params['expand_params'] = $expand_params; + } // Log the request after the filters have been applied. - WC_Stripe_Logger::debug( "Stripe API request: {$method} {$api}", [ 'request' => $request ] ); + WC_Stripe_Logger::debug( "Stripe API request: {$method} {$api}", $debug_params ); + + $api = self::add_expand_params_to_api( $api, $expand_params ); $response = wp_safe_remote_post( self::ENDPOINT . $api, @@ -283,9 +298,10 @@ public static function request( $request, $api = 'charges', $method = 'POST', $w * * @since 4.0.0 * @version 4.0.0 - * @param string $api + * @param string $api The Stripe API we want to call. + * @param string[]|null $expand_params The parameters to send as `expand[]` URL parameters in the request. */ - public static function retrieve( $api ) { + public static function retrieve( $api, $expand_params = null ) { // If keep count of consecutive 401 errors, and it exceeds INVALID_API_KEY_ERROR_COUNT_THRESHOLD, // we return null until the cache expires (INVALID_API_KEY_ERROR_COUNT_CACHE_TIMEOUT) or the keys are updated. $invalid_api_key_error_count = WC_Stripe_Database_Cache::get( self::INVALID_API_KEY_ERROR_COUNT_CACHE_KEY ); @@ -300,7 +316,14 @@ public static function retrieve( $api ) { return null; } - WC_Stripe_Logger::debug( "Stripe API request: GET {$api}" ); + $debug_params = []; + if ( is_array( $expand_params ) ) { + $debug_params['expand_params'] = $expand_params; + } + + WC_Stripe_Logger::debug( "Stripe API request: GET {$api}", $debug_params ); + + $api = self::add_expand_params_to_api( $api, $expand_params ); $response = wp_safe_remote_get( self::ENDPOINT . $api, @@ -597,4 +620,35 @@ public function update_payment_method_configurations( $id, $payment_method_confi ); return $response; } + + private static function extract_expand_params( array $request ): array { + if ( ! isset( $request['stripe_expand'] ) || ! is_array( $request['stripe_expand'] ) || [] === $request['stripe_expand'] ) { + return [ + 'request' => $request, + 'expand_params' => null, + ]; + } + + $expand_params = $request['stripe_expand']; + unset( $request['stripe_expand'] ); + + return [ + 'request' => $request, + 'expand_params' => $expand_params, + ]; + } + + private static function add_expand_params_to_api( string $api, ?array $expand_params ): string { + if ( ! is_array( $expand_params ) ) { + return $api; + } + + $expand_url_param = 'expand[]=' . implode( '&expand[]=', array_map( 'rawurlencode', $expand_params ) ); + // The API shouldn't include `?` already, but check just in case. + if ( str_contains( $api, '?' ) ) { + return $api . '&' . $expand_url_param; + } + + return $api . '?' . $expand_url_param; + } } diff --git a/includes/class-wc-stripe-customer.php b/includes/class-wc-stripe-customer.php index 4b5c244c4b..0e859b6eab 100644 --- a/includes/class-wc-stripe-customer.php +++ b/includes/class-wc-stripe-customer.php @@ -714,14 +714,15 @@ public function get_payment_methods( $payment_method_type ) { $payment_methods = get_transient( self::PAYMENT_METHODS_TRANSIENT_KEY . $payment_method_type . $this->get_id() ); if ( false === $payment_methods ) { - $params = WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID === $payment_method_type ? '?expand[]=data.sepa_debit.generated_from.charge&expand[]=data.sepa_debit.generated_from.setup_attempt' : ''; - $response = WC_Stripe_API::request( + $expand_params = WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID === $payment_method_type ? [ 'data.sepa_debit.generated_from.charge', 'data.sepa_debit.generated_from.setup_attempt' ] : null; + $response = WC_Stripe_API::request( [ - 'customer' => $this->get_id(), - 'type' => $payment_method_type, - 'limit' => self::PAYMENT_METHODS_API_LIMIT, + 'customer' => $this->get_id(), + 'type' => $payment_method_type, + 'limit' => self::PAYMENT_METHODS_API_LIMIT, + 'stripe_expand' => $expand_params, ], - 'payment_methods' . $params, + 'payment_methods', 'GET' ); @@ -769,15 +770,19 @@ public function get_all_payment_methods( array $payment_method_types = [], int $ do { $request_params = [ - 'customer' => $this->get_id(), - 'limit' => self::PAYMENT_METHODS_API_LIMIT, + 'customer' => $this->get_id(), + 'limit' => self::PAYMENT_METHODS_API_LIMIT, + 'stripe_expand' => [ + 'data.sepa_debit.generated_from.charge', + 'data.sepa_debit.generated_from.setup_attempt', + ], ]; if ( $last_payment_method_id ) { $request_params['starting_after'] = $last_payment_method_id; } - $response = WC_Stripe_API::request( $request_params, 'payment_methods?expand[]=data.sepa_debit.generated_from.charge&expand[]=data.sepa_debit.generated_from.setup_attempt', 'GET' ); + $response = WC_Stripe_API::request( $request_params, 'payment_methods', 'GET' ); if ( ! empty( $response->error ) ) { if ( diff --git a/includes/class-wc-stripe-order-handler.php b/includes/class-wc-stripe-order-handler.php index b06a0632ca..c7866224af 100644 --- a/includes/class-wc-stripe-order-handler.php +++ b/includes/class-wc-stripe-order-handler.php @@ -314,8 +314,8 @@ public function capture_payment( $order_id ) { $level3_data = $this->get_level3_data_from_order( $order ); $result = WC_Stripe_API::request_with_level3_data( [ - 'amount' => WC_Stripe_Helper::get_stripe_amount( $order_total ), - 'expand[]' => 'charges.data.balance_transaction', + 'amount' => WC_Stripe_Helper::get_stripe_amount( $order_total ), + 'stripe_expand' => [ 'charges.data.balance_transaction' ], ], 'payment_intents/' . $intent->id . '/capture', $level3_data, @@ -345,8 +345,8 @@ public function capture_payment( $order_id ) { $level3_data = $this->get_level3_data_from_order( $order ); $result = WC_Stripe_API::request_with_level3_data( [ - 'amount' => WC_Stripe_Helper::get_stripe_amount( $order_total ), - 'expand[]' => 'balance_transaction', + 'amount' => WC_Stripe_Helper::get_stripe_amount( $order_total ), + 'stripe_expand' => [ 'balance_transaction' ], ], 'charges/' . $charge . '/capture', $level3_data, diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index 11d0f2b29e..57b1906a6b 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -1558,7 +1558,7 @@ private function is_order_associated_to_setup_intent( int $order_id, string $int return false; } - $intent = $this->stripe_request( 'setup_intents/' . $intent_id . '?expand[]=payment_method.billing_details' ); + $intent = $this->stripe_request( 'setup_intents/' . $intent_id, [ 'stripe_expand' => [ 'payment_method.billing_details' ] ] ); if ( ! $intent ) { return false; } @@ -1646,10 +1646,10 @@ public function process_order_for_confirmed_intent( $order, $intent_id, $save_pa // Get payment intent to confirm status. if ( $payment_needed ) { - $intent = $this->stripe_request( 'payment_intents/' . $intent_id . '?expand[]=payment_method' ); + $intent = $this->stripe_request( 'payment_intents/' . $intent_id, [ 'stripe_expand' => [ 'payment_method' ] ] ); $error = isset( $intent->last_payment_error ) ? $intent->last_payment_error : false; } else { - $intent = $this->stripe_request( 'setup_intents/' . $intent_id . '?expand[]=payment_method&expand[]=latest_attempt' ); + $intent = $this->stripe_request( 'setup_intents/' . $intent_id, [ 'stripe_expand' => [ 'payment_method', 'latest_attempt' ] ] ); $error = isset( $intent->last_setup_error ) ? $intent->last_setup_error : false; } @@ -2166,7 +2166,7 @@ private function is_setup_intent_success_creation_redirection() { */ public function create_token_from_setup_intent( $setup_intent_id, $user ) { try { - $setup_intent = $this->stripe_request( 'setup_intents/' . $setup_intent_id . '?&expand[]=latest_attempt' ); + $setup_intent = $this->stripe_request( 'setup_intents/' . $setup_intent_id, [ 'stripe_expand' => [ 'latest_attempt' ] ] ); if ( ! empty( $setup_intent->last_payment_error ) ) { throw new WC_Stripe_Exception( __( "We're not able to add this payment method. Please try again later.", 'woocommerce-gateway-stripe' ) ); } @@ -2227,6 +2227,11 @@ protected function stripe_request( $path, $params = null, $order = null, $method if ( is_null( $params ) ) { return WC_Stripe_API::retrieve( $path ); } + + if ( is_array( $params ) && [ 'stripe_expand' ] === array_keys( $params ) ) { + return WC_Stripe_API::retrieve( $path, $params['stripe_expand'] ); + } + if ( ! is_null( $order ) ) { $level3_data = $this->get_level3_data_from_order( $order ); return WC_Stripe_API::request_with_level3_data( $params, $path, $level3_data, $order ); diff --git a/tests/phpunit/Helpers/WC_Helper_Stripe_Api.php b/tests/phpunit/Helpers/WC_Helper_Stripe_Api.php index 66f10ce8be..d96f16aca0 100644 --- a/tests/phpunit/Helpers/WC_Helper_Stripe_Api.php +++ b/tests/phpunit/Helpers/WC_Helper_Stripe_Api.php @@ -62,11 +62,12 @@ public static function reset() { /** * Retrieve data. This is the equivalent mock for WC_Stripe_API::retrieve * - * @param string data type + * @param string $key The Stripe API we want to call. + * @param string[]|null $expand_params The parameters to send as `expand[]` URL parameters in the request. * * @return array retrieved data mock */ - public static function retrieve( $key = 'account' ) { + public static function retrieve( $key = 'account', $expand_params = null ) { return self::$retrieve_response; } diff --git a/tests/phpunit/PaymentMethods/WC_Stripe_UPE_Payment_Gateway_Test.php b/tests/phpunit/PaymentMethods/WC_Stripe_UPE_Payment_Gateway_Test.php index 5cc8b95dcc..379634e1e9 100644 --- a/tests/phpunit/PaymentMethods/WC_Stripe_UPE_Payment_Gateway_Test.php +++ b/tests/phpunit/PaymentMethods/WC_Stripe_UPE_Payment_Gateway_Test.php @@ -954,7 +954,7 @@ public function test_process_redirect_payment_returns_valid_response() { ); $this->mock_gateway->expects( $this->once() ) ->method( 'stripe_request' ) - ->with( "payment_intents/$payment_intent_id?expand[]=payment_method" ) + ->with( "payment_intents/$payment_intent_id", [ 'stripe_expand' => [ 'payment_method' ] ] ) ->will( $this->returnValue( $this->array_to_object( $payment_intent_mock ) @@ -1017,7 +1017,7 @@ public function test_process_redirect_payment_only_runs_once() { $this->mock_gateway->expects( $this->once() ) ->method( 'stripe_request' ) - ->with( "payment_intents/$payment_intent_id?expand[]=payment_method" ) + ->with( "payment_intents/$payment_intent_id", [ 'stripe_expand' => [ 'payment_method' ] ] ) ->will( $this->returnValue( $this->array_to_object( $payment_intent_mock ) @@ -1102,7 +1102,7 @@ public function test_process_redirect_payment_locks_order() { // Expect the process to bail early. $this->mock_gateway->expects( $this->never() ) ->method( 'stripe_request' ) - ->with( "payment_intents/$payment_intent_id?expand[]=payment_method" ); + ->with( "payment_intents/$payment_intent_id", [ 'stripe_expand' => [ 'payment_method' ] ] ); $this->mock_gateway->process_upe_redirect_payment( $order_id, $payment_intent_id, false ); } @@ -1139,7 +1139,7 @@ public function test_checkout_without_payment_uses_setup_intents() { ); $this->mock_gateway->expects( $this->once() ) ->method( 'stripe_request' ) - ->with( "setup_intents/$setup_intent_id?expand[]=payment_method&expand[]=latest_attempt" ) + ->with( "setup_intents/$setup_intent_id", [ 'stripe_expand' => [ 'payment_method', 'latest_attempt' ] ] ) ->will( $this->returnValue( $this->array_to_object( $setup_intent_mock ) @@ -1190,7 +1190,7 @@ public function test_checkout_saves_payment_method_to_order() { ); $this->mock_gateway->expects( $this->once() ) ->method( 'stripe_request' ) - ->with( "payment_intents/$payment_intent_id?expand[]=payment_method" ) + ->with( "payment_intents/$payment_intent_id", [ 'stripe_expand' => [ 'payment_method' ] ] ) ->will( $this->returnValue( $this->array_to_object( $payment_intent_mock ) @@ -2280,7 +2280,7 @@ public function test_pre_order_payment_is_successful() { $this->mock_gateway->expects( $this->once() ) ->method( 'stripe_request' ) - ->with( "payment_intents/$payment_intent_id?expand[]=payment_method" ) + ->with( "payment_intents/$payment_intent_id", [ 'stripe_expand' => [ 'payment_method' ] ] ) ->will( $this->returnValue( $this->array_to_object( $payment_intent_mock ) @@ -2369,7 +2369,7 @@ public function test_pre_order_without_payment_uses_setup_intents() { $this->mock_gateway->expects( $this->once() ) ->method( 'stripe_request' ) - ->with( "setup_intents/$setup_intent_id?expand[]=payment_method&expand[]=latest_attempt" ) + ->with( "setup_intents/$setup_intent_id", [ 'stripe_expand' => [ 'payment_method', 'latest_attempt' ] ] ) ->will( $this->returnValue( $this->array_to_object( $setup_intent_mock )