PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 All 50 releases
← All changes | api/Resource/CustomerResource.php +58 -23 1.6.1 → 1.7.0 View file →
@@ -5,8 +5,9 @@
5 5 use FluentCart\App\App;
6 6 use FluentCart\App\Helpers\AddressHelper;
7 7 use FluentCart\App\Helpers\Status;
8 8 use FluentCart\App\Models\Customer;
9 +use FluentCart\App\Services\CustomerIdentity\EmailVerificationService;
9 10 use FluentCart\App\Services\Renderer\CheckoutFieldsSchema;
10 11 use FluentCart\Framework\Database\Orm\Builder;
11 12 use FluentCart\Framework\Database\Orm\Collection;
12 13 use FluentCart\Framework\Support\Arr;
@@ -135,13 +136,17 @@
135 136 $email = Arr::get($data, 'email');
136 137 $data = static::resolveCustomerName($data);
137 138
138 139 $data['purchase_value'] = [];
139 - $customer = static::getQuery()->firstOrCreate(
140 - ['email' => $email],
141 - $data
142 - );
143 140
141 + // Preserve an established account link; otherwise reuse the email row
142 + // without linking it. Only the verification flow can claim guest history.
143 + $ownerId = (int) Arr::get($data, 'user_id');
144 + $customer = $ownerId ? static::getQuery()->where('user_id', $ownerId)->orderBy('id')->first() : null;
145 + if (!$customer) {
146 + $customer = static::getQuery()->firstOrCreate(['email' => $email], $data);
147 + }
148 +
144 149 if (empty($customer)) {
145 150 return static::makeErrorResponse([
146 151 ['code' => 400, 'message' => __('Customer creation failed.', 'fluent-cart')]
147 152 ]);
@@ -146,13 +151,22 @@
146 151 ['code' => 400, 'message' => __('Customer creation failed.', 'fluent-cart')]
147 152 ]);
148 153 }
149 154
150 - $isUserAttached = false;
151 - $user = get_user_by('email', $email);
152 - if ($user) {
153 - $customer->update(['user_id' => $user->ID]);
154 - $isUserAttached = true;
155 + // Linking happens only where identity is established. A row that
156 + // already existed is never claimed here: firstOrCreate() may have found
157 + // somebody else's record by its address. A fresh row is linked to the
158 + // account holding its email only for an actor with authority over that
159 + // account (an admin screen, the MCP tools) or when the caller supplied
160 + // the user_id it established itself (a signed-in checkout, the User
161 + // API). An anonymous caller — a guest at checkout — links nothing.
162 + $isUserAttached = (bool) $customer->user_id;
163 + if ($customer->wasRecentlyCreated && !$isUserAttached) {
164 + $user = get_user_by('email', $email);
165 + if ($user && get_current_user_id() && current_user_can('edit_user', $user->ID)) {
166 + $customer->update(['user_id' => $user->ID]);
167 + $isUserAttached = true;
168 + }
155 169 }
156 170
157 171 if (Arr::get($data, 'wp_user') === 'yes' && !$isUserAttached) {
158 172 $isUserCreated = \FluentCart\App\Services\AuthService::createUserFromCustomer($customer);
@@ -410,29 +424,26 @@
410 424 }
411 425
412 426 $currentUser = get_user_by('ID', get_current_user_id());
413 427
414 - // Try to get the existing customer
415 - $query = Customer::query()->where('user_id', $currentUser->ID)
416 - ->orWhere('email', $currentUser->user_email)
417 - ->with(['billing_address', 'shipping_address']);
428 + // With verification enabled, reading the current customer must not claim a record by email.
429 + $existingCustomer = Customer::query()->where('user_id', $currentUser->ID)
430 + ->orderBy('id', 'ASC')
431 + ->with(['billing_address', 'shipping_address'])
432 + ->first();
418 433
434 + if (!$existingCustomer && !EmailVerificationService::isEnabled()) {
435 + $existingCustomer = static::claimUnlinkedCustomerByEmail($currentUser);
436 + }
419 437
420 - $existingCustomer = $query->first();
421 -
422 - // Return if found
423 438 if ($existingCustomer) {
424 - if ($existingCustomer->user_id != $currentUser->ID) {
425 - // Update the user_id if it doesn't match
426 - $existingCustomer->user_id = $currentUser->ID;
427 - $existingCustomer->save();
428 - }
429 -
430 439 static::$currentCustomerRuntimeCache = $existingCustomer;
431 440 return $existingCustomer;
432 441 }
433 442
434 - if (!$createIfNotExists) {
443 + if (!$createIfNotExists || (EmailVerificationService::isEnabled() && Customer::query()->where('email', $currentUser->user_email)->exists())) {
444 + // With verification enabled, confirmation links an existing guest row.
445 + // Otherwise an explicitly requested profile is separate from guest history.
435 446 return null;
436 447 }
437 448
438 449 $userId = $currentUser->ID;
@@ -457,8 +468,32 @@
457 468 ->first();
458 469
459 470 return static::$currentCustomerRuntimeCache;
460 471
472 + }
473 +
474 + private static function claimUnlinkedCustomerByEmail(\WP_User $user): ?Customer
475 + {
476 + if (!$user->user_email) {
477 + return null;
478 + }
479 +
480 + $unlinked = Customer::query()->where('email', $user->user_email)
481 + ->unclaimed()
482 + ->orderBy('id', 'ASC')
483 + ->first();
484 +
485 + if (!$unlinked) {
486 + return null;
487 + }
488 +
489 + Customer::query()->where('id', $unlinked->id)->unclaimed()->update(['user_id' => $user->ID]);
490 +
491 + // A concurrent request for the same account may have won the update.
492 + return Customer::query()->where('user_id', $user->ID)
493 + ->orderBy('id', 'ASC')
494 + ->with(['billing_address', 'shipping_address'])
495 + ->first();
461 496 }
462 497
463 498 private static function resolveCustomerName(array $data): array
464 499 {