Skip to content

Commit 0f16c7c

Browse files
authored
fix: prevent duplicate WP users on checkout retry (GH#903) (#911)
On checkout retry, the elseif guard at maybe_create_customer() returned 'email_exists' for ANY existing WP user with that email — including users whose checkout had previously failed before the customer record was saved (orphaned WP user). This blocked all retries for those users. Changes: 1. inc/checkout/class-checkout.php — narrow the block to only fire when the existing WP user also has a customer record. When no customer record exists (orphaned/partial state), pass the existing user's ID to wu_create_customer() so it skips WP user creation and links the new customer to the existing user. 2. inc/functions/customer.php — sanitize the email before the get_user_by() duplicate check so both the check and the subsequent wpmu_create_user() call operate on the same normalized value, preventing format-based mismatches. 3. tests/WP_Ultimo/Functions/Customer_Functions_Test.php — two new tests: - test_create_customer_reuses_existing_wp_user_on_retry: confirms that wu_create_customer() reuses the pre-existing WP user and creates exactly one wp_users row for a given email. - test_create_customer_prevents_duplicate_customer_for_same_user: confirms that a second wu_create_customer() call for the same user_id does not create a second customer row. Fixes #903
1 parent 55b2722 commit 0f16c7c

3 files changed

Lines changed: 103 additions & 2 deletions

File tree

inc/checkout/class-checkout.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,8 +1096,23 @@ protected function maybe_create_customer() {
10961096
'email' => wp_get_current_user()->user_email,
10971097
'email_verification' => 'verified',
10981098
];
1099-
} elseif (isset($customer_data['email']) && get_user_by('email', $customer_data['email'])) {
1100-
return new \WP_Error('email_exists', __('The email address you entered is already in use.', 'ultimate-multisite'));
1099+
} elseif (isset($customer_data['email']) && ($existing_wp_user = get_user_by('email', $customer_data['email']))) {
1100+
/*
1101+
* A WP user already exists with this email.
1102+
*
1103+
* Only block checkout when a customer record also exists for
1104+
* that user — the email is genuinely in use. If no customer
1105+
* exists yet, the previous checkout attempt created the WP
1106+
* user but failed before saving the customer (partial /
1107+
* orphaned state). Allow the retry to proceed by passing the
1108+
* existing user's ID to wu_create_customer(), which will skip
1109+
* WP user creation and link the new customer to it instead.
1110+
*/
1111+
if (wu_get_customer_by_user_id($existing_wp_user->ID)) {
1112+
return new \WP_Error('email_exists', __('The email address you entered is already in use.', 'ultimate-multisite'));
1113+
}
1114+
1115+
$customer_data['user_id'] = $existing_wp_user->ID;
11011116
}
11021117

11031118
/*

inc/functions/customer.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,17 @@ function wu_create_customer($customer_data) {
142142
]
143143
);
144144

145+
/*
146+
* Sanitize the email early so the duplicate check and the
147+
* subsequent wpmu_create_user() / register_new_user() call both
148+
* operate on the same normalized value. Without this, subtle
149+
* format differences (e.g. trailing whitespace) can cause
150+
* get_user_by() to miss an existing user and create a duplicate.
151+
*/
152+
if ($customer_data['email']) {
153+
$customer_data['email'] = sanitize_email($customer_data['email']);
154+
}
155+
145156
$user = get_user_by('email', $customer_data['email']);
146157

147158
if ( ! $user) {

tests/WP_Ultimo/Functions/Customer_Functions_Test.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,79 @@ public function test_get_customer_gateway_id_no_memberships(): void {
248248

249249
$this->assertEquals('', $result);
250250
}
251+
252+
/**
253+
* Test wu_create_customer reuses existing WP user instead of creating a duplicate.
254+
*
255+
* Scenario: a previous checkout attempt created a WP user but failed before
256+
* saving the customer record (orphaned user). On retry, wu_create_customer()
257+
* must reuse the existing user rather than calling wpmu_create_user() again.
258+
*/
259+
public function test_create_customer_reuses_existing_wp_user_on_retry(): void {
260+
261+
$email = 'retry-test-' . uniqid() . '@example.com';
262+
$user_id = self::factory()->user->create(['user_email' => $email]);
263+
264+
// Confirm no customer exists for this user yet (simulates orphaned state).
265+
$this->assertFalse(wu_get_customer_by_user_id($user_id));
266+
267+
// Now call wu_create_customer() with the same email.
268+
$customer = wu_create_customer([
269+
'email' => $email,
270+
'username' => 'retryuser',
271+
'password' => 'Str0ngP@ss!',
272+
'skip_validation' => true,
273+
]);
274+
275+
$this->assertNotWPError($customer);
276+
$this->assertInstanceOf(\WP_Ultimo\Models\Customer::class, $customer);
277+
278+
// The customer must be linked to the pre-existing WP user, not a new one.
279+
$this->assertSame($user_id, $customer->get_user_id());
280+
281+
// Confirm only one WP user exists with this email.
282+
$users_with_email = get_users(['search' => $email, 'search_columns' => ['user_email']]);
283+
$this->assertCount(1, $users_with_email);
284+
}
285+
286+
/**
287+
* Test wu_create_customer returns WP_Error when email belongs to a user
288+
* who already has a customer record (genuine email-in-use case).
289+
*
290+
* This is tested at the checkout layer (maybe_create_customer), but we also
291+
* verify wu_create_customer itself does not create a duplicate customer row.
292+
*/
293+
public function test_create_customer_prevents_duplicate_customer_for_same_user(): void {
294+
295+
$user_id = self::factory()->user->create();
296+
297+
// First customer creation for this user must succeed.
298+
$first = wu_create_customer([
299+
'user_id' => $user_id,
300+
'skip_validation' => true,
301+
]);
302+
303+
$this->assertNotWPError($first);
304+
$this->assertInstanceOf(\WP_Ultimo\Models\Customer::class, $first);
305+
306+
// A second call for the same user_id must not create a second customer row.
307+
wu_create_customer([
308+
'user_id' => $user_id,
309+
'skip_validation' => true,
310+
]);
311+
312+
// wu_create_customer reuses the existing user; save() may return an error
313+
// for the duplicate or the existing customer object — either is acceptable.
314+
// What must NOT happen is a second customer row being saved.
315+
316+
// There should still be exactly one customer linked to this user.
317+
$customers_for_user = array_filter(
318+
wu_get_customers(),
319+
function ($c) use ($user_id) {
320+
return (int) $c->get_user_id() === (int) $user_id;
321+
}
322+
);
323+
324+
$this->assertCount(1, $customers_for_user);
325+
}
251326
}

0 commit comments

Comments
 (0)