PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 1.10.2
Fluent Support – Helpdesk & Customer Support Ticket System v1.10.2
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.2, at app/Services/CustomerPortalService.php

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