PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.2.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.2.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 / Models / Ticket.php

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

1,187 lines 38.0 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;
4
5 use Exception;
6 use FluentSupport\App\Modules\PermissionManager;
7 use FluentSupport\App\Services\Helper;
8 use FluentSupport\App\Services\TicketHelper;
9 use FluentSupport\App\Services\TicketQueryService;
10 use FluentSupport\App\Services\Tickets\TicketService;
11 use FluentSupport\Framework\Support\Arr;
12
13 class Ticket extends Model
14 {
15 protected $table = 'fs_tickets';
16
17 protected $dates = ['waiting_since'];
18
19 protected $appends = ['display_ticket_number'];
20
21 /**
22 * The attributes that are mass assignable.
23 *
24 * @var array
25 */
26 protected $fillable = [
27 'customer_id',
28 'agent_id',
29 'product_id',
30 'mailbox_id',
31 'product_source',
32 'privacy',
33 'priority',
34 'client_priority',
35 'status',
36 'title',
37 'slug',
38 'hash',
39 'source',
40 'message_id',
41 'content',
42 'last_agent_response',
43 'last_customer_response',
44 'waiting_since',
45 'response_count',
46 'first_response_time',
47 'total_close_time',
48 'resolved_at',
49 'closed_by',
50 'created_by',
51 'serial_number',
52 'ticket_number'
53 ];
54
55 public static function boot()
56 {
57 parent::boot();
58
59 static::creating(function ($model) {
60 if (empty($model->slug)) {
61 $model->slug = static::slugify($model->title);
62 }
63
64 $model->hash = bin2hex(random_bytes(16));
65 $model->content_hash = md5($model->content);
66
67 $model->last_customer_response = current_time('mysql');
68 $model->created_at = current_time('mysql');
69 $model->updated_at = current_time('mysql');
70 $model->waiting_since = current_time('mysql');
71
72 });
73
74 static::created(function ($model) {
75 if (empty($model->serial_number) || empty($model->ticket_number)) {
76 $model->assignTicketNumber();
77 }
78 });
79
80 static::deleting(function ($model) {
81 //Delete the ticket meta
82 Meta::where('object_type', 'ticket_meta')->where('object_id', $model->id)->delete();
83 //Delete all cc info for the ticket
84 Meta::where('object_type', 'ticket')->where('object_id', $model->id)->delete();
85 //Delete draft info
86 Meta::where('object_type', '_fs_auto_draft')->where('object_id', $model->id)->delete();
87 //Delete internal notifications and notification recipient rows for the ticket
88 Notification::deleteByTicketId($model->id);
89 //delete the responses first (their attachments are cleaned up by Conversation::deleting)
90 Conversation::deleteAll($model->id);
91 // Delete ticket-level attachments (conversation_id IS NULL) and remove the ticket upload directory
92 $class = __NAMESPACE__ . '\Attachment';
93 $ticketAttachments = $class::where('ticket_id', $model->id)->whereNull('conversation_id')->get();
94 $class::purgeAttachments($ticketAttachments, $model->id);
95 $class::where('ticket_id', $model->id)->whereNull('conversation_id')->delete();
96 });
97 }
98
99 /**
100 * $searchable Columns in table to search
101 * @var array
102 */
103 protected $searchable = [
104 'content',
105 'title',
106 'slug',
107 'id',
108 'serial_number',
109 'ticket_number'
110 ];
111
112 /**
113 * Local scope to filter tickets by search/query string
114 * @param ModelQueryBuilder $query
115 * @param string $search
116 * @return ModelQueryBuilder
117 */
118 public function scopeSearchBy($query, $search)
119 {
120
121 if(!$search) {
122 return $query;
123 }
124
125 if (strpos($search, ':')) {
126 $array = explode(':', (string) $search);
127 $column = $array[0];
128 $value = $array[1];
129 $columns = $this->fillable;
130 $columns[] = 'id';
131
132 if (in_array($column, $columns) && $value) {
133 if (is_numeric($value)) {
134 $query->where($column, $value);
135 } else {
136 $query->where($column, 'LIKE', "%$value%");
137 }
138 return $query;
139 }
140 }
141
142 $fields = $this->searchable;
143 $query->where(function ($query) use ($fields, $search) {
144 $query->where(array_shift($fields), 'LIKE', "%$search%");
145 foreach ($fields as $field) {
146 $query->orWhere($field, 'LIKE', "%$search%");
147 }
148 });
149
150 return $query;
151 }
152
153 /**
154 * Local scope to filter tickets by different filtering condition
155 * @param ModelQueryBuilder $query
156 * @param mixed $search
157 * @return ModelQueryBuilder
158 */
159
160 public function doSearchForAdvancedFilter($query, $search)
161 {
162 foreach ($search as $s) {
163 $operator = $s['operator'];
164 //If selected item for ticket either title or content
165 if (in_array($s['property'], ['title', 'content'])) {
166 //If the selected condition is contains, query operator id LIKE
167 if ($operator == 'contains') {
168 $query = $query->where(function ($query) use ($s) {
169 $query->where($s['property'], 'LIKE', "%" . $s['value'] . "%");
170 });
171 } elseif ($operator == 'not_contains') {
172 //If the selected condition is not_contains, query operator id NOT LIKE
173 $query = $query->where(function ($query) use ($s) {
174 $query->where($s['property'], 'NOT LIKE', '%' . $s['value'] . '%');
175 });
176 }
177 }
178
179 //If selected item is Ticket Conversation Content
180 if ($s['property'] == 'conversation_content') {
181 $operator = $s['operator'];
182 if ($operator == 'contains') {
183 $query = $query->whereHas('responses', function ($q) use ($s) {
184 $q->where('content', 'LIKE', "%" . $s['value'] . "%");
185 });
186
187 } else if ($operator == 'not_contains') {
188 $query = $query->whereHas('responses', function ($q) use ($s) {
189 $q->where('content', 'NOT LIKE', "%" . $s['value'] . "%");
190 });
191 }
192 }
193
194 //If selected item is Ticket created or Last Response or Customer Waiting For, or Last Agent Response or Last Customer Response
195 if (in_array($s['property'], ['created_at', 'updated_at', 'waiting_since', 'last_agent_response', 'last_customer_response'])) {
196 $query = (new \FluentSupport\App\Models\Ticket())->buildDateBaseFilterQuery($query, $s);
197 }
198
199 //If selected item is Ticket Status or Client Priority or Agent Priority or Tags or Product or Waiting For Reply
200 if (in_array($s['property'], ['status', 'client_priority', 'priority', 'tags', 'product', 'waiting_for_reply', 'agent_id', 'mailbox_id'])) {
201 $query = (new \FluentSupport\App\Models\Ticket())->buildPropertiesFilterQuery($query, $s);
202 }
203 }
204 return $query;
205 }
206
207 /**
208 * Local scope to filter subscribers by search/query string
209 * @param ModelQueryBuilder $query
210 * @param array $statuses
211 * @return ModelQueryBuilder
212 */
213 public function scopeFilterByStatues($query, $statuses)
214 {
215 if ($statuses) {
216 $query->whereIn('status', $statuses);
217 }
218
219 return $query;
220 }
221
222 /**
223 * Local scope to filter tickets by not response by agent
224 * @param $query
225 * @return mixed
226 */
227 public function scopeWaitingOnly($query)
228 {
229 $query->where(function ($q) {
230 $q->whereColumn('last_agent_response', '<', 'last_customer_response')
231 ->orWhereNull('last_agent_response')
232 ->orWhere('status', 'new');
233 });
234 return $query;
235 }
236
237 /**
238 * scopeApplyFilters method will filet ticket based on the selected filters
239 * This method will get filter option as parameter, loop through and apply conditions in query
240 * @param $query
241 * @param $filters
242 * @return ModelQueryBuilder
243 */
244 public function scopeApplyFilters($query, $filters)
245 {
246 $supportedColumns = ['product_id', 'client_priority', 'priority', 'mailbox_id'];
247 foreach ($filters as $filterKey => $filterValue) {
248 if (!$filterValue && ($filterValue !== '0' && $filterValue !== 0)) {
249 continue;
250 }
251 //If filer using status
252 if ($filterKey == 'status_type') {
253 //Get list of ticket status
254 $statusArray = Helper::getTkStatusesByGroupName($filterValue);
255 if ($statusArray) {
256 //Apply filet where status in
257 $query->whereIn('status', $statusArray);
258 }
259 } else if (in_array($filterKey, $supportedColumns)) {
260 // Use whereIn for all supported columns (they all now support multi-select)
261 if (is_array($filterValue)) {
262 $query->whereIn($filterKey, $filterValue);
263 } else {
264 $query->where($filterKey, $filterValue);
265 }
266 } else if ($filterKey == 'waiting_for_reply') {
267 if ($filterValue != 'yes') {
268 continue;
269 }
270 //Apply filter where no response by agent
271 $query = $this->scopeWaitingOnly($query);
272 } else if ($filterKey == 'agent_id') {
273 // Handle array of agent IDs for multi-select
274 if (is_array($filterValue)) {
275 // Check if 'unassigned' is in the array
276 $hasUnassigned = in_array('unassigned', $filterValue);
277 $agentIds = array_filter($filterValue, function($v) {
278 return $v !== 'unassigned';
279 });
280
281 if ($hasUnassigned && !empty($agentIds)) {
282 // Include both unassigned and specific agents
283 $query->where(function($q) use ($agentIds) {
284 $q->whereNull('agent_id')
285 ->orWhereIn('agent_id', $agentIds);
286 });
287 } elseif ($hasUnassigned) {
288 // Only unassigned
289 $query->whereNull('agent_id');
290 } elseif (!empty($agentIds)) {
291 // Only specific agents
292 if (defined('FLUENTSUPPORTPRO')) {
293 if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
294 $watcherTickets = [];
295 foreach ($agentIds as $agentId) {
296 $watcherTickets = array_merge($watcherTickets, TicketHelper::getWatcherTicketIds($agentId));
297 }
298 $query->whereIn('id', array_unique($watcherTickets));
299 } else {
300 $query->whereIn('agent_id', $agentIds);
301 }
302 } else {
303 $query->whereIn('agent_id', $agentIds);
304 }
305 }
306 } else {
307 // Single value (backward compatibility)
308 if ($filterValue == 'unassigned') {
309 $query->whereNull($filterKey);
310 } else {
311 if (defined('FLUENTSUPPORTPRO')) {
312 if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
313 $watcherTickets = TicketHelper::getWatcherTicketIds($filterValue);
314 $query->whereIn('id', $watcherTickets);
315 } else {
316 //Apply filter, get only assigned ticket
317 $query->where($filterKey, $filterValue);
318 }
319 } else {
320 $query->where($filterKey, $filterValue);
321 }
322 }
323 }
324 } else if ($filterKey == 'agent_group') {
325 $groupIds = is_array($filterValue) ? $filterValue : [$filterValue];
326 $groupIds = array_filter(array_map('intval', $groupIds));
327 if (!empty($groupIds)) {
328 $agentIds = TagPivot::where('source_type', 'agent_group')
329 ->whereIn('tag_id', $groupIds)
330 ->pluck('source_id')
331 ->toArray();
332 if ($agentIds) {
333 $query->whereIn('agent_id', $agentIds);
334 } else {
335 $query->whereRaw('1 = 0');
336 }
337 }
338 } else if ($filterKey == 'ticket_tags') {
339 if (!$filterValue) {
340 continue;
341 }
342 //Apply filter where ticket only has this tag id
343 $query->whereHas('tags', function ($q) use ($filterValue) {
344 $q->whereIn('tag_id', $filterValue);
345 });
346 }
347 }
348
349 return $query;
350 }
351
352 /**
353 * Local scope to filter tickets by agent id
354 * @param ModelQueryBuilder $query
355 * @param int $agentId
356 * @return ModelQueryBuilder
357 */
358 public function scopeFilterByAgentId($query, $agentId)
359 {
360 if ($agentId) {
361 $query->where('agent_id', $agentId);
362 }
363
364 return $query;
365 }
366
367 /**
368 * Local scope to filter subscribers by search/query string
369 * @param ModelQueryBuilder $query
370 * @param int $customerId
371 * @return ModelQueryBuilder
372 */
373 public function scopeFilterByCustomerId($query, $customerId)
374 {
375 $query->where('customer_id', $customerId);
376
377 return $query;
378 }
379
380 /**
381 * Local scope to filter subscribers by search/query string
382 * @param ModelQueryBuilder $query
383 * @param int $productId
384 * @return ModelQueryBuilder
385 */
386 public function scopeFilterByProductId($query, $productId)
387 {
388 if ($productId) {
389 $query->where('product_id', $productId);
390 }
391
392 return $query;
393 }
394
395 /**
396 * Local scope to filter subscribers by search/query string
397 * @param ModelQueryBuilder $query
398 * @param array $priorities
399 * @return ModelQueryBuilder
400 */
401 public function scopeFilterByPriorities($query, $priorities)
402 {
403 if ($priorities) {
404 $query->whereIn('priority', $priorities);
405 }
406
407 return $query;
408 }
409
410 /**
411 * @param $filter
412 * @return string[]
413 */
414
415 public static function parseRelationalFilterQueryMethods($filter)
416 {
417 // default operator = in
418 $method = 'whereHas';
419 $subMethod = 'whereIn';
420
421 switch ($filter['operator']) {
422 case 'not_in':
423 $method = 'whereDoesntHave';
424 $subMethod = 'whereIn';
425
426 break;
427 case 'in_all':
428 $method = 'whereHas';
429 $subMethod = 'where';
430
431 break;
432 case 'not_in_all':
433 $method = 'whereDoesntHave';
434 $subMethod = 'where';
435
436 break;
437 }
438
439 return [$method, $subMethod];
440 }
441
442 /**
443 * Parse filter to set proper operator and value for the filter query.
444 *
445 * @param array $filter
446 * @return array
447 */
448 public static function filterParser($filter)
449 {
450 switch ($filter['operator']) {
451 case 'before':
452 $filter['operator'] = '<';
453 $filter['value'] = $filter['value'] . ' 23:59:59';
454 break;
455
456 case 'after':
457 $filter['operator'] = '>';
458 $filter['value'] = $filter['value'] . ' 23:59:59';
459 break;
460
461 case 'date_equal':
462 $filter['operator'] = 'LIKE';
463 $filter['value'] = '%' . $filter['value'] . '%';
464 break;
465
466 case 'days_before':
467 $filter['operator'] = '<';
468 $filter['value'] = gmdate('Y-m-d', time() - $filter['value'] * 24 * 60 * 60);
469 break;
470
471 case 'days_within':
472 $filter['operator'] = 'BETWEEN';
473 $filter['value'] = [
474 gmdate('Y-m-d', time() - $filter['value'] * 24 * 60 * 60),
475 gmdate('Y-m-d') . ' 23:59:59'
476 ];
477 break;
478 case 'date_range':
479 $filter['operator'] = 'BETWEEN';
480 if (isset($filter['value'][0]))
481 $filter['value'][0] .= ' 00:00:00';
482 if (isset($filter['value'][1]))
483 $filter['value'][1] .= ' 23:59:59';
484 break;
485 }
486
487 return $filter;
488 }
489
490 /**
491 * @param \FluentSupport\Framework\Database\Orm\Builder|\FluentSupport\Framework\Database\Query\Builder $query
492 * @param array $filters
493 * @return ModelQueryBuilder
494 */
495 public function buildDateBaseFilterQuery($query, $filters)
496 {
497 $filter = static::filterParser($filters);
498 $query->where(function ($dateQuery) use ($filter) {
499
500 if ($filter['operator'] == 'BETWEEN') {
501 $dateQuery->whereBetween($filter['property'], $filter['value']);
502 } else {
503 $dateQuery->where($filter['property'], $filter['operator'], $filter['value']);
504 }
505 });
506
507 return $query;
508 }
509
510 /**
511 * Relation builder
512 * @param $relation
513 * @param $query
514 * @param $method
515 * @param $subMethod
516 * @param $subField
517 * @param $filter
518 * @param false $provider
519 * @return ModelQueryBuilder
520 */
521
522 public static function buildRelationFilterQuery($relation, $query, $method, $subMethod, $subField, $filter, $provider = false)
523 {
524 if (in_array($filter['operator'], ['in_all', 'not_in_all']) && $filter['value']) {
525 foreach ($filter['value'] as $item) {
526 $query = static::buildRelationFilterQuery($relation, $query, $method, $subMethod, $subField, ['value' => $item, 'operator' => ''], $provider);
527 }
528 } else {
529 $query = $query->{$method}($relation, function ($relationQuery) use ($subMethod, $subField, $filter, $provider) {
530 $relationQuery = $relationQuery->{$subMethod}($subField, $filter['value']);
531
532 if ($provider) {
533 $relationQuery = $relationQuery->where('provider', $provider);
534 }
535
536 return $relationQuery;
537 });
538 }
539
540 return $query;
541 }
542
543 /**
544 * get tickets by advanced filter segment data
545 * @param $query
546 * @param $filter
547 * @return ModelQueryBuilder
548 */
549
550 public function buildPropertiesFilterQuery($query, $filter)
551 {
552 if (in_array($filter['property'], ['tags', 'product'])) {
553 $subField = $filter['property'] == 'tags' ? 'tag_id' : 'product_id';
554 list($method, $subMethod) = static::parseRelationalFilterQueryMethods($filter);
555 $query = static::buildRelationFilterQuery($filter['property'], $query, $method, $subMethod, $subField, $filter);
556 } elseif ($filter['property'] == 'waiting_for_reply') {
557 if (($filter['value'] == 'yes' && $filter['operator'] == 'in') || ($filter['value'] == 'no' && $filter['operator'] == 'not_in')) {
558 $query = $query->where(function ($q) {
559 $q->whereColumn('last_agent_response', '<', 'last_customer_response')
560 ->orWhereNull('last_agent_response')
561 ->orWhere('status', 'new');
562 });
563 } else {
564 $query = $query->where(function ($q) {
565 $q->whereColumn('last_customer_response', '<', 'last_agent_response');
566 });
567 }
568 } else {
569 $method = $filter['operator'] == 'in' ? 'whereIn' : 'whereNotIn';
570 $query = $query->{$method}($filter['property'], (array)$filter['value']);
571 }
572 return $query;
573 }
574
575 /**
576 * method to search by properties
577 * @param $provider
578 * @param $query
579 * @param $search
580 * @param string $operator
581 * @return ModelQueryBuilder
582 */
583 public function buildSearchableQuery($provider, $query, $search, $operator = 'LIKE')
584 {
585 switch ($provider) {
586 case 'customer':
587 $fields = (new Customer())->getSearchableFields();
588 break;
589 case 'agent':
590 $fields = (new Agent())->getSearchableFields();
591 break;
592 default:
593 $fields = $this->searchable;
594 break;
595 }
596
597 $query->whereHas($provider, function ($query) use ($fields, $search, $operator) {
598 $query->where(array_shift($fields), $operator, $search);
599
600 $nameArray = explode(' ', (string) $search);
601
602 if (count($nameArray) >= 2) {
603 $query->orWhere(function ($q) use ($nameArray, $operator) {
604 $firstName = array_shift($nameArray);
605 $lastName = implode(' ', $nameArray);
606
607 $q->where('first_name', $operator, $firstName);
608 $q->where('last_name', $operator, $lastName);
609 });
610 }
611
612 foreach ($fields as $field) {
613 $query->orWhere($field, $operator, $search);
614 }
615 });
616
617 return $query;
618 }
619
620 /**
621 * Filter by ticket general properties like customer name, agent name etc
622 * @param $provider
623 * @param $query
624 * @param $filters
625 * @return ModelQueryBuilder
626 */
627 public function filterTicketByUser($provider, $query, $filters)
628 {
629 foreach ($filters as $filter) {
630 if ($filter['operator'] == 'in' || $filter['operator'] == 'not_in') {
631 $method = $filter['operator'] == 'in' ? 'whereIn' : 'whereNotIn';
632 $query = $query->whereHas($provider, function ($q) use ($method, $filter) {
633 $q->{$method}($filter['property'], $filter['value']);
634 });
635 }
636
637 if ($filter['operator'] == 'contains' || $filter['operator'] == 'not_contains') {
638 $operator = $filter['operator'] == 'contains' ? 'LIKE' : 'NOT LIKE';
639 $query->whereHas($provider, function ($q) use ($operator, $filter) {
640 $q->where($filter['property'], $operator, '%' . $filter['value'] . '%');
641 });
642 }
643
644 if ($filter['operator'] == '=' || $filter['operator'] == '!=') {
645 $operator = $filter['operator'];
646 $query->whereHas($provider, function ($q) use ($operator, $filter) {
647 $q->where($filter['property'], $operator, $filter['value']);
648 });
649 }
650 }
651 return $query;
652 }
653
654 /**
655 * One2Many: Customer has to many Click Tickets
656 * @return Model Collection
657 */
658 public function responses()
659 {
660 $class = __NAMESPACE__ . '\Conversation';
661
662 return $this->hasMany(
663 $class, 'ticket_id', 'id'
664 )->orderBy('created_at', 'desc')
665 ->orderBy('id', 'desc');
666 }
667
668 public function preview_response()
669 {
670 $class = __NAMESPACE__ . '\Conversation';
671
672 return $this->hasOne(
673 $class, 'ticket_id', 'id'
674 );
675 }
676
677 public function tags()
678 {
679 $class = __NAMESPACE__ . '\TicketTag';
680
681 return $this->belongsToMany(
682 $class, 'fs_tag_pivot', 'source_id', 'tag_id'
683 )->wherePivot('source_type', 'ticket_tag');
684 }
685
686 public function watchers()
687 {
688 $class = __NAMESPACE__ . '\TagPivot';
689
690 return $this->hasMany($class, 'source_id', 'id')
691 ->where('source_type', 'ticket_watcher')
692 ->select(['tag_id']);
693 }
694
695 /**
696 * One2one: Customer has to many Click Tickets
697 * @return Model Collection
698 */
699 public function customer()
700 {
701 $class = __NAMESPACE__ . '\Customer';
702
703 return $this->belongsTo(
704 $class, 'customer_id', 'id'
705 );
706 }
707
708 /**
709 * One2one: Customer has to many Click Tickets
710 * @return Model Collection
711 */
712 public function agent()
713 {
714 $class = __NAMESPACE__ . '\Agent';
715
716 return $this->belongsTo(
717 $class, 'agent_id', 'id'
718 );
719 }
720
721 public function closed_by_person()
722 {
723 $class = __NAMESPACE__ . '\Person';
724
725 return $this->belongsTo(
726 $class, 'closed_by', 'id'
727 );
728 }
729
730 public function created_by_person()
731 {
732 $class = __NAMESPACE__ . '\Agent';
733
734 return $this->belongsTo(
735 $class, 'created_by', 'id'
736 );
737 }
738
739 public function product()
740 {
741 $class = __NAMESPACE__ . '\Product';
742
743 return $this->belongsTo(
744 $class, 'product_id', 'id'
745 );
746 }
747
748 public function mailbox()
749 {
750 $class = __NAMESPACE__ . '\MailBox';
751
752 return $this->belongsTo(
753 $class, 'mailbox_id', 'id'
754 );
755 }
756
757
758 public function deleteTicket()
759 {
760 /*
761 * Action on ticket deleting
762 *
763 * @since v1.0.0
764 * @param object $ticket
765 */
766 do_action('fluent_support/deleting_ticket', $this);
767 // Delete the ticket
768 $this->delete();
769 }
770
771 public static function getNextSerialNumber()
772 {
773 $businessSettings = Helper::getOption('global_business_settings', []);
774 $minNumber = (int) ($businessSettings['min_serial_number'] ?? 1);
775 $minNumber = (int) apply_filters('fluent_support/min_serial_number', $minNumber);
776
777 try {
778 $lastTicketNumber = self::query()->max('serial_number');
779 } catch (\Exception $e) {
780 $lastTicketNumber = null;
781 }
782
783 $nextNumber = ((int) $lastTicketNumber) + 1;
784
785 return max($nextNumber, $minNumber);
786 }
787
788 public static function isMinimumSerialNumberEnabled()
789 {
790 $businessSettings = Helper::getOption('global_business_settings', []);
791 return ($businessSettings['enable_min_serial_number'] ?? 'no') === 'yes';
792 }
793
794 public static function getTicketPrefix($ticket = null)
795 {
796 $businessSettings = Helper::getOption('global_business_settings', []);
797 $prefix = self::isMinimumSerialNumberEnabled() ? trim((string) ($businessSettings['ticket_prefix'] ?? '')) : '';
798
799 $productId = $ticket ? $ticket->product_id : null;
800
801 return apply_filters('fluent_support/ticket_prefix', $prefix, $ticket, $productId);
802 }
803
804 public function getDisplayTicketNumberAttribute()
805 {
806 return $this->ticket_number ?: ($this->serial_number ?: $this->id);
807 }
808
809 public function scopeWherePublicIdentifier($query, $identifier)
810 {
811 return $query->where('serial_number', $identifier);
812 }
813
814 protected function assignTicketNumber()
815 {
816 for ($attempt = 0; $attempt < 5; $attempt++) {
817 $nextNumber = $this->serial_number ?: (self::isMinimumSerialNumberEnabled() ? self::getNextSerialNumber() : $this->id);
818 $ticketNumber = $this->ticket_number ?: (self::getTicketPrefix($this) . $nextNumber);
819
820 try {
821 self::where('id', $this->id)->update([
822 'serial_number' => $nextNumber,
823 'ticket_number' => $ticketNumber
824 ]);
825 $this->serial_number = $nextNumber;
826 $this->ticket_number = $ticketNumber;
827
828 return $nextNumber;
829 } catch (\Exception $e) {
830 if (stripos($e->getMessage(), 'duplicate') === false) {
831 throw $e;
832 }
833 }
834 }
835
836 throw new \RuntimeException('Could not allocate a unique ticket number.');
837 }
838
839 public static function slugify($title)
840 {
841 $slug = sanitize_title($title, 'support-ticket-' . time(), 'display');
842 if (Ticket::where('slug', $slug)->first()) {
843 $slug .= '-' . time();
844 }
845 return $slug;
846 }
847
848 public function hasTag($tagId)
849 {
850 $tags = $this->tags;
851 foreach ($tags as $tag) {
852 if ($tag->id == $tagId) {
853 return true;
854 }
855 }
856
857 return false;
858 }
859
860 public function attachments()
861 {
862 $class = __NAMESPACE__ . '\Attachment';
863 return $this->hasMany($class, 'ticket_id', 'id')->where('conversation_id', NULL);
864 }
865
866 public function customData($scope = 'admin', $rendered = false)
867 {
868 if (!defined('FLUENTSUPPORTPRO')) {
869 return [];
870 }
871
872 $fields = \FluentSupportPro\App\Services\CustomFieldsService::getFieldLabels($scope);
873
874 if (!$fields) {
875 return [];
876 }
877
878 $keys = array_keys($fields);
879
880 $customRows = Meta::where('object_type', 'ticket_meta')->where('object_id', $this->id)
881 ->whereIn('key', $keys)
882 ->get();
883
884 if (!$customRows) {
885 return [];
886 }
887
888 $formattedData = [];
889
890 $customRenderers = \FluentSupportPro\App\Services\CustomFieldsService::getCustomerRenderers();
891
892 foreach ($customRows as $row) {
893 $dataKey = $row->key;
894
895 $value = $row->value;
896
897 $fieldType = $fields[$dataKey]['type'];
898
899 if ($value) {
900 if (in_array($fieldType, $customRenderers) && $rendered) {
901 $value = apply_filters('fluent_support/custom_field_render_' . $fieldType, $value, $scope);
902 } else if ($fieldType == 'checkbox') {
903 $value = array_values(array_filter(explode('|', $value)));
904 }
905
906 if (!is_array($value) && !is_object($value)) {
907 $formattedData[$dataKey] = links_add_target(make_clickable($value));
908 } else {
909 $formattedData[$dataKey] = $value;
910 }
911 }
912 }
913
914 return $formattedData;
915 }
916
917 /**
918 * @param $data This is the data that will be saved to the ticket_meta for custom fields
919 * @return bool
920 */
921 public function syncCustomFields($data)
922 {
923 if (!is_array($data)) {
924 return false;
925 }
926
927 $fields = apply_filters('fluent_support/ticket_custom_fields', []);
928
929 if (!$fields) {
930 return false;
931 }
932
933 $keys = array_keys($fields);
934
935 $validData = Arr::only($data, $keys);
936
937 foreach ($validData as $dataKey => $validDatum) {
938 if (empty($validDatum)) {
939 Meta::where('object_type', 'ticket_meta')
940 ->where('object_id', $this->id)
941 ->where('key', $dataKey)
942 ->delete();
943 continue;
944 }
945
946 if ($fields[$dataKey]['type'] == 'checkbox' || is_array($validDatum)) {
947 $validDatum = implode('|', $validDatum);
948 $validDatum = '|' . $validDatum . '|';
949 }
950
951 $exist = Meta::where('object_type', 'ticket_meta')
952 ->where('object_id', $this->id)
953 ->where('key', $dataKey)
954 ->first();
955
956 if ($exist) {
957 $exist->value = $validDatum;
958 $exist->save();
959 } else {
960 Meta::insert([
961 'object_type' => 'ticket_meta',
962 'object_id' => $this->id,
963 'key' => $dataKey,
964 'value' => $validDatum
965 ]);
966 }
967 }
968
969 return true;
970 }
971
972 public function getLastAgentResponse()
973 {
974 $query = \FluentSupport\App\App::db()->table('fs_conversations')
975 ->select(['fs_conversations.*'])
976 ->where('fs_conversations.conversation_type', 'response')
977 ->where('fs_conversations.ticket_id', $this->id)
978 ->where('fs_persons.person_type', '=', 'agent')
979 ->join('fs_persons', 'fs_persons.id', '=', 'fs_conversations.person_id')
980 ->orderBy('fs_conversations.id', 'DESC');
981
982 return $query->first();
983 }
984
985 public function getLastResponse()
986 {
987 return \FluentSupport\App\App::db()->table('fs_conversations')
988 ->where('ticket_id', $this->id)
989 ->where('conversation_type', 'response')
990 ->latest('id')
991 ->first();
992 }
993
994 /**
995 * This method will assign tags to the ticket
996 * @param $tagIds This is the array of tag ids that will be assigned to the ticket
997 * @return \FluentSupport\App\Models\Ticket
998 */
999 public function applyTags($tagIds)
1000 {
1001 $result = false;
1002
1003 if (!is_array($tagIds)) {
1004 $tagIds = array($tagIds);
1005 }
1006
1007 foreach ($tagIds as $tagId) {
1008 if (!$this->hasTag($tagId)) {
1009 $this->tags()->attach($tagId, ['source_type' => 'ticket_tag']);
1010 $result = true;
1011
1012 /*
1013 * Action while tag added to ticket
1014 *
1015 * @since v1.0.0
1016 * @param integer $tagId
1017 * @param object $ticket
1018 */
1019 do_action('fluent_support/ticket_tag_added', $tagId, $this);
1020 }
1021 }
1022 return $result;
1023 }
1024
1025 /**
1026 * This method will remove tags from ticket
1027 * @param $tagIds This is the array of tag ids that will be removed from the ticket
1028 * @return \FluentSupport\App\Models\Ticket
1029 */
1030 public function detachTags($tagIds)
1031 {
1032 $result = false;
1033
1034 if (!is_array($tagIds)) {
1035 $tagIds = array($tagIds);
1036 }
1037
1038 foreach ($tagIds as $tagId) {
1039 if ($this->hasTag($tagId)) {
1040 $this->tags()->detach($tagId);
1041
1042 /*
1043 * Action while tag removed from ticket
1044 *
1045 * @since v1.0.0
1046 * @param integer $tagId
1047 * @param object $ticket
1048 */
1049 do_action('fluent_support/ticket_tag_removed', $tagId, $this);
1050 $result = true;
1051 }
1052 }
1053 return $result;
1054 }
1055
1056 /**
1057 * @deprecated Use TicketService::storeTicket() instead.
1058 */
1059 public function createTicket($ticketData, $maybeNewCustomer = false)
1060 {
1061 _deprecated_function(__METHOD__, '2.0.5', 'TicketService::storeTicket()');
1062
1063 if (empty($ticketData['customer_id']) && $maybeNewCustomer) {
1064 $email = Arr::get($maybeNewCustomer, 'email');
1065 if (!$email || !is_email($email)) {
1066 return new \WP_Error('error', 'A valid email is required to create a ticket');
1067 }
1068
1069 $existingCustomer = Customer::where('email', $email)->first();
1070 if ($existingCustomer) {
1071 $ticketData['customer_id'] = $existingCustomer->id;
1072 } else {
1073 $customerData = Arr::only($maybeNewCustomer, (new Customer())->getFillable());
1074 $customerData = array_filter($customerData);
1075 $createCustomer = Customer::create($customerData);
1076 if (!$createCustomer) {
1077 return new \WP_Error('error', 'Customer could not be created');
1078 }
1079 $ticketData['customer_id'] = $createCustomer->id;
1080 }
1081 }
1082
1083 if (empty($ticketData['customer_id'])) {
1084 return new \WP_Error('error', 'Ticket could not be created');
1085 }
1086
1087 $customer = Customer::findOrFail($ticketData['customer_id']);
1088
1089 return (new TicketService())->storeTicket($ticketData, $customer);
1090 }
1091
1092 /**
1093 * This `createResponse` will create a response for a ticket
1094 * @param array $data
1095 * @param int $ticketId
1096 * @return array
1097 * @throws Exception
1098 */
1099
1100 public static function countTicketByMailBoxId($mailbox_id)
1101 {
1102 return self::where('mailbox_id', $mailbox_id)->count();
1103 }
1104
1105 public static function syncMailBoxId($mailbox_id, $fallback_id)
1106 {
1107 return self::where('mailbox_id', $mailbox_id)
1108 ->update([
1109 'mailbox_id' => $fallback_id
1110 ]);
1111 }
1112
1113 public static function getTicketsQuery()
1114 {
1115 return self::with([
1116 'customer' => function ($query) {
1117 $query->select(['first_name', 'last_name', 'email', 'id', 'avatar']);
1118 }, 'agent' => function ($query) {
1119 $query->select(['first_name', 'last_name', 'id']);
1120 },
1121 'product',
1122 'tags',
1123 'preview_response' => function ($query) {
1124 $query->latest('id');
1125 }
1126 ]);
1127 }
1128
1129 public function getSettingsValue($valueKey = false, $default = false)
1130 {
1131 $exist = Meta::where('object_type', 'ticket')
1132 ->where('key', 'settings')
1133 ->where('object_id', $this->id)
1134 ->first();
1135
1136 if ($exist) {
1137 $value = Helper::safeUnserialize($exist->value);
1138 if ($valueKey) {
1139 if (!is_array($value)) {
1140 return $default;
1141 }
1142 return Arr::get($value, $valueKey, $default);
1143 }
1144 return $value;
1145 }
1146
1147 return $default;
1148 }
1149
1150 public function updateSettingsValue($valueKey, $value)
1151 {
1152 $exist = Meta::where('object_type', 'ticket')
1153 ->where('key', 'settings')
1154 ->where('object_id', $this->id)
1155 ->first();
1156
1157 if ($exist) {
1158 $existingValue = Helper::safeUnserialize($exist->value);
1159
1160 if (!is_array($existingValue)) {
1161 $existingValue = [];
1162 }
1163
1164 $existingValue[$valueKey] = $value;
1165
1166 $exist->value = maybe_serialize($existingValue);
1167 $exist->save();
1168 return $this;
1169 }
1170
1171 $settings = [
1172 'object_type' => 'ticket',
1173 'key' => 'settings',
1174 'object_id' => $this->id,
1175 'value' => maybe_serialize([
1176 $valueKey => $value
1177 ])
1178 ];
1179
1180 Meta::create($settings);
1181
1182 return $this;
1183
1184 }
1185
1186 }
1187