PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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 1.3.19 trunk All 48 releases
fluent-cart / api / Resource / CustomerResource.php

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

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