PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.3.1
Fluent Support – Helpdesk & Customer Support Ticket System v2.3.1
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
fluent-support / app / Models / Traits / CustomerTrait.php

CustomerTrait.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.3.1, at app/Models/Traits/CustomerTrait.php

553 lines 18.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Models\Traits;
4
5 use Exception;
6 use FluentSupport\App\Models\Ticket;
7 use FluentSupport\App\Services\Helper;
8 use FluentSupport\Framework\Support\Arr;
9 use FluentSupport\App\Services\ProfileInfoService;
10
11 trait CustomerTrait
12 {
13 /**
14 * Identity columns that must never be mass-assigned on update.
15 *
16 * On create these are forced by the model's `creating` hook, but `update()`
17 * bypasses that hook, so they have to be filtered out here instead.
18 *
19 * @var array
20 */
21 private static $immutableUpdateKeys = [
22 'hash', // portal identity token
23 'user_id', // WordPress user linkage
24 'person_type', // customer/agent record type, enforces the global scope
25 ];
26
27 /**
28 * This getCustomers method will return all customers
29 * @param string $search
30 * @param string $status
31 * @return object
32 */
33 public function getCustomers($search, $status=false)
34 {
35 $customersQuery = static::latest();
36
37 if ( $search ) {
38 $customersQuery = $customersQuery->searchBy($search);
39 }
40
41 if ($status && $status != 'all') {
42 $customersQuery->filterByStatues([$status]);
43 }
44
45 $customers = $customersQuery->paginate();
46
47 if ( defined( 'FLUENTCRM' ) && $customers->isEmpty() && !$status ) {
48 $customers = $this->findInCrmAndMakeCustomer ( $search );
49 ! is_null( $customers ) ? $customers : $customers = $customersQuery->paginate();
50 }
51
52 if ( defined('FLUENT_SUPPORT_PRO_DIR_FILE') && $customers->isEmpty() && !$status ) {
53 $customers = $this->findUserInWPAndMakeCustomer( $search );
54 ! is_null( $customers ) ? $customers : $customers = $customersQuery->paginate();
55 }
56
57 return $this->attachCustomersMetaData( $customers );
58 }
59
60 public function getCustomerField($customer_id,$userID = null)
61 {
62 $basicFields = [
63 'first_name' => [
64 'label' => __('First Name', 'fluent-support'),
65 'data_type' => 'text',
66 'placeholder' => __('First Name', 'fluent-support'),
67 'type' => 'input-text',
68 'wrapper_class' => 'fs_half_field required',
69 ],
70 'last_name' => [
71 'label' => __('Last Name', 'fluent-support'),
72 'data_type' => 'text',
73 'placeholder' => __('Last Name', 'fluent-support'),
74 'type' => 'input-text',
75 'wrapper_class' => 'fs_half_field',
76 ],
77 'email' => [
78 'label' => __('Email', 'fluent-support'),
79 'data_type' => 'email',
80 'placeholder' => __('Email', 'fluent-support'),
81 'type' => 'input-text',
82 'wrapper_class' => 'fs_half_field required',
83 'disabled' => !empty($customer_id),
84 ],
85 'title' => [
86 'label' => __('Job Title', 'fluent-support'),
87 'data_type' => 'text',
88 'placeholder' => __('Job Title', 'fluent-support'),
89 'type' => 'input-text',
90 'wrapper_class' => 'fs_half_field',
91 ],
92 'note' => [
93 'label' => __('Note', 'fluent-support'),
94 'data_type' => 'textarea',
95 'placeholder' => __('Note', 'fluent-support'),
96 'type' => 'input-text',
97 ],
98 'status' => [
99 'label' => __('Status', 'fluent-support'),
100 'data_type' => 'text',
101 'type' => 'input-radio',
102 'options' => [
103 'active' => ['id' => 'active', 'value' => 'active', 'label' => __('Active', 'fluent-support')],
104 'inactive' => ['id' => 'inactive', 'value' => 'inactive', 'label' => __('Blocked', 'fluent-support')],
105 ],
106 ],
107 'status_html' => [
108 'dependency' => [
109 'depends_on' => 'status',
110 'operator' => '=',
111 'value' => 'inactive',
112 ],
113 'type' => 'html-viewer',
114 'wrapper_class' => 'fs_warn_alert_wrapper',
115 'html' => __('If you select the <b>Blocked</b> status, this customer will not be able to submit a ticket or any response.', 'fluent-support'),
116 ],
117 ];
118
119 $addressFields = [
120 'address_line_1' => [
121 'label' => __('Address Line 1', 'fluent-support'),
122 'data_type' => 'text',
123 'placeholder' => __('Address Line 1', 'fluent-support'),
124 'type' => 'input-text',
125 'wrapper_class' => 'fs_half_field',
126 ],
127 'address_line_2' => [
128 'label' => __('Address Line 2', 'fluent-support'),
129 'data_type' => 'text',
130 'placeholder' => __('Address Line 2', 'fluent-support'),
131 'type' => 'input-text',
132 'wrapper_class' => 'fs_half_field',
133 ],
134 'city' => [
135 'label' => __('City', 'fluent-support'),
136 'data_type' => 'text',
137 'placeholder' => __('City', 'fluent-support'),
138 'type' => 'input-text',
139 'wrapper_class' => 'fs_half_field',
140 ],
141 'state' => [
142 'label' => __('State', 'fluent-support'),
143 'data_type' => 'text',
144 'placeholder' => __('State', 'fluent-support'),
145 'type' => 'input-text',
146 'wrapper_class' => 'fs_half_field',
147 ],
148 'zip' => [
149 'label' => __('Zip Code', 'fluent-support'),
150 'data_type' => 'text',
151 'placeholder' => __('Zip Code', 'fluent-support'),
152 'type' => 'input-text',
153 'wrapper_class' => 'fs_half_field',
154 ],
155 'country' => [
156 'label' => __('Country', 'fluent-support'),
157 'placeholder' => __('Country', 'fluent-support'),
158 'type' => 'country-selector',
159 'wrapper_class' => 'fs_half_field',
160 ],
161 ];
162
163 if ($userID) {
164 $addressFields = apply_filters('fluent_support/custom_registration_form_fields',$addressFields);
165
166 foreach ($addressFields as $key => $field) {
167 if (isset($field['type']) && $field['type'] === 'country-selector') {
168 continue; // Skip conversion for country-selector
169 }
170
171 //To create a custom field using _formBuilder.vue, the 'type' should be 'input-text', and the 'data_type' may vary, such as (text, number, date...).
172 $addressFields[$key]['data_type'] = $addressFields[$key]['type'];
173 $addressFields[$key]['type'] = 'input-text';
174 }
175 }
176
177 $loading = false;
178
179 $state = [
180 'basic_fields' => $basicFields,
181 'address_fields' => $addressFields,
182 'loading' => $loading,
183 ];
184
185 return $state;
186
187 }
188
189 /**
190 * This `findUserInWPAndMakeCustomer` method will find customer in WP Users and make it as a customer
191 * @param string $search
192 * @return object
193 */
194 public function findUserInWPAndMakeCustomer ( $search )
195 {
196 if ( is_email( $search ) ){
197 $user = get_user_by( 'email', $search );
198
199 if( $user ) {
200 unset( $user->user_pass );
201 $customerData = [
202 'user_id' => $user->ID,
203 'email' => $user->user_email,
204 'first_name' => $user->first_name,
205 'last_name' => $user->last_name
206 ];
207 $customer = static::maybeCreateCustomer( $customerData );
208 return static::where('email', $user->user_email)->paginate();
209 }
210 }
211 }
212
213 /**
214 * This `findInCrmAndMakeCustomer` method will find customer in Fluent CRM and make it as a customer
215 * @param string $search
216 * @return object
217 */
218 public function findInCrmAndMakeCustomer ( $search )
219 {
220 $customer = \FluentCrm\App\Models\Subscriber::where( 'email', $search )->first();
221 if( $customer ) {
222 $customerData = [
223 'email' => $customer->email,
224 'first_name' => $customer->first_name,
225 'last_name' => $customer->last_name
226 ];
227 $customer = static::maybeCreateCustomer( $customerData );
228 return static::where('email', $customer->email)->paginate();
229 }
230 }
231
232 /**
233 * This getCustomer method will return a single customer
234 * @param int $id
235 * @param array $with
236 * @return object
237 */
238
239 public function getCustomer($id, $with)
240 {
241 $customer = static::find($id);
242
243 $data = [
244 'customer' => $customer
245 ];
246
247 if (!$with) {
248 return $data;
249 }
250
251 return $this->getCustomerAdditionalData($with, $customer, $data);
252 }
253
254 /**
255 * This createCustomer method will create a new customer
256 * @param array $data
257 * @return object
258 */
259
260 public function createCustomer($data)
261 {
262 $data = Arr::only($data, $this->getFillable());
263
264 $user = get_user_by('email', $data['email']);
265
266 if ($user) {
267 $data['user_id'] = $user->ID;
268 if (empty($data['first_name'])) {
269 $data['first_name'] = $user->first_name;
270 }
271 if (empty($data['last_name'])) {
272 $data['last_name'] = $user->last_name;
273 }
274 }
275
276 return static::create($data);
277 }
278
279 /**
280 * This updateCustomer method will update a customer
281 * @since 1.5.7
282 * @param int $customerId
283 * @param array $data
284 * @return object
285 */
286
287 public function updateCustomer($customerId, $data)
288 {
289 $customFieldsKeys = apply_filters('fluent_support/custom_registration_form_fields_key', Helper::getBusinessSettings('custom_registration_form_field'));
290 $customFieldsKeys = is_array($customFieldsKeys) ? $customFieldsKeys : [];
291 $customFormValue = Arr::only($data, $customFieldsKeys);
292
293 $data = $this->takeValidKeysForUpdate($data);
294
295 if (isset($data['last_response_at']) && empty($data['last_response_at'])) {
296 unset($data['last_response_at']);
297 }
298
299 $customer = static::findOrFail($customerId);
300
301 if($this->customerExists($customerId, $data['email']))
302 {
303 throw new \Exception('Another Customer has same email address');
304 }
305
306 $user = get_user_by('email', $data['email']);
307
308 if ($user && !empty($customFieldsKeys)) {
309 // Never trust the submitted email to pick the account: an email edit
310 // must not silently re-bind this record to whoever owns that address.
311 $linkedUserId = $this->resolveUserLinkage($customer, (int) $user->ID);
312
313 if ($linkedUserId) {
314 $data['user_id'] = $linkedUserId;
315
316 //Update Custom field data for user
317 foreach ($customFieldsKeys as $key) {
318 if (isset($customFormValue[$key])) {
319 $fieldValue = $customFormValue[$key];
320 update_user_meta($linkedUserId, $key, $fieldValue);
321 }
322 }
323 }
324 }
325
326 static::where('id', $customer->id)->update($data);
327
328 return static::findOrFail($customerId);
329 }
330
331 /**
332 * deleteCustomer method will delete a customer and all tickets by that customer
333 * @since 1.5.7
334 * @param int $customerId
335 * @return array
336 */
337
338 public function deleteCustomer($customerId)
339 {
340 $customer = static::findOrFail($customerId);
341
342 $tickets = Ticket::where('customer_id', $customer->id)->get();
343
344 foreach ($tickets as $ticket) {
345 $ticket->deleteTicket();
346 }
347
348 $customer->delete();
349
350 return [
351 'message' => __('Customer Deleted Successfully', 'fluent-support')
352 ];
353 }
354
355 /**
356 * bulkDeleteCustomers method will delete multiple customers and all their tickets
357 * @since 1.9.3
358 * @param array $customerIds
359 * @return array
360 */
361 public function bulkDeleteCustomers($customerIds)
362 {
363 if (empty($customerIds) || !is_array($customerIds)) {
364 return [
365 'message' => __('No customers selected for deletion', 'fluent-support')
366 ];
367 }
368
369 $deletedCount = 0;
370 $errors = [];
371
372 foreach ($customerIds as $customerId) {
373 try {
374 $customer = static::findOrFail($customerId);
375
376 $tickets = Ticket::where('customer_id', $customer->id)->get();
377
378 foreach ($tickets as $ticket) {
379 $ticket->deleteTicket();
380 }
381
382 $customer->delete();
383 $deletedCount++;
384
385 } catch (\Exception $e) {
386 /* translators: %1$d: customer ID, %2$s: error message */
387 $errors[] = sprintf(__('Failed to delete customer ID %1$d: %2$s', 'fluent-support'), $customerId, $e->getMessage());
388 }
389 }
390
391 if ($deletedCount > 0) {
392 /* translators: %1$d: number of customers deleted */
393 $message = $deletedCount === 1
394 ? __('Customer deleted successfully', 'fluent-support')
395 : sprintf(__('%1$d customers deleted successfully', 'fluent-support'), $deletedCount);
396 } else {
397 $message = __('No customers were deleted', 'fluent-support');
398 }
399
400 $response = [
401 'message' => $message,
402 'deleted_count' => $deletedCount,
403 'total_requested' => count($customerIds)
404 ];
405
406 if (!empty($errors)) {
407 $response['errors'] = $errors;
408 }
409
410 return $response;
411 }
412
413 /**
414 * This attachCustomersMetaData method will attach meta data to customers
415 * @since 1.5.7
416 * @param object $customers
417 * @return object
418 */
419 private function attachCustomersMetaData($customers)
420 {
421 foreach ($customers as $customer) {
422 $customer->total_tickets = $customer->getTicketCounts();
423 $customer->total_responses = $customer->getResponseCounts();
424 if ($customer->user_id) {
425 //Get profile link, if they are WP user
426 $customer->user_profile = admin_url('user-edit.php?user_id=' . $customer->user_id);
427 }
428 }
429
430 return $customers;
431 }
432
433 /**
434 * This getCustomerAdditionalData method will return additional data for a customer
435 * @since 1.5.7
436 * @param array $with
437 * @param object $customer
438 * @param array $data
439 * @return array
440 */
441
442 private function getCustomerAdditionalData($with, $customer, $data)
443 {
444 $customFieldKeys = apply_filters('fluent_support/custom_registration_form_fields_key', []);
445
446 $userMetaData = get_user_meta($customer['user_id']);
447
448 //Custom field data from wp user_meta
449 foreach ($customFieldKeys as $fieldKey) {
450 if (isset($userMetaData[$fieldKey][0])) {
451 $customer[$fieldKey] = $userMetaData[$fieldKey][0];
452 }
453 }
454
455 if (in_array('widgets', $with)) {
456 $data['widgets'] = ProfileInfoService::getProfileExtraWidgets($customer);
457 }
458
459 if (in_array('tickets', $with)) {
460 /*
461 * Filter ticket limit to show ticket in customer page sidebar
462 * @since 1.5.6
463 * @param int $limit
464 */
465 $limit = apply_filters('fluent_support/customer_page_ticket_widgets_limit', 20);
466
467 $data['tickets'] = Ticket::select(['id', 'title', 'status', 'customer_id', 'created_at'])
468 ->where('customer_id', $customer->id)
469 ->latest('id')
470 ->limit($limit)
471 ->get();
472 }
473
474 if(in_array('fluentcrm_profile', $with)) {
475 $data['fluentcrm_profile'] = Helper::getFluentCrmContactData($customer);
476 }
477
478 return $data;
479 }
480
481 /**
482 * This customerExists method will check if customer exists
483 * @since 1.5.7
484 * @param int $customerId
485 * @param string $email
486 * @return mixed
487 */
488
489 private function customerExists($customerId, $email)
490 {
491 $customer = static::where('id','!=', $customerId)->where('email', $email)->first();
492 if ($customer) {
493 return $customer;
494 }
495 return false;
496 }
497
498
499 /**
500 * Decide which WordPress account this customer record may be linked to.
501 *
502 * Binding a support record to a WP account is an identity change: the portal
503 * resolves the logged-in customer by `user_id` (see Helper::getCurrentCustomer),
504 * so a re-bind hands this record's tickets to the newly bound account. It is
505 * therefore restricted to administrators and refused when the account already
506 * belongs to another customer. When a re-bind is not allowed the existing
507 * linkage is kept, so ordinary email corrections still go through.
508 *
509 * @param object $customer
510 * @param int $targetUserId WP user matching the submitted email
511 * @return int user id to link, or 0 for none
512 */
513 private function resolveUserLinkage($customer, $targetUserId)
514 {
515 $currentUserId = (int) $customer->user_id;
516
517 // Already linked to this account, nothing to authorize.
518 if ($targetUserId === $currentUserId) {
519 return $currentUserId;
520 }
521
522 if (!current_user_can('manage_options')) {
523 return $currentUserId;
524 }
525
526 // Conflict: that account is already bound to a different customer record.
527 $alreadyLinked = static::where('id', '!=', $customer->id)
528 ->where('user_id', $targetUserId)
529 ->first();
530
531 if ($alreadyLinked) {
532 return $currentUserId;
533 }
534
535 return $targetUserId;
536 }
537
538 /**
539 * This takeValidKeysForUpdate method will take valid keys data for update
540 * @since 1.5.7
541 * @param array $data
542 * @return array
543 */
544 private function takeValidKeysForUpdate($data)
545 {
546 // $fillable is a numerically indexed list, so the immutable columns are
547 // values rather than keys and have to be removed with array_diff().
548 $validKeys = array_diff($this->getFillable(), self::$immutableUpdateKeys);
549
550 return Arr::only($data, $validKeys);
551 }
552 }
553