Skip to content

Commit 5f46f43

Browse files
authored
Validate customer creation calls before we create empty customer details (#4464)
* Explore adding validation before we try to create a customer * Validate customer data before we create Stripe customer * Expand code to reject whitespace-only data * Use WooCommerce billing requirements * Changelog * Improve naming for filter * Fix incorrect usage of str_starts_with() * Use onlyMethods() instead of setMethods() in mock builder
1 parent abed6cd commit 5f46f43

File tree

4 files changed

+398
-3
lines changed

4 files changed

+398
-3
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* Update - Update filter names to use the wc_stripe_* prefix
1616
* Add - Show payment methods sync status on the UI
1717
* Fix - No such customer error when creating a payment method with a new Stripe account
18+
* Fix - Validate create customer payload against required billing fields before sending to Stripe
1819

1920
= 9.6.0 - 2025-07-07 =
2021
* Fix - Register Express Checkout script before use to restore buttons on “order-pay” pages

includes/class-wc-stripe-customer.php

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,92 @@ protected function generate_customer_request( $args = [] ) {
199199
return wp_parse_args( $args, $defaults );
200200
}
201201

202+
/**
203+
* Validate that we have valid data before we try to create a customer.
204+
*
205+
* @param array $create_customer_request
206+
*
207+
* @throws WC_Stripe_Exception
208+
*/
209+
private function validate_create_customer_request( $create_customer_request ) {
210+
$checkout_billing_fields = WC_Checkout::instance()->get_checkout_fields( 'billing' );
211+
$required_billing_fields = array_filter(
212+
$checkout_billing_fields,
213+
function ( $field_data ) {
214+
return $field_data['required'] ?? false;
215+
}
216+
);
217+
218+
$required_fields = [];
219+
220+
if ( isset( $required_billing_fields['billing_email'] ) ) {
221+
$required_fields['email'] = true;
222+
}
223+
224+
if ( isset( $required_billing_fields['billing_first_name'] ) || isset( $required_billing_fields['billing_last_name'] ) ) {
225+
$required_fields['name'] = true;
226+
}
227+
228+
$required_address_fields = [];
229+
$address_field_mapping = [
230+
'billing_address_1' => 'line1',
231+
'billing_address_2' => 'line2',
232+
'billing_city' => 'city',
233+
'billing_country' => 'country',
234+
'billing_postcode' => 'postal_code',
235+
'billing_state' => 'state',
236+
];
237+
238+
foreach ( $address_field_mapping as $field => $stripe_field_name ) {
239+
if ( isset( $required_billing_fields[ $field ] ) ) {
240+
$required_address_fields[ $stripe_field_name ] = true;
241+
}
242+
}
243+
244+
if ( [] !== $required_address_fields ) {
245+
$required_fields['address'] = $required_address_fields;
246+
}
247+
248+
/**
249+
* Filters the required customer fields when creating a customer in Stripe.
250+
*
251+
* @since 9.7.0
252+
* @param array $required_fields The required customer fields as derived from the required billing fields in checkout.
253+
*/
254+
$required_fields = apply_filters( 'wc_stripe_create_customer_required_fields', $required_fields );
255+
256+
foreach ( $required_fields as $field => $field_requirements ) {
257+
if ( true === $field_requirements ) {
258+
if ( empty( trim( $create_customer_request[ $field ] ?? '' ) ) ) {
259+
throw new WC_Stripe_Exception(
260+
sprintf( 'missing_required_customer_field: %s', $field ),
261+
/* translators: %s is a field name, e.g. 'email' or 'name'. */
262+
sprintf( __( 'Missing required customer field: %s', 'woocommerce-gateway-stripe' ), $field )
263+
);
264+
}
265+
}
266+
if ( is_array( $field_requirements ) ) {
267+
if ( ! isset( $create_customer_request[ $field ] ) || ! is_array( $create_customer_request[ $field ] ) ) {
268+
throw new WC_Stripe_Exception(
269+
sprintf( 'missing_required_customer_field: %s', $field ),
270+
/* translators: %s is a field name, e.g. 'email' or 'name'. */
271+
sprintf( __( 'Missing required customer field: %s', 'woocommerce-gateway-stripe' ), $field )
272+
);
273+
}
274+
275+
foreach ( $field_requirements as $sub_field => $sub_field_requirements ) {
276+
if ( true === $sub_field_requirements && empty( trim( $create_customer_request[ $field ][ $sub_field ] ?? '' ) ) ) {
277+
throw new WC_Stripe_Exception(
278+
sprintf( 'missing_required_customer_field: %s->%s', $field, $sub_field ),
279+
/* translators: %1$s is a field name, e.g. address, and %2$s is a secondary field name, e.g. line1 or city. */
280+
sprintf( __( 'Missing required customer field: %1$s->%2$s', 'woocommerce-gateway-stripe' ), $field, $sub_field )
281+
);
282+
}
283+
}
284+
}
285+
}
286+
}
287+
202288
/**
203289
* Get value of billing data field, either from POST or order object.
204290
*
@@ -311,6 +397,8 @@ public function get_existing_customer( $email, $name ) {
311397
*
312398
* @param array $args
313399
* @return WP_Error|int
400+
*
401+
* @throws WC_Stripe_Exception
314402
*/
315403
public function create_customer( $args = [] ) {
316404
$args = $this->generate_customer_request( $args );
@@ -321,9 +409,24 @@ public function create_customer( $args = [] ) {
321409
}
322410

323411
if ( empty( $response ) ) {
324-
$response = WC_Stripe_API::request( apply_filters( 'wc_stripe_create_customer_args', $args ), 'customers' );
412+
/**
413+
* Filters the arguments used to create a customer.
414+
*
415+
* @since 4.0.0
416+
*
417+
* @param array $args The arguments used to create a customer.
418+
*/
419+
$create_customer_args = apply_filters( 'wc_stripe_create_customer_args', $args );
420+
421+
$this->validate_create_customer_request( $create_customer_args );
422+
423+
$response = WC_Stripe_API::request( $create_customer_args, 'customers' );
325424
} else {
326-
$response = WC_Stripe_API::request( apply_filters( 'wc_stripe_update_customer_args', $args ), 'customers/' . $response->id );
425+
/**
426+
* This filter is documented in includes/class-wc-stripe-customer.php.
427+
*/
428+
$update_customer_args = apply_filters( 'wc_stripe_update_customer_args', $args );
429+
$response = WC_Stripe_API::request( $update_customer_args, 'customers/' . $response->id );
327430
}
328431

329432
if ( ! empty( $response->error ) ) {
@@ -358,7 +461,15 @@ public function update_customer( $args = [], $is_retry = false ) {
358461
throw new WC_Stripe_Exception( 'id_required_to_update_user', __( 'Attempting to update a Stripe customer without a customer ID.', 'woocommerce-gateway-stripe' ) );
359462
}
360463

361-
$args = $this->generate_customer_request( $args );
464+
$args = $this->generate_customer_request( $args );
465+
466+
/**
467+
* Filters the arguments used to update a customer.
468+
*
469+
* @since 4.3.1
470+
*
471+
* @param array $args The arguments used to update a customer.
472+
*/
362473
$args = apply_filters( 'wc_stripe_update_customer_args', $args );
363474
$response = WC_Stripe_API::request( $args, 'customers/' . $this->get_id() );
364475

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
125125
* Update - Update filter names to use the wc_stripe_* prefix
126126
* Add - Show payment methods sync status on the UI
127127
* Fix - No such customer error when creating a payment method with a new Stripe account
128+
* Fix - Validate create customer payload against required billing fields before sending to Stripe
128129

129130

130131
[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).

0 commit comments

Comments
 (0)