PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.1.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.1.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 2.1.0, at app/Services/CustomerPortalService.php

579 lines 19.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 // 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(esc_html__('Customer not found', 'fluent-support'));
242 }
243
244 if ($customer->status == 'inactive') {
245 throw new \Exception(esc_html__('Sorry, You do not have access to customer portal', 'fluent-support'));
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(esc_html__('Sorry! No customer found', 'fluent-support'));
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(sanitize_sql_orderby($ticketOptions['sorting']['sort_by']), sanitize_sql_orderby($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 | false //
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
358 $onBehalf = [
359 'user_id' => $user->ID,
360 'email' => $user->user_email,
361 'last_ip_address' => $userIp
362 ];
363 }
364
365 if ($forceCreate) {
366 return Customer::maybeCreateCustomer($onBehalf);
367 }
368
369 return Customer::getCustomerFromData($onBehalf);
370 }
371
372 /**
373 * resolveMailboxId method will either get information of the mailbox added by user or default and return the id
374 * @param int $mailboxId
375 * @return null
376 */
377 private function resolveMailboxId($mailboxId)
378 {
379 $mailbox = MailBox::find($mailboxId);
380 if ($mailbox) {
381 return $mailbox->id;
382 }
383
384 $mailbox = Helper::getDefaultMailBox();
385
386 if ($mailbox) {
387 return $mailbox->id;
388 }
389 return null;
390 }
391
392 // Supportive methods for getTicket
393
394 /**
395 * This `getTicketByID` method is responsible for getting a ticket by id
396 * @param $ticketId
397 * @return object $ticket
398 */
399 private function getTicketByID($ticketId)
400 {
401 $ticket = Ticket::where('id', $ticketId)
402 ->with([
403 'customer' => function ($query) {
404 $query->select(['first_name', 'email', 'person_type', 'last_name', 'id', 'avatar']);
405 }, 'agent' => function ($query) {
406 $query->select(['first_name', 'email', 'person_type', 'last_name', 'id', 'title', 'avatar']);
407 },
408 'product',
409 'attachments' => function ($q) {
410 $q->whereIn('status', ['active', 'inline']);
411 }
412 ])
413 ->first();
414
415 return $ticket;
416 }
417
418 /**
419 * This `checkCustomerTicketAccess` method is responsible for checking customer ticket access
420 * @param object $customer
421 * @param object $ticket
422 * @return bool true if access is granted
423 * @throws Exception
424 */
425 public function checkCustomerTicketAccess($customer, $ticket, $action = false)
426 {
427 if (!$customer) {
428 throw new \Exception(esc_html__('Sorry, You do not have permission to this support ticket', 'fluent-support'));
429 }
430
431 if ($customer->status == 'inactive') {
432 throw new \Exception(esc_html__('Sorry, You do not have access to customer portal', 'fluent-support'));
433 }
434
435 if ($ticket->privacy == 'private' && $customer->id != $ticket->customer_id) {
436 if ($action) {
437 throw new \Exception(sprintf(
438 // translators: %s is the action being performed (e.g., "view", "edit", "delete")
439 esc_html__("Sorry! You cannot %s this ticket", 'fluent-support'),
440 esc_html($action)
441 ));
442 } else {
443 throw new \Exception(esc_html__('You do not have permission to view this support ticket', 'fluent-support'));
444 }
445 }
446
447 $result = apply_filters('fluent_support/can_customer_access_ticket', true, $customer, $ticket, $action);
448
449 if ($result && !is_wp_error($result)) {
450 return $result;
451 }
452
453 if (!$result) {
454 throw new \Exception(esc_html__('Sorry, You cannot access this ticket', 'fluent-support'));
455 }
456
457 throw new \Exception(esc_html($result->get_error_message()));
458 }
459
460
461 /**
462 * This `getResponses` method is responsible for getting a ticket's responses by ticket id
463 * @param int $ticketId
464 * @return mixed
465 */
466 private function getResponses($ticketId)
467 {
468 $responses = Conversation::where('ticket_id', $ticketId)
469 ->with([
470 'person' => function ($query) {
471 $query->select(['first_name', 'email', 'person_type', 'last_name', 'id', 'title', 'avatar']);
472 },
473 'attachments'
474 ])
475 ->filterByType(['response', 'ticket_merge_activity', 'ticket_split_activity'])
476 ->orderBy('created_at', 'desc')
477 ->orderBy('id', 'desc')
478 ->get();
479
480 foreach ($responses as $response) {
481 if (defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') && Helper::isAgentFeedbackEnabled()) {
482 $agentFeedback = Meta::where('object_id', $response->id)
483 ->where('object_type', 'conversation_meta')
484 ->where('key', 'agent_feedback_ratings')
485 ->first();
486
487 if ($agentFeedback) {
488 $response->agent_feedback = $agentFeedback->value;
489 }
490 }
491
492 // translators: %s is the time duration (e.g., "2 hours", "3 days")
493 $response->human_date = sprintf(__('%s ago', 'fluent-support'), human_time_diff(strtotime($response->created_at), current_time('timestamp')));
494 $response->content = links_add_target(make_clickable($response->content));
495 if ($response->person) {
496 $response->person->setHidden(['email']);
497 }
498 }
499
500 return $responses;
501 }
502
503 /**
504 * This `syncTicketAdditionData` method is responsible for syncing ticket additional data
505 * @param object $ticket
506 * @return object $ticket
507 */
508 private function syncTicketAdditionData($ticket)
509 {
510 $ticket->content = links_add_target(make_clickable($ticket->content));
511
512 if ($ticket->customer) {
513 $ticket->customer->setHidden(['email']);
514 }
515
516 if ($ticket->agent) {
517 $ticket->agent->setHidden(['email']);
518 }
519
520 if ($ticket->status == 'closed') {
521 $ticket->load('closed_by_person');
522 if ($ticket->closed_by_person) {
523 $ticket->closed_by_person->setVisible(['first_name', 'last_name', 'id', 'full_name', 'photo']);
524 }
525 }
526
527 if (defined('FLUENTSUPPORTPRO')) {
528 $ticket->custom_fields = $ticket->customData('public', true);
529 }
530
531 // Load agent info if ticket was created on behalf of customer
532 if ($ticket->created_by) {
533 $ticket->load('created_by_person');
534 if ($ticket->created_by_person) {
535 $ticket->created_by_agent = [
536 'full_name' => $ticket->created_by_person->full_name,
537 'photo' => $ticket->created_by_person->photo,
538 ];
539 }
540 }
541
542 return $ticket;
543 }
544
545 public function addUserFeedback($approvalStatus, $conversationID)
546 {
547 $existingAgentFeedback = Meta::where([
548 'object_id' => $conversationID,
549 'key' => 'agent_feedback_ratings',
550 ])->first();
551
552 if ($existingAgentFeedback) {
553 return $this->updateExistingFeedback($existingAgentFeedback, $approvalStatus);
554 } else {
555 $agentFeedback = Meta::create([
556 'object_id' => $conversationID,
557 'key' => 'agent_feedback_ratings',
558 'object_type' => 'conversation_meta',
559 'value' => $approvalStatus,
560 ]);
561 return $agentFeedback;
562 }
563 }
564
565 private function updateExistingFeedback($existingAgentFeedback, $approvalStatus)
566 {
567 if (($existingAgentFeedback->value === 'like' && $approvalStatus === 'like') ||
568 ($existingAgentFeedback->value === 'dislike' && $approvalStatus === 'dislike')) {
569 $existingAgentFeedback->delete();
570 } else {
571 $existingAgentFeedback->update([
572 'value' => $approvalStatus,
573 ]);
574 }
575 return $existingAgentFeedback;
576 }
577
578 }
579