diff --git a/changelog.txt b/changelog.txt index 1ec1f71d7f..20005a09d8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -7,6 +7,7 @@ * Dev - Implements WooCommerce constants for the tax statuses * Fix - Ensure all Javascript strings use the correct text domain for translation * Tweak - Use more specific selector in express checkout e2e tests +* Fix - Relax customer validation that was preventing payments from the pay for order page = 9.8.0 - 2025-08-11 = * Add - Adds the current setting value for the Optimized Checkout to the Stripe System Status Report data diff --git a/includes/class-wc-stripe-customer.php b/includes/class-wc-stripe-customer.php index 4b5c244c4b..a67b3e3a04 100644 --- a/includes/class-wc-stripe-customer.php +++ b/includes/class-wc-stripe-customer.php @@ -10,6 +10,24 @@ */ class WC_Stripe_Customer { + /** + * Constant for the customer context when adding a payment method. + */ + public const CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD = 'add_payment_method'; + + /** + * Constant for the customer context when paying for an order via the "Pay for Order" page. + */ + public const CUSTOMER_CONTEXT_PAY_FOR_ORDER = 'pay_for_order'; + + /** + * Constants for the customer contexts where minimal billing details are permitted. + */ + public const MINIMAL_BILLING_DETAILS_CONTEXTS = [ + self::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD, + self::CUSTOMER_CONTEXT_PAY_FOR_ORDER, + ]; + /** * String prefix for Stripe payment methods request transient. */ @@ -209,19 +227,19 @@ protected function generate_customer_request( $args = [] ) { /** * Validate that we have valid data before we try to create a customer. * - * @param array $create_customer_request - * @param bool $is_add_payment_method_page + * @param array $create_customer_request The base data to build the customer request. + * @param null|string $current_context Flag to indicate whether we are in a context where limited details are permitted. * * @throws WC_Stripe_Exception */ - private function validate_create_customer_request( $create_customer_request, $is_add_payment_method_page = false ) { + private function validate_create_customer_request( $create_customer_request, ?string $current_context = null ) { /** * Filters the required customer fields when creating a customer in Stripe. * * @since 9.7.0 - * @param array $required_fields The required customer fields as derived from the required billing fields in checkout. + * @param array $required_fields The required customer fields as derived from the required billing fields in checkout. In some contexts, like adding a payment method, we allow minimal details to be provided. */ - $required_fields = apply_filters( 'wc_stripe_create_customer_required_fields', $this->get_create_customer_required_fields( $is_add_payment_method_page ) ); + $required_fields = apply_filters( 'wc_stripe_create_customer_required_fields', $this->get_create_customer_required_fields( $current_context ) ); foreach ( $required_fields as $field => $field_requirements ) { if ( true === $field_requirements ) { @@ -258,13 +276,12 @@ private function validate_create_customer_request( $create_customer_request, $is /** * Get the list of required fields for the create customer request. * - * @param bool $is_add_payment_method_page + * @param string|null $current_context The context we are creating the customer in. * * @return array */ - private function get_create_customer_required_fields( $is_add_payment_method_page = false ) { - // If we are on the add payment method page, we need to check just for the email field. - if ( $is_add_payment_method_page ) { + private function get_create_customer_required_fields( ?string $current_context = null ) { + if ( in_array( $current_context, self::MINIMAL_BILLING_DETAILS_CONTEXTS, true ) ) { return [ 'email' => true, ]; @@ -422,12 +439,12 @@ public function get_existing_customer( $email, $name ) { * Create a customer via API. * * @param array $args - * @param bool $is_add_payment_method_page Whether the request is for the add payment method page. + * @param string|null $current_context The context we are creating the customer in. * @return WP_Error|int * * @throws WC_Stripe_Exception */ - public function create_customer( $args = [], $is_add_payment_method_page = false ) { + public function create_customer( $args = [], $current_context = null ) { $args = $this->generate_customer_request( $args ); // For guest users, check if a customer already exists with the same email and name in Stripe account before creating a new one. @@ -435,6 +452,9 @@ public function create_customer( $args = [], $is_add_payment_method_page = false $response = $this->get_existing_customer( $args['email'], $args['name'] ); } + // $current_context was initially introduced as a boolean flag, so check for old callers. + $current_context = $this->normalize_current_context( $current_context ); + if ( empty( $response ) ) { /** * Filters the arguments used to create a customer. @@ -445,7 +465,7 @@ public function create_customer( $args = [], $is_add_payment_method_page = false */ $create_customer_args = apply_filters( 'wc_stripe_create_customer_args', $args ); - $this->validate_create_customer_request( $create_customer_args, $is_add_payment_method_page ); + $this->validate_create_customer_request( $create_customer_args, $current_context ); $response = WC_Stripe_API::request( $create_customer_args, 'customers' ); } else { @@ -523,19 +543,45 @@ public function update_customer( $args = [], $is_retry = false ) { * Updates existing Stripe customer or creates new customer for User through API. * * @param array $args Additional arguments for the request (optional). + * @param string|null $current_context The context we are creating the customer in. * * @return string Customer ID * * @throws WC_Stripe_Exception */ - public function update_or_create_customer( $args = [], $is_add_payment_method_page = false ) { + public function update_or_create_customer( $args = [], $current_context = null ) { if ( empty( $this->get_id() ) ) { - return $this->recreate_customer( $args, $is_add_payment_method_page ); + // $current_context was initially introduced as a boolean flag, so check for old callers. + $current_context = $this->normalize_current_context( $current_context ); + + return $this->recreate_customer( $args, $current_context ); } else { return $this->update_customer( $args ); } } + /** + * Normalize the current context to a string, as the argument was initially introduced as a boolean flag. + * + * @param string|bool|null $current_context The current context. + * @return string|null The normalized context. + */ + private function normalize_current_context( $current_context ): ?string { + if ( null === $current_context ) { + return null; + } + + if ( is_bool( $current_context ) ) { + return $current_context ? self::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD : null; + } + + if ( is_string( $current_context ) ) { + return $current_context; + } + + return null; + } + /** * Checks to see if error is of invalid request * error and it is no such customer. @@ -978,13 +1024,13 @@ public function delete_id_from_meta() { * Recreates the customer for this user. * * @param array $args Additional arguments for the request (optional). - * @param bool $is_add_payment_method_page Whether the request is for the add payment method page. + * @param string|null $current_context The context we are creating the customer in. * * @return string ID of the new Customer object. */ - private function recreate_customer( $args = [], $is_add_payment_method_page = false ) { + private function recreate_customer( $args = [], ?string $current_context = null ) { $this->delete_id_from_meta(); - return $this->create_customer( $args, $is_add_payment_method_page ); + return $this->create_customer( $args, $current_context ); } /** diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index 38aff19df0..eede946884 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -1197,7 +1197,7 @@ public function create_and_confirm_setup_intent_ajax() { // Manually create the payment information array to create & confirm the setup intent. $payment_information = [ 'payment_method' => $payment_method, - 'customer' => $customer->update_or_create_customer( [], true ), + 'customer' => $customer->update_or_create_customer( [], WC_Stripe_Customer::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD ), 'selected_payment_type' => $payment_type, 'return_url' => wc_get_account_endpoint_url( 'payment-methods' ), 'use_stripe_sdk' => 'true', // We want the user to complete the next steps via the JS elements. ref https://docs.stripe.com/api/setup_intents/create#create_setup_intent-use_stripe_sdk 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 966e902b68..65bdc565f7 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -2858,10 +2858,12 @@ private function get_customer_id_for_order( WC_Order $order ): string { $user = $this->get_user_from_order( $order ); $customer = new WC_Stripe_Customer( $user->ID ); + $current_context = $this->is_valid_pay_for_order_endpoint() ? WC_Stripe_Customer::CUSTOMER_CONTEXT_PAY_FOR_ORDER : null; + // Pass the order object so we can retrieve billing details // in payment flows where it is not present in the request. $args = [ 'order' => $order ]; - return $customer->update_or_create_customer( $args ); + return $customer->update_or_create_customer( $args, $current_context ); } /** diff --git a/readme.txt b/readme.txt index 79121c90ff..2b9f1c87eb 100644 --- a/readme.txt +++ b/readme.txt @@ -117,5 +117,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o * Dev - Implements WooCommerce constants for the tax statuses * Fix - Ensure all Javascript strings use the correct text domain for translation * Tweak - Use more specific selector in express checkout e2e tests +* Fix - Relax customer validation that was preventing payments from the pay for order page [See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt). diff --git a/tests/phpunit/WC_Stripe_Customer_Test.php b/tests/phpunit/WC_Stripe_Customer_Test.php index 1215aca61f..422e06ab7a 100644 --- a/tests/phpunit/WC_Stripe_Customer_Test.php +++ b/tests/phpunit/WC_Stripe_Customer_Test.php @@ -98,21 +98,61 @@ public function provide_test_validate_create_customer_request_cases(): array { 'expected_exception_message' => 'missing_required_customer_field: address->country', 'expected_exception_string' => 'Missing required customer field: address->country', ], - 'add payment method page, all fields present and required, no overrides' => [ + 'add payment method page with boolean, all fields present and required, no overrides' => [ 'billing_fields' => [], // only email is required 'woo_billing_fields' => null, 'stripe_billing_fields' => null, 'expected_exception_message' => null, 'expected_exception_string' => null, - 'is_add_payment_method_page' => true, + 'current_context' => true, ], - 'add payment method page, email is empty string' => [ + 'add payment method page with context, all fields present and required, no overrides' => [ + 'billing_fields' => [], // only email is required + 'woo_billing_fields' => null, + 'stripe_billing_fields' => null, + 'expected_exception_message' => null, + 'expected_exception_string' => null, + 'current_context' => \WC_Stripe_Customer::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD, + ], + 'add payment method page with boolean, email is empty string' => [ + 'billing_fields' => [ 'email' => '' ], + 'woo_billing_fields' => null, + 'stripe_billing_fields' => null, + 'expected_exception_message' => 'missing_required_customer_field: email', + 'expected_exception_string' => 'Missing required customer field: email', + 'current_context' => true, + ], + 'add payment method page with context, email is empty string' => [ + 'billing_fields' => [ 'email' => '' ], + 'woo_billing_fields' => null, + 'stripe_billing_fields' => null, + 'expected_exception_message' => 'missing_required_customer_field: email', + 'expected_exception_string' => 'Missing required customer field: email', + 'current_context' => \WC_Stripe_Customer::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD, + ], + 'pay for order page, only email present and required, no overrides' => [ + 'billing_fields' => [], // only email is required + 'woo_billing_fields' => null, + 'stripe_billing_fields' => null, + 'expected_exception_message' => null, + 'expected_exception_string' => null, + 'current_context' => \WC_Stripe_Customer::CUSTOMER_CONTEXT_PAY_FOR_ORDER, + ], + 'pay for order page, only email is empty string' => [ 'billing_fields' => [ 'email' => '' ], 'woo_billing_fields' => null, 'stripe_billing_fields' => null, 'expected_exception_message' => 'missing_required_customer_field: email', 'expected_exception_string' => 'Missing required customer field: email', - 'is_add_payment_method_page' => true, + 'current_context' => \WC_Stripe_Customer::CUSTOMER_CONTEXT_PAY_FOR_ORDER, + ], + 'all fields present and required, no overrides, context is false' => [ + 'billing_fields' => [], + 'woo_billing_fields' => null, + 'stripe_billing_fields' => null, + 'expected_exception_message' => null, + 'expected_exception_string' => null, + 'current_context' => false, ], ]; } @@ -126,9 +166,9 @@ public function test_validate_create_customer_request( ?array $stripe_billing_fields = null, ?string $expected_exception_message = null, ?string $expected_exception_string = null, - ?bool $is_add_payment_method_page = false + $current_context = null ) { - if ( $is_add_payment_method_page ) { + if ( true === $current_context || in_array( $current_context, \WC_Stripe_Customer::MINIMAL_BILLING_DETAILS_CONTEXTS, true ) ) { $default_billing_data = [ 'email' => 'test@example.com', 'first_name' => '', @@ -278,7 +318,7 @@ public function test_validate_create_customer_request( } try { - $customer->create_customer( $args, $is_add_payment_method_page ); + $customer->create_customer( $args, $current_context ); } catch ( \WC_Stripe_Exception $stripe_exception ) { $was_exception_thrown = true;