PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 1.10.0
Fluent Support – Helpdesk & Customer Support Ticket System v1.10.0
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 / Services / CustomerPortalService.php

CustomerPortalService.php in Fluent Support – Helpdesk & Customer Support Ticket System 1.10.0, at app/Services/CustomerPortalService.php

562 lines 18.6 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\Services;
4
5 use Exception;
6 use FluentSupport\App\Models\MailBox;
7 use FluentSupport\App\Models\Meta;
8 use FluentSupport\App\Models\Ticket;
9 use FluentSupport\App\Models\Customer;
10 use FluentSupport\App\Services\Tickets\ResponseService;
11 use FluentSupport\App\Services\Tickets\TicketService;
12 use FluentSupport\Framework\Support\Arr;
13 use FluentSupport\App\Models\Attachment;
14 use FluentSupport\App\Models\Conversation;
15
16 class CustomerPortalService
17 {
18 /**
19 * This `getTickets` method is responsible for getting tickets for customer
20 * @param object $customer
21 * @param string $requestedStatus
22 * @param array|null $options
23 * @return object
24 * @throws Exception
25 * @since 1.8.1
26 */
27 public function getTickets($customer, $requestedStatus, $options = [])
28 {
29 $this->validateCustomer($customer);
30
31 $statuses = $this->getTicketStatues($requestedStatus);
32
33 return $this->ticketsAdditionalData($customer, $statuses, $options);
34 }
35
36 /**
37 * getTicket method will get the ticket information with customer and agent as well as response in a ticket by ticket id
38 * @param array $customerAdditionalData
39 * @param int $ticketId
40 * @return array
41 * @since 1.5.7
42 */
43 public function getTicket($customerAdditionalData, $ticketId)
44 {
45 $ticket = $this->getTicketByID($ticketId);
46 $ticket->human_date = sprintf(__('%s ago', 'fluent-support'), human_time_diff(strtotime($ticket->created_at), current_time('timestamp')));
47
48 $customer = $this->getCustomer($customerAdditionalData, $ticket);
49
50 $this->checkCustomerTicketAccess($customer, $ticket);
51
52 return [
53 'ticket' => $this->syncTicketAdditionData($ticket),
54 'responses' => $this->getResponses($ticketId),
55 'sign_on_id' => $ticket->customer_id
56 ];
57 }
58
59 /**
60 * This `createTicket` method is responsible for creating ticket for customer
61 * @param object $customer
62 * @param array $data
63 * @param int $mailboxId
64 * @return Ticket
65 * @throws Exception
66 */
67 public function createTicket($customer, $data, $mailboxId)
68 {
69 $this->validateCustomer($customer);
70
71 $data['title'] = sanitize_text_field(wp_unslash($data['title']));
72 $data['content'] = wp_specialchars_decode(wp_unslash(wp_kses_post($data['content'])));
73 $data['customer_id'] = $customer->id;
74 $data['product_source'] = 'local';
75 $data['mailbox_id'] = $this->resolveMailboxId($mailboxId);
76 $data['source'] = 'web';
77
78 $disabledFields = apply_filters('fluent_support/disabled_ticket_fields', []);
79 $this->validateDisabledFields($data, $disabledFields);
80 return $this->storeTicket($data, $customer, $disabledFields);
81 }
82
83
84 /**
85 * This `createResponse` method is responsible for creating response by customer in a ticket by ticket id, and data
86 * @param array $customerAdditionalData
87 * @param int $ticketId
88 * @param array $data
89 * @return array
90 * @throws Exception
91 * @since 1.5.7
92 */
93 public function createResponse($customerAdditionalData, $ticketId, $data)
94 {
95 $data['content'] = wp_specialchars_decode(wp_unslash($data['content']));
96 $data['conversation_type'] = 'response';
97
98 $ticket = Ticket::with(['customer'])->findOrFail($ticketId);
99 $customer = $this->getCustomer($customerAdditionalData, $ticket);
100
101 $this->checkCustomerTicketAccess($customer, $ticket, 'response');
102
103 $responseData = (new ResponseService())->createResponse($data, $customer, $ticket);
104
105 return [
106 'message' => __('Reply has been added', 'fluent-support'),
107 'response' => $responseData['response'],
108 'ticket' => $responseData['ticket']
109 ];
110 }
111
112
113 /**
114 * This `closeTicket` is responsible for closing ticket by ticket id
115 * @param array $customerAdditionalData
116 * @param int $ticketId
117 * @return array
118 * @throws Exception
119 */
120 public function closeTicket($customerAdditionalData, $ticketId)
121 {
122 $ticket = Ticket::with(['customer'])->findOrFail($ticketId);
123 $customer = $this->getCustomer($customerAdditionalData, $ticket);
124
125 $this->checkCustomerTicketAccess($customer, $ticket, 'close');
126
127 return [
128 'message' => __('Ticket has been closed', 'fluent-support'),
129 'ticket' => (new TicketService())->close($ticket, $customer)
130 ];
131 }
132
133 /**
134 * This `reOpenTicket` is responsible for reopening ticket by ticket id
135 * @param array $customerAdditionalData
136 * @param int $ticketId
137 * @return array
138 * @throws Exception
139 */
140 public function reOpenTicket($customerAdditionalData, $ticketId)
141 {
142 $ticket = Ticket::with(['customer'])->findOrFail($ticketId);
143 $customer = $this->getCustomer($customerAdditionalData, $ticket);
144
145 $this->checkCustomerTicketAccess($customer, $ticket, 'reopen');
146
147 return [
148 'message' => __('Ticket has been opened again', 'fluent-support'),
149 'ticket' => (new TicketService())->reopen($ticket, $customer)
150 ];
151 }
152
153 /**
154 * This `validateDisabledFields` method is responsible for validating disabled fields
155 * @param array $data
156 * @param array $disabledFields
157 * @return array $data
158 * @since 1.5.7
159 */
160 private function validateDisabledFields($data, $disabledFields)
161 {
162 if (!in_array('priority', $disabledFields)) {
163 $data['priority'] = sanitize_text_field($data['client_priority']);
164 $data['client_priority'] = sanitize_text_field($data['client_priority']);
165 }
166
167 if (in_array('product_services', $disabledFields)) {
168 unset($data['product_id']);
169 }
170
171 return $data;
172 }
173
174
175 /**
176 * This `storeTicket` method is responsible for storing a ticket in Ticket Model
177 * @param array $data
178 * @param object $customer
179 * @param array $disabledFields
180 * @return Ticket
181 * @since 1.5.7
182 */
183 private function storeTicket($data, $customer, $disabledFields)
184 {
185 /*
186 * Filter ticket data
187 *
188 * @since v1.0.0
189 * @param array $data
190 * @param object $customer
191 */
192 $data = apply_filters('fluent_support/create_ticket_data', $data, $customer);
193
194 /*
195 * Action before ticket create
196 *
197 * @since v1.0.0
198 * @param array $data
199 * @param object $customer
200 */
201 do_action('fluent_support/before_ticket_create', $data, $customer);
202
203 $ticket = Ticket::create($data);
204
205 TicketService::addTicketAttachments($data, $disabledFields, $ticket, $customer);
206 $this->addCustomData($data, $ticket);
207
208 do_action('fluent_support/ticket_created', $ticket, $customer);
209
210 return $ticket;
211 }
212
213
214 /**
215 * This `addCustomData` method is responsible for adding custom data to ticket
216 * @param array $data
217 * @param object $ticket
218 * @return void
219 */
220 private function addCustomData($data, $ticket)
221 {
222 if (defined('FLUENTSUPPORTPRO')) {
223 $customData = Arr::get($data, 'custom_data');
224 if ($customData) {
225 $customData = wp_unslash($customData);
226 $ticket->syncCustomFields($customData);
227 }
228 }
229 }
230
231 /**
232 * This `validateCustomer` method is responsible for validating customer
233 * @param object|null $customer // It can be null if there's no customer
234 * @since 1.5.7
235 * @throws Exception
236 */
237 private function validateCustomer($customer)
238 {
239 if (!$customer) {
240 throw new \Exception('Customer not found');
241 }
242
243 if ($customer->status == 'inactive') {
244 throw new \Exception('Sorry, You do not have access to customer portal');
245 }
246 }
247
248 /**
249 * This `getCustomer` method is responsible for getting customer
250 * @param array $customerAdditionalData
251 * @param object $ticket
252 * @return object $customer
253 * @throws Exception
254 *
255 * @since 1.5.7
256 */
257 public function getCustomer($customerAdditionalData, $ticket)
258 {
259 if (Arr::get($customerAdditionalData, 'intended_ticket_hash') && Helper::isPublicSignedTicketEnabled()) {
260 $customer = $ticket->customer;
261 } else {
262 $customer = $this->resolveCustomer(Arr::get($customerAdditionalData, 'on_behalf'), Arr::get($customerAdditionalData, 'user_ip'));
263 }
264
265 if (!$customer) {
266 throw new \Exception('Sorry! No customer found');
267 }
268
269 return $customer;
270 }
271
272 /**
273 * This `getTicketStatues` method is responsible for getting ticket statuses
274 * @param string $requestedStatus
275 * @return array
276 * @since 1.8.1
277 */
278 private function getTicketStatues($requestedStatus)
279 {
280 $statuses = [
281 'open' => ['new', 'active', 'on-hold'],
282 'all' => [],
283 'closed' => ['closed']
284 ];
285
286 return Arr::get($statuses, $requestedStatus, []);
287 }
288
289
290 /**
291 * This `ticketsAdditionalData` method is responsible for getting tickets with additional data
292 * @param object $customer
293 * @param array $statuses
294 * @param array|null $options
295 * @return object $tickets
296 * @since 1.5.7
297 */
298 private function ticketsAdditionalData($customer, $statuses, $options = [])
299 {
300 $defaultOptions = [
301 'search' => null,
302 'sorting' => null,
303 'filters' => null
304 ];
305
306 $ticketOptions = wp_parse_args($options, $defaultOptions);
307
308 $tickets = Ticket::with([
309 'customer' => function ($query) {
310 $query->select(['first_name', 'last_name', 'id']);
311 }, 'agent' => function ($query) {
312 $query->select(['first_name', 'last_name', 'id']);
313 }
314 ])->where('customer_id', $customer->id)
315 ->when(!empty($ticketOptions['sorting'] && !empty($ticketOptions['sorting']['sort_by'])), function ($query) use ($ticketOptions) {
316 return $query->orderBy($ticketOptions['sorting']['sort_by'], $ticketOptions['sorting']['sort_type']);
317 })
318 ->when(!empty($options['filters']['product_id']), function ($query) use ($ticketOptions) {
319 return $query->where('product_id', $ticketOptions['filters']['product_id']);
320 })
321 ->when($statuses, function ($query) use ($statuses) {
322 return $query->whereIn('status', $statuses);
323 })
324 ->when($ticketOptions['search'], function ($query) use ($ticketOptions) {
325 return $query->searchBy($ticketOptions['search']);
326 })
327 ->when(empty($ticketOptions['sorting']), function ($query) {
328 return $query->latest('updated_at');
329 })
330 ->paginate();
331
332 foreach ($tickets as $ticket) {
333 $ticket->human_date = sprintf(__('%s ago', 'fluent-support'), human_time_diff(strtotime($ticket->created_at), current_time('timestamp')));
334 $ticket->preview_response = $ticket->getLastResponse();
335 }
336
337 return $tickets;
338 }
339
340 /**
341 * `resolveCustomer` method will create and return or only return existing customer
342 * This method will get customer id or customer info or option to force create as parameter.
343 * @param array $onBehalf
344 * @param string $userIp // IP address of user
345 * @param bool $forceCreate Default: false // If true, it will create a new customer
346 * @return Customer // Collection
347 */
348 public function resolveCustomer($onBehalf, $userIp, $forceCreate = false)
349 {
350 if (!$onBehalf) {
351 $user = get_user_by('ID', get_current_user_id());
352 if (!$user) {
353 return false;
354 }
355 $onBehalf = [
356 'user_id' => $user->ID,
357 'email' => $user->user_email,
358 'last_ip_address' => $userIp
359 ];
360 }
361
362 if ($forceCreate) {
363 return Customer::maybeCreateCustomer($onBehalf);
364 }
365
366 return Customer::getCustomerFromData($onBehalf);
367 }
368
369 /**
370 * resolveMailboxId method will either get information of the mailbox added by user or default and return the id
371 * @param int $mailboxId
372 * @return null
373 */
374 private function resolveMailboxId($mailboxId)
375 {
376 $mailbox = MailBox::find($mailboxId);
377 if ($mailbox) {
378 return $mailbox->id;
379 }
380
381 $mailbox = Helper::getDefaultMailBox();
382
383 if ($mailbox) {
384 return $mailbox->id;
385 }
386 return null;
387 }
388
389 // Supportive methods for getTicket
390
391 /**
392 * This `getTicketByID` method is responsible for getting a ticket by id
393 * @param $ticketId
394 * @return object $ticket
395 */
396 private function getTicketByID($ticketId)
397 {
398 $ticket = Ticket::where('id', $ticketId)
399 ->with([
400 'customer' => function ($query) {
401 $query->select(['first_name', 'email', 'person_type', 'last_name', 'id', 'avatar']);
402 }, 'agent' => function ($query) {
403 $query->select(['first_name', 'email', 'person_type', 'last_name', 'id', 'title', 'avatar']);
404 },
405 'product',
406 'attachments' => function ($q) {
407 $q->whereIn('status', ['active', 'inline']);
408 }
409 ])
410 ->first();
411
412 return $ticket;
413 }
414
415 /**
416 * This `checkCustomerTicketAccess` method is responsible for checking customer ticket access
417 * @param object $customer
418 * @param object $ticket
419 * @return bool true if access is granted
420 * @throws Exception
421 */
422 public function checkCustomerTicketAccess($customer, $ticket, $action = false)
423 {
424 if (!$customer) {
425 throw new \Exception('Sorry, You do not have permission to this support ticket');
426 }
427
428 if ($customer->status == 'inactive') {
429 throw new \Exception('Sorry, You do not have access to customer portal');
430 }
431
432 if ($ticket->privacy == 'private' && $customer->id != $ticket->customer_id) {
433 if ($action) {
434 throw new \Exception(sprintf(
435 esc_html__("Sorry! You cannot %s this ticket", 'fluent-support'),
436 esc_html($action)
437 ));
438 } else {
439 throw new \Exception(esc_html__('You do not have permission to view this support ticket', 'fluent-support'));
440 }
441 }
442
443 $result = apply_filters('fluent_support/can_customer_access_ticket', true, $customer, $ticket, $action);
444
445 if ($result && !is_wp_error($result)) {
446 return $result;
447 }
448
449 if (!$result) {
450 throw new \Exception(esc_html__('Sorry, You cannot access this ticket', 'fluent-support'));
451 }
452
453 throw new \Exception(esc_html($result->get_error_message()));
454 }
455
456
457 /**
458 * This `getResponses` method is responsible for getting a ticket's responses by ticket id
459 * @param int $ticketId
460 * @return mixed
461 */
462 private function getResponses($ticketId)
463 {
464 $responses = Conversation::where('ticket_id', $ticketId)
465 ->with([
466 'person' => function ($query) {
467 $query->select(['first_name', 'email', 'person_type', 'last_name', 'id', 'title', 'avatar']);
468 },
469 'attachments'
470 ])
471 ->filterByType(['response', 'ticket_merge_activity', 'ticket_split_activity'])
472 ->latest('id')
473 ->get();
474
475 foreach ($responses as $response) {
476 if (defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') && Helper::isAgentFeedbackEnabled()) {
477 $agentFeedback = Meta::where('object_id', $response->id)
478 ->where('object_type', 'conversation_meta')
479 ->where('key', 'agent_feedback_ratings')
480 ->first();
481
482 if ($agentFeedback) {
483 $response->agent_feedback = $agentFeedback->value;
484 }
485 }
486
487 $response->human_date = sprintf(__('%s ago', 'fluent-support'), human_time_diff(strtotime($response->created_at), current_time('timestamp')));
488 $response->content = links_add_target(make_clickable($response->content));
489 if ($response->person) {
490 $response->person->setHidden(['email']);
491 }
492 }
493
494 return $responses;
495 }
496
497 /**
498 * This `syncTicketAdditionData` method is responsible for syncing ticket additional data
499 * @param object $ticket
500 * @return object $ticket
501 */
502 private function syncTicketAdditionData($ticket)
503 {
504 $ticket->content = links_add_target(make_clickable($ticket->content));
505
506 if ($ticket->customer) {
507 $ticket->customer->setHidden(['email']);
508 }
509
510 if ($ticket->agent) {
511 $ticket->agent->setHidden(['email']);
512 }
513
514 if ($ticket->status == 'closed') {
515 $ticket->load('closed_by_person');
516 if ($ticket->closed_by_person) {
517 $ticket->closed_by_person->setVisible(['first_name', 'last_name', 'id', 'full_name', 'photo']);
518 }
519 }
520
521 if (defined('FLUENTSUPPORTPRO')) {
522 $ticket->custom_fields = $ticket->customData('public', true);
523 }
524
525 return $ticket;
526 }
527
528 public function addUserFeedback($approvalStatus, $conversationID)
529 {
530 $existingAgentFeedback = Meta::where([
531 'object_id' => $conversationID,
532 'key' => 'agent_feedback_ratings',
533 ])->first();
534
535 if ($existingAgentFeedback) {
536 return $this->updateExistingFeedback($existingAgentFeedback, $approvalStatus);
537 } else {
538 $agentFeedback = Meta::create([
539 'object_id' => $conversationID,
540 'key' => 'agent_feedback_ratings',
541 'object_type' => 'conversation_meta',
542 'value' => $approvalStatus,
543 ]);
544 return $agentFeedback;
545 }
546 }
547
548 private function updateExistingFeedback($existingAgentFeedback, $approvalStatus)
549 {
550 if (($existingAgentFeedback->value === 'like' && $approvalStatus === 'like') ||
551 ($existingAgentFeedback->value === 'dislike' && $approvalStatus === 'dislike')) {
552 $existingAgentFeedback->delete();
553 } else {
554 $existingAgentFeedback->update([
555 'value' => $approvalStatus,
556 ]);
557 }
558 return $existingAgentFeedback;
559 }
560
561 }
562