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
fluent-cart / api / Resource / CustomerResource.php

CustomerResource.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.0, at api/Resource/CustomerResource.php

566 lines 20.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\Api\Resource;
4
5 use FluentCart\App\App;
6 use FluentCart\App\Helpers\AddressHelper;
7 use FluentCart\App\Helpers\Status;
8 use FluentCart\App\Models\Customer;
9 use FluentCart\App\Services\CustomerIdentity\EmailVerificationService;
10 use FluentCart\App\Services\Renderer\CheckoutFieldsSchema;
11 use FluentCart\Framework\Database\Orm\Builder;
12 use FluentCart\Framework\Database\Orm\Collection;
13 use FluentCart\Framework\Support\Arr;
14
15 class CustomerResource extends BaseResourceApi
16 {
17 /**
18 * Per-request memo for getCurrentCustomer(). In production every HTTP
19 * request runs in a fresh PHP process, so this lives exactly one
20 * request. Long-running processes that simulate multiple requests
21 * (test suites, CLI) must clear it between simulated requests via
22 * resetCurrentCustomerRuntimeCache() — as a function-static it was
23 * unreachable and leaked the first request's customer into every
24 * subsequent one.
25 *
26 * @var object|null
27 */
28 private static $currentCustomerRuntimeCache = null;
29
30 public static function resetCurrentCustomerRuntimeCache(): void
31 {
32 static::$currentCustomerRuntimeCache = null;
33 }
34
35 public static function getQuery(): Builder
36 {
37 return Customer::query();
38 }
39
40 /**
41 * Get customers based on specified parameters.
42 *
43 * @param array $params Array containing the necessary parameters.
44 * [
45 * "params" => (array) Required.
46 * [
47 * 'search' => (string) Optional.Search Customer.
48 * [
49 * "column name(e.g., first_name|last_name|email|id)" => [
50 * column => "column name(e.g., first_name|last_name|email|id)",
51 * operator => "operator (e.g., like_all|rlike|or_rlike|or_like_all)",
52 * value => "value" ]
53 * ],
54 * 'filters' => (string) Optional.Filters customer.
55 * [
56 * "column name(e.g., first_name|last_name|email)" => [
57 * column => "column name(e.g., first_name|last_name|email)",
58 * operator => "operator (e.g., between|or_between|like_all|in)",
59 * value => "value" ]
60 * ],
61 * 'order_by' => (string) Optional. Column to order by,
62 * 'order_type' => (string) Optional. Order type for sorting (ASC or DESC),
63 * 'per_page' => (int) Optional. Number of items for per page,
64 * 'page' => (int) Optional. Page number for pagination
65 * ]
66 * ]
67 *
68 */
69 public static function get(array $params = [])
70 {
71 $sortBy = Arr::get($params, 'sort_by', 'id');
72 $sortType = Arr::get($params, 'sort_type', 'DESC');
73 $search = Arr::get($params, 'search', '');
74
75 return static::getQuery()->when($search, function ($query) use ($search) {
76 return $query->searchBy($search);
77 })
78 ->applyCustomFilters(Arr::get($params, 'filters', []))
79 ->orderBy(
80 sanitize_sql_orderby($sortBy),
81 sanitize_sql_orderby($sortType))
82 ->paginate(Arr::get($params, 'per_page', 15), ['*'], 'page', Arr::get($params, 'page'));
83 }
84
85 /**
86 * Find customer by ID.
87 *
88 * @param int $id Required. The ID of the customer.
89 * @param array $params Optional. Additional parameters for finding a customer.
90 * [
91 * 'with' => (array) Optional. Relationships name to be eager loaded,
92 * ]
93 *
94 */
95 public static function find($id, $params = [])
96 {
97 $with = Arr::get($params, 'with', []);
98 $customer = Customer::with($with)->find($id);
99 if (!empty($customer) && isset($customer['labels'])) {
100 $customer['selected_labels'] = Collection::make($customer['labels'])->pluck('label_id');
101 }
102
103 return [
104 'customer' => (!empty($customer) ? $customer : null)
105 ];
106 }
107
108 public static function findOrder($id, $params = [])
109 {
110 $customer = Customer::with('orders.filteredOrderItems')->find($id);
111
112 return [
113 'data' => (!empty($customer) ? $customer->orders : null)
114 ];
115 }
116
117 /**
118 * Create a new customer with the given data
119 *
120 * @param array $data Required. Array containing the necessary parameters
121 * [
122 * 'first_name' => (string) Required. The first name of the customer,
123 * 'last_name' => (string) Optional. The last name of the customer,
124 * 'email' => (string) Required. The email of the customer,
125 * 'city' => (string) Optional. The city of the customer,
126 * 'state' => (string) Optional. The state of the customer,
127 * 'postcode' => (string) Optional. The postal code of the customer,
128 * 'country' => (string) Optional. The country of the customer,
129 * 'wp_user' => (string) Optional. Create customer as WP user,
130 * ]
131 * @param array $params Optional. Additional parameters for creating a customer.
132 *
133 */
134 public static function create($data, $params = [])
135 {
136 $email = Arr::get($data, 'email');
137 $data = static::resolveCustomerName($data);
138
139 $data['purchase_value'] = [];
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
149 if (empty($customer)) {
150 return static::makeErrorResponse([
151 ['code' => 400, 'message' => __('Customer creation failed.', 'fluent-cart')]
152 ]);
153 }
154
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 }
169 }
170
171 if (Arr::get($data, 'wp_user') === 'yes' && !$isUserAttached) {
172 $isUserCreated = \FluentCart\App\Services\AuthService::createUserFromCustomer($customer);
173 if (is_wp_error($isUserCreated)) {
174 return static::makeErrorResponse([
175 ['code' => 423, 'message' => __('Failed to create user.', 'fluent-cart')]
176 ]);
177 }
178 }
179
180 if ($customer->wasRecentlyCreated) {
181 return static::makeSuccessResponse(
182 $customer,
183 __('Customer created successfully!', 'fluent-cart')
184 );
185 }
186
187 return static::makeErrorResponse([
188 ['code' => 400, 'message' => __('Customer already exists.', 'fluent-cart')]
189 ]);
190
191
192 }
193
194 /**
195 * Update customer with the given data
196 *
197 * @param array $data Required. Array containing the necessary parameters
198 * [
199 * 'first_name' => (string) Required. The first name of the customer,
200 * 'last_name' => (string) Optional. The last name of the customer,
201 * 'email' => (string) Required. The email of the customer,
202 * 'city' => (string) Optional. The city of the customer,
203 * 'state' => (string) Optional. The state of the customer,
204 * 'postcode' => (string) Optional. The postal code of the customer,
205 * 'country' => (string) Optional. The country of the customer,
206 * ]
207 * @param int $id Required. The ID of the customer.
208 * @param array $params Optional. Additional parameters for creating a customer.
209 *
210 */
211 public static function update($data, $id, $params = [])
212 {
213 $customer = static::getQuery()->find($id);
214
215 if ($customer) {
216 $data = static::resolveCustomerName($data);
217
218 if ($customer->user_id != 0) {
219 $data['email'] = $customer->email;
220 $isUserUpdated = static::updateUser($data, $customer->user_id);
221
222 if (is_wp_error($isUserUpdated)) {
223 return static::makeErrorResponse([
224 ['code' => 423, 'message' => __('Failed to update user.', 'fluent-cart')]
225 ]);
226 }
227 }
228 $customer->update($data);
229 $customer->refresh();
230
231 if ($customer) {
232 return static::makeSuccessResponse(
233 $customer,
234 __('Customer updated successfully!', 'fluent-cart')
235 );
236 }
237
238 return static::makeErrorResponse([
239 ['code' => 400, 'message' => __('Customer update failed.', 'fluent-cart')]
240 ]);
241 }
242
243 return static::makeErrorResponse([
244 ['code' => 400, 'message' => __('Customer not found, please reload the page and try again!', 'fluent-cart')]
245 ]);
246 }
247
248 /**
249 * Delete a customer based on the given ID and parameters.
250 *
251 * @param int $id Optional. The ID of the customer.
252 * @param array $params Optional. Additional parameters for deleting multiple customers.
253 * [
254 * 'ids' => (array) Required. The array of customer IDs to be deleted.
255 * ]
256 *
257 */
258 public static function delete($id, $params = [])
259 {
260 $ids = Arr::get($params, 'ids');
261
262 $customers = static::getQuery()->with(['orders'])->whereIn('id', $ids)->get();
263
264 foreach ($customers as $customer) {
265 $customer->orders()->delete();
266 $customer->delete();
267 }
268
269 if ($customer) {
270 return static::makeSuccessResponse(
271 '',
272 __('Selected Customers has been deleted permanently', 'fluent-cart')
273 );
274 }
275
276 return static::makeErrorResponse([
277 ['code' => 400, 'message' => __('Customer update failed.', 'fluent-cart')]
278 ]);
279 }
280
281 /**
282 * Update customer additional information with the given data
283 *
284 * @param array $data Required. Array containing the necessary parameters
285 * [
286 * 'labels' => (array) Required. The id of the labels,
287 * ]
288 * @param int $id Required. The ID of the customer.
289 * @param array $params Optional. Additional parameters for updating a customer info.
290 *
291 */
292 public static function updateAdditionalInfo($data, $id, $params = [])
293 {
294 $customer = static::find($id, ['with' => ['labels']]);
295 $customer = $customer['customer'];
296
297 if ($customer) {
298 $newLabelIds = Arr::get($data, 'labels', []);
299 // Pluck and convert $existingLabelIds to a collection of strings
300 $existingLabelIds = Collection::make($customer['labels'])->pluck('label_id')->map(function ($value) {
301 return (string)$value;
302 });
303
304 if (count($newLabelIds) > 0 || count($existingLabelIds) > 0) {
305 $isUpdated = LabelResource::addLabelToLabelRelationships($customer, [
306 'labelable_id' => $id,
307 'labelable_type' => Customer::class,
308 'new_label_ids' => $newLabelIds,
309 'existing_label_ids' => $existingLabelIds
310 ]);
311
312 if ($isUpdated) {
313 return static::makeSuccessResponse(
314 $isUpdated,
315 __('Customer updated successfully!', 'fluent-cart')
316 );
317 }
318
319 return static::makeErrorResponse([
320 ['code' => 400, 'message' => __('Customer update failed.', 'fluent-cart')]
321 ], 400);
322 }
323
324 return static::makeErrorResponse([
325 ['code' => 400, 'message' => __('Customer does not have any changes to update.', 'fluent-cart')]
326 ], 400);
327 }
328
329 return static::makeErrorResponse([
330 ['code' => 404, 'message' => __('Customer not found, please reload the page and try again!', 'fluent-cart')]
331 ], 404);
332 }
333
334 /**
335 * Update the status of multiple customers with the given parameters.
336 *
337 * @param array $params Optional. Array containing the necessary parameters
338 * [
339 * 'new_status' => (string) Required. The new status to be set for the customers.
340 * 'customer_ids' => (array) Required. Customer IDs whose status will be updated.
341 * ]
342 *
343 */
344 public static function updateStatus($params = [])
345 {
346 $newStatus = Arr::get($params, 'new_status', '');
347
348 if (!$newStatus) {
349 return static::makeErrorResponse([
350 ['code' => 403, 'message' => __('Please select status', 'fluent-cart')]
351 ]);
352 }
353
354 $validStatuses = Status::getEditableCustomerStatuses();
355 if (!isset($validStatuses[$newStatus])) {
356 return static::makeErrorResponse([
357 ['code' => 403, 'message' => __('Provided customer status is not valid', 'fluent-cart')]
358 ]);
359 }
360
361 $customers = static::getQuery()->with(['orders'])->whereIn('id', Arr::get($params, 'customer_ids'))->get();
362
363 foreach ($customers as $customer) {
364 $customer->updateCustomerStatus($newStatus);
365 }
366
367 return static::makeSuccessResponse(
368 '',
369 __('Customer Status has been changed', 'fluent-cart')
370 );
371 }
372
373 /**
374 * Manage customers based on the provided action and customer IDs.
375 *
376 * @param array $params Optional. Array containing the necessary parameters
377 * [
378 * 'action' => (string) Required. The action to be performed on the selected customers.
379 * (e.g., Possible values: 'delete_customers', 'change_customer_status')
380 * 'customer_ids' => (array) Required. Customer IDs whose action will be performed.
381 * ]
382 *
383 */
384 public static function manageCustomer($params = [])
385 {
386
387 $action = Arr::get($params, 'action', '');
388 $customerIds = Arr::get($params, 'customer_ids', []);
389
390 $customerIds = array_map(function ($id) {
391 return (int)$id;
392 }, $customerIds);
393
394
395 $customerIds = array_filter($customerIds);
396
397 if (!$customerIds) {
398 return static::makeErrorResponse([
399 ['code' => 403, 'message' => __('Customers selection is required', 'fluent-cart')]
400 ]);
401 }
402
403 if ($action == 'delete_customers') {
404 return static::delete(null, ['ids' => $customerIds]);
405 }
406
407 if ($action == 'change_customer_status') {
408 return static::updateStatus($params);
409 }
410
411 return static::makeErrorResponse([
412 ['code' => 400, 'message' => __('Selected action is invalid', 'fluent-cart')]
413 ]);
414 }
415
416 public static function getCurrentCustomer(bool $createIfNotExists = false): ?object
417 {
418 if (static::$currentCustomerRuntimeCache !== null) {
419 return static::$currentCustomerRuntimeCache;
420 }
421
422 if (!is_user_logged_in()) {
423 return null;
424 }
425
426 $currentUser = get_user_by('ID', get_current_user_id());
427
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();
433
434 if (!$existingCustomer && !EmailVerificationService::isEnabled()) {
435 $existingCustomer = static::claimUnlinkedCustomerByEmail($currentUser);
436 }
437
438 if ($existingCustomer) {
439 static::$currentCustomerRuntimeCache = $existingCustomer;
440 return $existingCustomer;
441 }
442
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.
446 return null;
447 }
448
449 $userId = $currentUser->ID;
450
451 $appRequestData = App::request()->all();
452
453 $customer = Customer::query()->create([
454 'first_name' => $currentUser->first_name,
455 'last_name' => $currentUser->last_name,
456 'email' => $currentUser->user_email,
457 'user_id' => $userId,
458 'country' => Arr::get($appRequestData, 'country', ''),
459 'city' => Arr::get($appRequestData, 'city', ''),
460 'state' => Arr::get($appRequestData, 'state', ''),
461 'postcode' => Arr::get($appRequestData, 'postcode', ''),
462 ]);
463
464 // get customer by id
465 static::$currentCustomerRuntimeCache = static::getQuery()
466 ->where('id', $customer->id)
467 ->with(['billing_address', 'shipping_address'])
468 ->first();
469
470 return static::$currentCustomerRuntimeCache;
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();
496 }
497
498 private static function resolveCustomerName(array $data): array
499 {
500 if (CheckoutFieldsSchema::isFullNameRequired()) {
501 $fullName = trim(Arr::get($data, 'full_name', ''));
502 $nameParts = AddressHelper::guessFirstNameAndLastName($fullName);
503 $data['first_name'] = Arr::get($nameParts, 'first_name', '');
504 $data['last_name'] = Arr::get($nameParts, 'last_name', '');
505 } else {
506 $data['first_name'] = trim(Arr::get($data, 'first_name', ''));
507 $data['last_name'] = trim(Arr::get($data, 'last_name', ''));
508 }
509
510 return $data;
511 }
512
513 private static function updateUser($data, $userId)
514 {
515 $firstName = sanitize_text_field(Arr::get($data, 'first_name'));
516 $lastName = sanitize_text_field(Arr::get($data, 'last_name'));
517 $name = trim($firstName . ' ' . $lastName);
518 $email = sanitize_email(Arr::get($data, 'email', ''));
519
520 if (!$name) {
521 return false;
522 }
523
524 $data = array_filter([
525 'ID' => $userId,
526 'first_name' => $firstName,
527 'last_name' => $lastName,
528 'nickname' => $name,
529 'user_nicename' => $name,
530 'display_name' => $name,
531 'user_url' => Arr::get($data, 'user_url'),
532 ]);
533
534 $allowEmailUpdate = current_user_can('manage_options');
535
536 if (!$allowEmailUpdate) {
537 $currentUser = wp_get_current_user();
538 $currentEmail = strtolower($currentUser->user_email);
539
540 $targetUser = get_userdata($userId);
541 $targetEmail = $targetUser ? strtolower($targetUser->user_email) : null;
542
543 // Non-admin: allow only if editing own account
544 if ($currentEmail && $currentEmail === $targetEmail) {
545 $allowEmailUpdate = true;
546 }
547 }
548
549 if ($allowEmailUpdate) {
550 $data['user_email'] = $email;
551 $data['user_login'] = $email;
552 }
553
554 // Update basic user data
555 $result = wp_update_user($data);
556
557 if (is_wp_error($result)) {
558 return $result;
559 }
560
561 return $result;
562
563 }
564
565 }
566