PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 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 All 69 releases
← All changes | app/Models/Ticket.php +347 -722 1.10.2 → 2.4.5 View file →
@@ -2,15 +2,12 @@
2 2
3 3 namespace FluentSupport\App\Models;
4 4
5 5 use Exception;
6 -use FluentSupport\App\Http\Controllers\AuthController;
7 6 use FluentSupport\App\Modules\PermissionManager;
8 7 use FluentSupport\App\Services\Helper;
9 -use FluentSupport\App\Services\ProfileInfoService;
10 8 use FluentSupport\App\Services\TicketHelper;
11 9 use FluentSupport\App\Services\TicketQueryService;
12 -use FluentSupport\App\Services\Tickets\ResponseService;
13 10 use FluentSupport\App\Services\Tickets\TicketService;
14 11 use FluentSupport\Framework\Support\Arr;
15 12
16 13 class Ticket extends Model
@@ -19,8 +16,18 @@
19 16
20 17 protected $dates = ['waiting_since'];
21 18
22 19 /**
20 + * The hash is a bearer credential for the public ticket view, so keep it
21 + * out of API responses. Property access still works for signed URLs.
22 + *
23 + * @var array
24 + */
25 + protected $hidden = ['hash', 'content_hash'];
26 +
27 + protected $appends = ['display_ticket_number'];
28 +
29 + /**
23 30 * The attributes that are mass assignable.
24 31 *
25 32 * @var array
26 33 */
@@ -46,9 +53,12 @@
46 53 'response_count',
47 54 'first_response_time',
48 55 'total_close_time',
49 56 'resolved_at',
50 - 'closed_by'
57 + 'closed_by',
58 + 'created_by',
59 + 'serial_number',
60 + 'ticket_number'
51 61 ];
52 62
53 63 public static function boot()
54 64 {
@@ -58,9 +68,9 @@
58 68 if (empty($model->slug)) {
59 69 $model->slug = static::slugify($model->title);
60 70 }
61 71
62 - $model->hash = substr(md5(time() . wp_generate_uuid4()), 0, 8) . wp_rand(1, 99);
72 + $model->hash = bin2hex(random_bytes(16));
63 73 $model->content_hash = md5($model->content);
64 74
65 75 $model->last_customer_response = current_time('mysql');
66 76 $model->created_at = current_time('mysql');
@@ -68,22 +78,40 @@
68 78 $model->waiting_since = current_time('mysql');
69 79
70 80 });
71 81
82 + static::updating(function ($model) {
83 + // Rotate the hash on customer change so links already emailed to
84 + // the previous customer stop working.
85 + if ($model->isDirty('customer_id')) {
86 + $model->hash = bin2hex(random_bytes(16));
87 + }
88 + });
89 +
90 + static::created(function ($model) {
91 + if (empty($model->serial_number) || empty($model->ticket_number)) {
92 + $model->assignTicketNumber();
93 + }
94 + });
95 +
72 96 static::deleting(function ($model) {
73 - //Delete the ticket meta
74 97 Meta::where('object_type', 'ticket_meta')->where('object_id', $model->id)->delete();
75 - //Delete all cc info for the ticket
98 + // CC info
76 99 Meta::where('object_type', 'ticket')->where('object_id', $model->id)->delete();
77 - //Delete draft info
100 + // Drafts
78 101 Meta::where('object_type', '_fs_auto_draft')->where('object_id', $model->id)->delete();
79 - //delete the responses first
102 + Notification::deleteByTicketId($model->id);
103 + // Response attachments are cleaned up by Conversation::deleting
80 104 Conversation::deleteAll($model->id);
105 + // Ticket-level attachments, plus the ticket upload directory
106 + $class = __NAMESPACE__ . '\Attachment';
107 + $ticketAttachments = $class::where('ticket_id', $model->id)->whereNull('conversation_id')->get();
108 + $class::purgeAttachments($ticketAttachments, $model->id);
109 + $class::where('ticket_id', $model->id)->whereNull('conversation_id')->delete();
81 110 });
82 111 }
83 112
84 113 /**
85 - * $searchable Columns in table to search
86 114 * @var array
87 115 */
88 116 protected $searchable = [
89 117 'content',
@@ -88,13 +116,16 @@
88 116 protected $searchable = [
89 117 'content',
90 118 'title',
91 119 'slug',
92 - 'id'
120 + 'id',
121 + 'serial_number',
122 + 'ticket_number'
93 123 ];
94 124
95 125 /**
96 - * Local scope to filter tickets by search/query string
126 + * Supports `column:value` to match a single fillable column.
127 + *
97 128 * @param ModelQueryBuilder $query
98 129 * @param string $search
99 130 * @return ModelQueryBuilder
100 131 */
@@ -99,10 +130,15 @@
99 130 * @return ModelQueryBuilder
100 131 */
101 132 public function scopeSearchBy($query, $search)
102 133 {
134 +
135 + if(!$search) {
136 + return $query;
137 + }
138 +
103 139 if (strpos($search, ':')) {
104 - $array = explode(':', $search);
140 + $array = explode(':', (string) $search);
105 141 $column = $array[0];
106 142 $value = $array[1];
107 143 $columns = $this->fillable;
108 144 $columns[] = 'id';
@@ -128,11 +164,10 @@
128 164 return $query;
129 165 }
130 166
131 167 /**
132 - * Local scope to filter tickets by different filtering condition
133 168 * @param ModelQueryBuilder $query
134 - * @param mixed $search
169 + * @param array $search
135 170 * @return ModelQueryBuilder
136 171 */
137 172
138 173 public function doSearchForAdvancedFilter($query, $search)
@@ -138,17 +173,14 @@
138 173 public function doSearchForAdvancedFilter($query, $search)
139 174 {
140 175 foreach ($search as $s) {
141 176 $operator = $s['operator'];
142 - //If selected item for ticket either title or content
143 177 if (in_array($s['property'], ['title', 'content'])) {
144 - //If the selected condition is contains, query operator id LIKE
145 178 if ($operator == 'contains') {
146 179 $query = $query->where(function ($query) use ($s) {
147 180 $query->where($s['property'], 'LIKE', "%" . $s['value'] . "%");
148 181 });
149 182 } elseif ($operator == 'not_contains') {
150 - //If the selected condition is not_contains, query operator id NOT LIKE
151 183 $query = $query->where(function ($query) use ($s) {
152 184 $query->where($s['property'], 'NOT LIKE', '%' . $s['value'] . '%');
153 185 });
154 186 }
@@ -153,9 +185,8 @@
153 185 });
154 186 }
155 187 }
156 188
157 - //If selected item is Ticket Conversation Content
158 189 if ($s['property'] == 'conversation_content') {
159 190 $operator = $s['operator'];
160 191 if ($operator == 'contains') {
161 192 $query = $query->whereHas('responses', function ($q) use ($s) {
@@ -168,14 +199,16 @@
168 199 });
169 200 }
170 201 }
171 202
172 - //If selected item is Ticket created or Last Response or Customer Waiting For, or Last Agent Response or Last Customer Response
203 + if ($s['property'] == 'source') {
204 + $query = static::buildSourceFilterQuery($query, $s);
205 + }
206 +
173 207 if (in_array($s['property'], ['created_at', 'updated_at', 'waiting_since', 'last_agent_response', 'last_customer_response'])) {
174 208 $query = (new \FluentSupport\App\Models\Ticket())->buildDateBaseFilterQuery($query, $s);
175 209 }
176 210
177 - //If selected item is Ticket Status or Client Priority or Agent Priority or Tags or Product or Waiting For Reply
178 211 if (in_array($s['property'], ['status', 'client_priority', 'priority', 'tags', 'product', 'waiting_for_reply', 'agent_id', 'mailbox_id'])) {
179 212 $query = (new \FluentSupport\App\Models\Ticket())->buildPropertiesFilterQuery($query, $s);
180 213 }
181 214 }
@@ -181,10 +214,78 @@
181 214 }
182 215 return $query;
183 216 }
184 217
218 + /** Advanced-filter value for tickets with no recorded source (older tickets). */
219 + const SOURCE_NONE = '__none__';
220 +
185 221 /**
186 - * Local scope to filter subscribers by search/query string
222 + * Where tickets come from, for the advanced filter's Source field.
223 + *
224 + * @return array<string,string> source => label
225 + */
226 + public static function sourceFilterOptions()
227 + {
228 + return apply_filters('fluent_support/ticket_source_filter_options', [
229 + 'web' => __('Web', 'fluent-support'),
230 + 'email' => __('Email', 'fluent-support'),
231 + self::SOURCE_AGENT_INITIATED => __('Agent outreach', 'fluent-support'),
232 + 'mcp' => __('MCP (AI agents)', 'fluent-support'),
233 + 'telegram' => __('Telegram', 'fluent-support'),
234 + 'slack' => __('Slack', 'fluent-support'),
235 + 'whatsapp' => __('WhatsApp', 'fluent-support'),
236 + 'awesome-support' => __('Imported: Awesome Support', 'fluent-support'),
237 + 'freshdesk' => __('Imported: Freshdesk', 'fluent-support'),
238 + 'helpscout' => __('Imported: Help Scout', 'fluent-support'),
239 + 'js-helpdesk' => __('Imported: JS Help Desk', 'fluent-support'),
240 + 'support-candy' => __('Imported: SupportCandy', 'fluent-support'),
241 + 'zendesk' => __('Imported: Zendesk', 'fluent-support'),
242 + self::SOURCE_NONE => __('No source', 'fluent-support'),
243 + ]);
244 + }
245 +
246 + /**
247 + * Source "in" / "not in". SOURCE_NONE stands for NULL or empty, which SQL's
248 + * IN and NOT IN would otherwise skip.
249 + *
250 + * @param \FluentSupport\Framework\Database\Orm\Builder $query
251 + * @param array $filter
252 + * @return \FluentSupport\Framework\Database\Orm\Builder
253 + */
254 + public static function buildSourceFilterQuery($query, $filter)
255 + {
256 + $values = array_values(array_filter(array_map('sanitize_text_field', (array) Arr::get($filter, 'value', [])), 'strlen'));
257 + if (!$values) {
258 + return $query;
259 + }
260 +
261 + $includeNone = in_array(self::SOURCE_NONE, $values, true);
262 + $sources = array_values(array_diff($values, [self::SOURCE_NONE]));
263 +
264 + if (Arr::get($filter, 'operator') === 'not_in') {
265 + return $query->where(function ($q) use ($sources, $includeNone) {
266 + if ($includeNone) {
267 + $q->whereNotNull('source')->where('source', '!=', '');
268 + if ($sources) {
269 + $q->whereNotIn('source', $sources);
270 + }
271 + } else {
272 + $q->whereNotIn('source', $sources)->orWhereNull('source');
273 + }
274 + });
275 + }
276 +
277 + return $query->where(function ($q) use ($sources, $includeNone) {
278 + if ($sources) {
279 + $q->whereIn('source', $sources);
280 + }
281 + if ($includeNone) {
282 + $q->orWhereNull('source')->orWhere('source', '');
283 + }
284 + });
285 + }
286 +
287 + /**
187 288 * @param ModelQueryBuilder $query
188 289 * @param array $statuses
189 290 * @return ModelQueryBuilder
190 291 */
@@ -197,9 +298,35 @@
197 298 return $query;
198 299 }
199 300
200 301 /**
201 - * Local scope to filter tickets by not response by agent
302 + * Who replied last: 'agent', 'customer', or null when neither timestamp is
303 + * set. PHP counterpart of the timestamp check in scopeWaitingOnly(). A new
304 + * ticket resolves to 'customer' because boot() seeds last_customer_response.
305 + *
306 + * @return string|null
307 + */
308 + public function getLastReplyByAttribute()
309 + {
310 + $agentAt = $this->last_agent_response;
311 + $customerAt = $this->last_customer_response;
312 +
313 + if (!$agentAt && !$customerAt) {
314 + return null;
315 + }
316 + if (!$agentAt) {
317 + return 'customer';
318 + }
319 + if (!$customerAt) {
320 + return 'agent';
321 + }
322 +
323 + // A tie resolves to 'customer'. scopeWaitingOnly() uses a strict <, so SQL treats a tie as answered.
324 + return strtotime($customerAt) >= strtotime($agentAt) ? 'customer' : 'agent';
325 + }
326 +
327 + /**
328 + * Tickets awaiting an agent reply.
202 329 * @param $query
203 330 * @return mixed
204 331 */
205 332 public function scopeWaitingOnly($query)
@@ -212,10 +339,8 @@
212 339 return $query;
213 340 }
214 341
215 342 /**
216 - * scopeApplyFilters method will filet ticket based on the selected filters
217 - * This method will get filter option as parameter, loop through and apply conditions in query
218 343 * @param $query
219 344 * @param $filters
220 345 * @return ModelQueryBuilder
221 346 */
@@ -222,22 +347,18 @@
222 347 public function scopeApplyFilters($query, $filters)
223 348 {
224 349 $supportedColumns = ['product_id', 'client_priority', 'priority', 'mailbox_id'];
225 350 foreach ($filters as $filterKey => $filterValue) {
226 - if (!$filterValue && ($filterValue !== '0' || $filterValue !== 0)) {
351 + if (!$filterValue && ($filterValue !== '0' && $filterValue !== 0)) {
227 352 continue;
228 353 }
229 - //If filer using status
230 354 if ($filterKey == 'status_type') {
231 - //Get list of ticket status
232 355 $statusArray = Helper::getTkStatusesByGroupName($filterValue);
233 356 if ($statusArray) {
234 - //Apply filet where status in
235 357 $query->whereIn('status', $statusArray);
236 358 }
237 359 } else if (in_array($filterKey, $supportedColumns)) {
238 - // Use whereIn for product_id (always expects array) and where for other columns
239 - if ($filterKey === 'product_id') {
360 + if (is_array($filterValue)) {
240 361 $query->whereIn($filterKey, $filterValue);
241 362 } else {
242 363 $query->where($filterKey, $filterValue);
243 364 }
@@ -244,30 +365,73 @@
244 365 } else if ($filterKey == 'waiting_for_reply') {
245 366 if ($filterValue != 'yes') {
246 367 continue;
247 368 }
248 - //Apply filter where no response by agent
249 369 $query = $this->scopeWaitingOnly($query);
250 370 } else if ($filterKey == 'agent_id') {
251 - //Apply filter where ticket is not assigned
252 - if ($filterValue == 'unassigned') {
253 - $query->whereNull($filterKey);
371 + if (is_array($filterValue)) {
372 + $hasUnassigned = in_array('unassigned', $filterValue);
373 + $agentIds = array_filter($filterValue, function($v) {
374 + return $v !== 'unassigned';
375 + });
376 +
377 + if ($hasUnassigned && !empty($agentIds)) {
378 + $query->where(function($q) use ($agentIds) {
379 + $q->whereNull('agent_id')
380 + ->orWhereIn('agent_id', $agentIds);
381 + });
382 + } elseif ($hasUnassigned) {
383 + $query->whereNull('agent_id');
384 + } elseif (!empty($agentIds)) {
385 + if (defined('FLUENTSUPPORTPRO')) {
386 + if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
387 + $watcherTickets = [];
388 + foreach ($agentIds as $agentId) {
389 + $watcherTickets = array_merge($watcherTickets, TicketHelper::getWatcherTicketIds($agentId));
390 + }
391 + $query->whereIn('id', array_unique($watcherTickets));
392 + } else {
393 + $query->whereIn('agent_id', $agentIds);
394 + }
395 + } else {
396 + $query->whereIn('agent_id', $agentIds);
397 + }
398 + }
254 399 } else {
255 - if (defined('FLUENTSUPPORTPRO')) {
256 - if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
257 - $watcherTickets = TicketHelper::getWatcherTicketIds($filterValue);
258 - $query->whereIn('id', $watcherTickets);
400 + // Single value (legacy)
401 + if ($filterValue == 'unassigned') {
402 + $query->whereNull($filterKey);
403 + } else {
404 + if (defined('FLUENTSUPPORTPRO')) {
405 + if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
406 + $watcherTickets = TicketHelper::getWatcherTicketIds($filterValue);
407 + $query->whereIn('id', $watcherTickets);
408 + } else {
409 + $query->where($filterKey, $filterValue);
410 + }
259 411 } else {
260 - //Apply filter, get only assigned ticket
261 412 $query->where($filterKey, $filterValue);
262 413 }
263 414 }
264 415 }
416 + } else if ($filterKey == 'agent_group') {
417 + $groupIds = is_array($filterValue) ? $filterValue : [$filterValue];
418 + $groupIds = array_filter(array_map('intval', $groupIds));
419 + if (!empty($groupIds)) {
420 + $agentIds = TagPivot::where('source_type', 'agent_group')
421 + ->whereIn('tag_id', $groupIds)
422 + ->pluck('source_id')
423 + ->toArray();
424 + if ($agentIds) {
425 + $query->whereIn('agent_id', $agentIds);
426 + } else {
427 + $query->whereRaw('1 = 0');
428 + }
429 + }
265 430 } else if ($filterKey == 'ticket_tags') {
266 431 if (!$filterValue) {
267 432 continue;
268 433 }
269 - //Apply filter where ticket only has this tag id
270 434 $query->whereHas('tags', function ($q) use ($filterValue) {
271 435 $q->whereIn('tag_id', $filterValue);
272 436 });
273 437 }
@@ -276,9 +440,8 @@
276 440 return $query;
277 441 }
278 442
279 443 /**
280 - * Local scope to filter tickets by agent id
281 444 * @param ModelQueryBuilder $query
282 445 * @param int $agentId
283 446 * @return ModelQueryBuilder
284 447 */
@@ -291,9 +454,8 @@
291 454 return $query;
292 455 }
293 456
294 457 /**
295 - * Local scope to filter subscribers by search/query string
296 458 * @param ModelQueryBuilder $query
297 459 * @param int $customerId
298 460 * @return ModelQueryBuilder
299 461 */
@@ -304,9 +466,8 @@
304 466 return $query;
305 467 }
306 468
307 469 /**
308 - * Local scope to filter subscribers by search/query string
309 470 * @param ModelQueryBuilder $query
310 471 * @param int $productId
311 472 * @return ModelQueryBuilder
312 473 */
@@ -319,9 +480,8 @@
319 480 return $query;
320 481 }
321 482
322 483 /**
323 - * Local scope to filter subscribers by search/query string
324 484 * @param ModelQueryBuilder $query
325 485 * @param array $priorities
326 486 * @return ModelQueryBuilder
327 487 */
@@ -391,16 +551,16 @@
391 551 break;
392 552
393 553 case 'days_before':
394 554 $filter['operator'] = '<';
395 - $filter['value'] = date('Y-m-d', current_time('timestamp') - $filter['value'] * 24 * 60 * 60);
555 + $filter['value'] = gmdate('Y-m-d', time() - $filter['value'] * 24 * 60 * 60);
396 556 break;
397 557
398 558 case 'days_within':
399 559 $filter['operator'] = 'BETWEEN';
400 560 $filter['value'] = [
401 - date('Y-m-d', current_time('timestamp') - $filter['value'] * 24 * 60 * 60),
402 - date('Y-m-d') . ' 23:59:59'
561 + gmdate('Y-m-d', time() - $filter['value'] * 24 * 60 * 60),
562 + gmdate('Y-m-d') . ' 23:59:59'
403 563 ];
404 564 break;
405 565 case 'date_range':
406 566 $filter['operator'] = 'BETWEEN';
@@ -434,9 +594,8 @@
434 594 return $query;
435 595 }
436 596
437 597 /**
438 - * Relation builder
439 598 * @param $relation
440 599 * @param $query
441 600 * @param $method
442 601 * @param $subMethod
@@ -467,9 +626,8 @@
467 626 return $query;
468 627 }
469 628
470 629 /**
471 - * get tickets by advanced filter segment data
472 630 * @param $query
473 631 * @param $filter
474 632 * @return ModelQueryBuilder
475 633 */
@@ -499,9 +657,8 @@
499 657 return $query;
500 658 }
501 659
502 660 /**
503 - * method to search by properties
504 661 * @param $provider
505 662 * @param $query
506 663 * @param $search
507 664 * @param string $operator
@@ -523,9 +680,9 @@
523 680
524 681 $query->whereHas($provider, function ($query) use ($fields, $search, $operator) {
525 682 $query->where(array_shift($fields), $operator, $search);
526 683
527 - $nameArray = explode(' ', $search);
684 + $nameArray = explode(' ', (string) $search);
528 685
529 686 if (count($nameArray) >= 2) {
530 687 $query->orWhere(function ($q) use ($nameArray, $operator) {
531 688 $firstName = array_shift($nameArray);
@@ -544,9 +701,9 @@
544 701 return $query;
545 702 }
546 703
547 704 /**
548 - * Filter by ticket general properties like customer name, agent name etc
705 + * Filter by fields on the related customer or agent.
549 706 * @param $provider
550 707 * @param $query
551 708 * @param $filters
552 709 * @return ModelQueryBuilder
@@ -578,9 +735,10 @@
578 735 return $query;
579 736 }
580 737
581 738 /**
582 - * One2Many: Customer has to many Click Tickets
739 + * Newest first.
740 + *
583 741 * @return Model Collection
584 742 */
585 743 public function responses()
586 744 {
@@ -587,12 +745,43 @@
587 745 $class = __NAMESPACE__ . '\Conversation';
588 746
589 747 return $this->hasMany(
590 748 $class, 'ticket_id', 'id'
591 - )->with('person', 'attachments', 'ccinfo')
592 - ->latest('id');
749 + )->orderBy('created_at', 'desc')
750 + ->orderBy('id', 'desc');
593 751 }
594 752
753 + /** source value for a ticket an agent opened to reach out to the customer. */
754 + const SOURCE_AGENT_INITIATED = 'agent_initiated';
755 +
756 + /**
757 + * Whether an agent opened this ticket to reach out. The message is then the
758 + * agent's first response, and the ticket body is only a placeholder.
759 + *
760 + * @return bool
761 + */
762 + public function isAgentInitiated()
763 + {
764 + if ($this->source === self::SOURCE_AGENT_INITIATED) {
765 + return true;
766 + }
767 +
768 + // Before 2.4.1 only the "initialized this ticket" placeholder body marked it.
769 + return $this->created_by && self::hasLegacyAgentInitiatedMarker($this->content);
770 + }
771 +
772 + /**
773 + * @param string $content
774 + * @return bool
775 + */
776 + public static function hasLegacyAgentInitiatedMarker($content)
777 + {
778 + $content = (string) $content;
779 +
780 + return strpos($content, ' initialized this ticket') !== false
781 + || strpos($content, __(' initialized this ticket', 'fluent-support')) !== false;
782 + }
783 +
595 784 public function preview_response()
596 785 {
597 786 $class = __NAMESPACE__ . '\Conversation';
598 787
@@ -619,9 +808,8 @@
619 808 ->select(['tag_id']);
620 809 }
621 810
622 811 /**
623 - * One2one: Customer has to many Click Tickets
624 812 * @return Model Collection
625 813 */
626 814 public function customer()
627 815 {
@@ -632,9 +820,8 @@
632 820 );
633 821 }
634 822
635 823 /**
636 - * One2one: Customer has to many Click Tickets
637 824 * @return Model Collection
638 825 */
639 826 public function agent()
640 827 {
@@ -653,8 +840,17 @@
653 840 $class, 'closed_by', 'id'
654 841 );
655 842 }
656 843
844 + public function created_by_person()
845 + {
846 + $class = __NAMESPACE__ . '\Agent';
847 +
848 + return $this->belongsTo(
849 + $class, 'created_by', 'id'
850 + );
851 + }
852 +
657 853 public function product()
658 854 {
659 855 $class = __NAMESPACE__ . '\Product';
660 856
@@ -681,12 +877,79 @@
681 877 * @since v1.0.0
682 878 * @param object $ticket
683 879 */
684 880 do_action('fluent_support/deleting_ticket', $this);
685 - // Delete the ticket
686 881 $this->delete();
687 882 }
688 883
884 + public static function getNextSerialNumber()
885 + {
886 + $businessSettings = Helper::getOption('global_business_settings', []);
887 + $minNumber = (int) ($businessSettings['min_serial_number'] ?? 1);
888 + $minNumber = (int) apply_filters('fluent_support/min_serial_number', $minNumber);
889 +
890 + try {
891 + $lastTicketNumber = self::query()->max('serial_number');
892 + } catch (\Exception $e) {
893 + $lastTicketNumber = null;
894 + }
895 +
896 + $nextNumber = ((int) $lastTicketNumber) + 1;
897 +
898 + return max($nextNumber, $minNumber);
899 + }
900 +
901 + public static function isMinimumSerialNumberEnabled()
902 + {
903 + $businessSettings = Helper::getOption('global_business_settings', []);
904 + return ($businessSettings['enable_min_serial_number'] ?? 'no') === 'yes';
905 + }
906 +
907 + public static function getTicketPrefix($ticket = null)
908 + {
909 + $businessSettings = Helper::getOption('global_business_settings', []);
910 + $prefix = self::isMinimumSerialNumberEnabled() ? trim((string) ($businessSettings['ticket_prefix'] ?? '')) : '';
911 +
912 + $productId = $ticket ? $ticket->product_id : null;
913 +
914 + return apply_filters('fluent_support/ticket_prefix', $prefix, $ticket, $productId);
915 + }
916 +
917 + public function getDisplayTicketNumberAttribute()
918 + {
919 + return $this->ticket_number ?: ($this->serial_number ?: $this->id);
920 + }
921 +
922 + public function scopeWherePublicIdentifier($query, $identifier)
923 + {
924 + return $query->where('serial_number', $identifier);
925 + }
926 +
927 + protected function assignTicketNumber()
928 + {
929 + for ($attempt = 0; $attempt < 5; $attempt++) {
930 + $nextNumber = $this->serial_number ?: (self::isMinimumSerialNumberEnabled() ? self::getNextSerialNumber() : $this->id);
931 + $ticketNumber = $this->ticket_number ?: (self::getTicketPrefix($this) . $nextNumber);
932 +
933 + try {
934 + self::where('id', $this->id)->update([
935 + 'serial_number' => $nextNumber,
936 + 'ticket_number' => $ticketNumber
937 + ]);
938 + $this->serial_number = $nextNumber;
939 + $this->ticket_number = $ticketNumber;
940 +
941 + return $nextNumber;
942 + } catch (\Exception $e) {
943 + if (stripos($e->getMessage(), 'duplicate') === false) {
944 + throw $e;
945 + }
946 + }
947 + }
948 +
949 + throw new \RuntimeException('Could not allocate a unique ticket number.');
950 + }
951 +
689 952 public static function slugify($title)
690 953 {
691 954 $slug = sanitize_title($title, 'support-ticket-' . time(), 'display');
692 955 if (Ticket::where('slug', $slug)->first()) {
@@ -748,9 +1011,9 @@
748 1011
749 1012 if ($value) {
750 1013 if (in_array($fieldType, $customRenderers) && $rendered) {
751 1014 $value = apply_filters('fluent_support/custom_field_render_' . $fieldType, $value, $scope);
752 - } else if ($fieldType == 'checkbox') {
1015 + } else if (in_array($fieldType, ['checkbox', 'date-range'])) {
753 1016 $value = array_values(array_filter(explode('|', $value)));
754 1017 }
755 1018
756 1019 if (!is_array($value) && !is_object($value)) {
@@ -764,9 +1027,9 @@
764 1027 return $formattedData;
765 1028 }
766 1029
767 1030 /**
768 - * @param $data This is the data that will be saved to the ticket_meta for custom fields
1031 + * @param array $data Custom field values keyed by field key.
769 1032 * @return bool
770 1033 */
771 1034 public function syncCustomFields($data)
772 1035 {
@@ -841,11 +1104,10 @@
841 1104 ->first();
842 1105 }
843 1106
844 1107 /**
845 - * This method will assign tags to the ticket
846 - * @param $tagIds This is the array of tag ids that will be assigned to the ticket
847 - * @return \FluentSupport\App\Models\Ticket
1108 + * @param int|array $tagIds
1109 + * @return bool True if any tag was added.
848 1110 */
849 1111 public function applyTags($tagIds)
850 1112 {
851 1113 $result = false;
@@ -872,11 +1134,10 @@
872 1134 return $result;
873 1135 }
874 1136
875 1137 /**
876 - * This method will remove tags from ticket
877 - * @param $tagIds This is the array of tag ids that will be removed from the ticket
878 - * @return \FluentSupport\App\Models\Ticket
1138 + * @param int|array $tagIds
1139 + * @return bool True if any tag was removed.
879 1140 */
880 1141 public function detachTags($tagIds)
881 1142 {
882 1143 $result = false;
@@ -903,678 +1164,43 @@
903 1164 return $result;
904 1165 }
905 1166
906 1167 /**
907 - * This `createTicket` method will create a new ticket and it will also create a new customer or
908 - * a customer with WP profile if given.
909 - * @param array $ticketData
910 - * @param array $maybeNewCustomer
911 - * @return $this | \WP_Error
1168 + * @deprecated Use TicketService::storeTicket() instead.
912 1169 */
913 -
914 1170 public function createTicket($ticketData, $maybeNewCustomer = false)
915 1171 {
916 - $ticketData = $this->maybeCreateNewCustomer($ticketData, $maybeNewCustomer);
1172 + _deprecated_function(__METHOD__, '2.0.5', 'TicketService::storeTicket()');
917 1173
918 - if (!$ticketData) {
919 - return new \WP_Error('error', 'Ticket could not be created');
920 - }
921 -
922 - $customer = Customer::findOrFail($ticketData['customer_id']);
923 - $ticketData = $this->buildTicketData($ticketData, $customer);
924 - $disabledFields = apply_filters('fluent_support/disabled_ticket_fields', []);
925 -
926 - return $this->storeTicket($ticketData, $customer, $disabledFields);
927 - }
928 -
929 - // This is a supporting method for createTicket method
930 - // it will create ticket data array for ticket creation
931 - private function buildTicketData($ticketData, $customer)
932 - {
933 - if (empty($ticketData['mailbox_id'])) {
934 - $mailbox = Helper::getDefaultMailBox();
935 - $ticketData['mailbox_id'] = $mailbox->id;
936 - } else {
937 - $mailbox = MailBox::findOrFail($ticketData['mailbox_id']); // just for validation
938 - }
939 -
940 - if (!empty($ticketData['product_id'])) {
941 - $ticketData['product_source'] = 'local';
942 - }
943 -
944 - $ticketData['title'] = sanitize_text_field(wp_unslash($ticketData['title']));
945 -
946 - $ticketData['content'] = wp_specialchars_decode(wp_unslash(wp_kses_post($ticketData['content'])));
947 -
948 - if (!empty($ticketData['priority'])) {
949 - $ticketData['priority'] = sanitize_text_field($ticketData['priority']);
950 - }
951 -
952 - if (!empty($ticketData['client_priority'])) {
953 - $ticketData['client_priority'] = sanitize_text_field($ticketData['client_priority']);
954 - }
955 -
956 - /*
957 - * Filter ticket data
958 - *
959 - * @since v1.0.0
960 - * @param array $ticketData
961 - * @param object $customer
962 - */
963 - $ticketData = apply_filters('fluent_support/create_ticket_data', $ticketData, $customer);
964 -
965 - return $ticketData;
966 - }
967 -
968 - // This is a supporting method for createTicket method
969 - // it will store the ticket and return the ticket object
970 - private function storeTicket($ticketData, $customer, $disabledFields)
971 - {
972 - /*
973 - * Action before ticket create
974 - *
975 - * @since v1.0.0
976 - * @param array $ticketData
977 - * @param object $customer
978 - */
979 - do_action('fluent_support/before_ticket_create', $ticketData, $customer);
980 -
981 - $createdTicket = Ticket::create($ticketData);
982 -
983 - TicketService::addTicketAttachments($ticketData, $disabledFields, $createdTicket, $customer);
984 -
985 - if (defined('FLUENTSUPPORTPRO') && !empty($ticketData['custom_fields'])) {
986 - $createdTicket->syncCustomFields($ticketData['custom_fields']);
987 - }
988 -
989 - /*
990 - * Action on ticket create
991 - *
992 - * @since v1.0.0
993 - * @param object $createdTicket
994 - * @param object $customer
995 - */
996 - do_action('fluent_support/ticket_created', $createdTicket, $customer);
997 - do_action('fluent_support/ticket_created_behalf_of_customer', $createdTicket, $customer, Helper::getAgentByUserId());
998 - return $createdTicket;
999 - }
1000 -
1001 - // This is a supporting method for createTicket method
1002 - // it will create a new customer or a customer with WP profile if given
1003 - // and after creating a new user it will store customer_id
1004 - // inside $ticketData array as we only need customer_id to create ticket
1005 - private function maybeCreateNewCustomer($ticketData, $maybeNewCustomer)
1006 - {
1007 - if (!empty($ticketData['customer_id'])) {
1008 - return $ticketData;
1009 - }
1010 -
1011 - $createdUserId = false;
1012 -
1013 - if (Arr::get($ticketData, 'create_wp_user') == 'yes' && !empty($maybeNewCustomer['username'])) {
1014 - // Check if username already in use, if not create a wp new user
1015 - if (!username_exists($maybeNewCustomer['username'])) {
1016 - $authController = new AuthController();
1017 - $createdUserId = $authController->createUser($maybeNewCustomer);
1018 - $authController->maybeUpdateUser($createdUserId, $maybeNewCustomer);
1174 + if (empty($ticketData['customer_id']) && $maybeNewCustomer) {
1175 + $email = Arr::get($maybeNewCustomer, 'email');
1176 + if (!$email || !is_email($email)) {
1177 + return new \WP_Error('error', 'A valid email is required to create a ticket');
1019 1178 }
1020 - }
1021 1179
1022 - $email = Arr::get($maybeNewCustomer, 'email');
1023 - if (!$email || !is_email($email)) {
1024 - return false;
1025 - }
1026 -
1027 - $existingCustomer = Customer::where('email', $email)->first();
1028 -
1029 - if ($existingCustomer) {
1030 - $ticketData['customer_id'] = $existingCustomer->id;
1031 - return $ticketData;
1032 - }
1033 -
1034 - // create the customer now
1035 - $customerData = Arr::only($maybeNewCustomer, (new Customer())->getFillable());
1036 - $customerData['user_id'] = $createdUserId;
1037 -
1038 - $customerData = array_filter($customerData);
1039 -
1040 - $createCustomer = Customer::create($customerData);
1041 -
1042 - do_action('fluent_support/customer_created', $createCustomer);
1043 -
1044 - if (!$createCustomer) {
1045 - return false;
1046 - }
1047 -
1048 - $ticketData['customer_id'] = $createCustomer->id;
1049 - return $ticketData;
1050 - }
1051 -
1052 -
1053 - /**
1054 - * This `getTicket` will load a ticket with all its data
1055 - * @param array $ticketWith
1056 - * @param bool $withCrmData
1057 - * @param int $ticketId
1058 - * @return array
1059 - * @throws Exception
1060 - */
1061 - public function getTicket($ticketWith, $withCrmData, $ticketId)
1062 - {
1063 - $agent = Helper::getAgentByUserId();
1064 - $restrictedBusinessBoxes = PermissionManager::currentUserRestrictedBusinessBoxes();
1065 -
1066 - $ticket = self::with($ticketWith)->findOrFail($ticketId);
1067 -
1068 - if (in_array($ticket->mailbox_id, $restrictedBusinessBoxes)) {
1069 - throw new \Exception('Ticket cannot be fetched due to restricted mailbox');
1070 - }
1071 -
1072 - $customFieldsKey = apply_filters('fluent_support/custom_registration_form_fields_key', Helper::getBusinessSettings('custom_registration_form_field'));
1073 - $ticket->customer->custom_field_keys = $customFieldsKey;
1074 -
1075 - if ($ticket->customer->user_id) {
1076 - $customFieldKeysUsingHook = apply_filters('fluent_support/custom_registration_form_fields_key', []);
1077 -
1078 - foreach ($customFieldKeysUsingHook as $key) {
1079 - $userMeta = get_user_meta($ticket->customer->user_id,$key, true);
1080 - if($userMeta) {
1081 - $ticket->customer->$key = $userMeta;
1082 - }
1083 - }
1084 - }
1085 -
1086 - if (defined('FLUENTSUPPORTPRO_PLUGIN_VERSION') && Helper::isAgentFeedbackEnabled()) {
1087 - foreach ($ticket->responses as $response) {
1088 - $agentFeedback = Meta::where('object_id', $response->id)
1089 - ->where('object_type', 'conversation_meta')
1090 - ->where('key', 'agent_feedback_ratings')
1091 - ->first();
1092 -
1093 - if ($agentFeedback) {
1094 - $response->agent_feedback = $agentFeedback->value;
1095 - }
1096 - }
1097 - }
1098 -
1099 - $ticket->customer->profile_edit_url = $this->getCustomerProfileUrl($ticket->customer); //Get and set customer profile url
1100 - $this->checkAgentPermission($ticket); // Check Agent Permission
1101 - $this->checkIfClosedTicket($ticket); // Check if ticket is closed, if closed then load ticket with closed data
1102 -
1103 - return $this->getTicketAdditionalData($agent, $ticket->responses, $ticket, $withCrmData);
1104 - }
1105 -
1106 - // This checkIfClosedTicket method will validate if ticket is closed, if closed then load ticket with closed data
1107 - private function checkIfClosedTicket($ticket)
1108 - {
1109 - if ($ticket->status == 'closed') {
1110 - $ticket->load('closed_by_person');
1111 - }
1112 - }
1113 -
1114 - // This getCustomerProfileUrl method will return customer profile url
1115 - private function getCustomerProfileUrl($customer)
1116 - {
1117 - return $customer->getUserProfileEditUrl();
1118 - }
1119 -
1120 - // This getTicketAdditionalData method will load ticket with additional data
1121 - private function getTicketAdditionalData($agent, $responses, $ticket, $isCrmProfileRequested = false)
1122 - {
1123 - foreach ($responses as $response) {
1124 - $response->content = links_add_target(make_clickable(wpautop($response->content, false)));
1125 - if (!empty($response->ccinfo)) {
1126 - $val = Helper::safeUnserialize($response->ccinfo->value);
1127 - if(isset($val['cc_email']) && !empty($val['cc_email'])){
1128 - $response->cc_info = $val['cc_email'];
1129 - } else {
1130 - $response->cc_info = '';
1131 - }
1180 + $existingCustomer = Customer::where('email', $email)->first();
1181 + if ($existingCustomer) {
1182 + $ticketData['customer_id'] = $existingCustomer->id;
1132 1183 } else {
1133 - $response->cc_info = '';
1134 - }
1135 - }
1136 -
1137 - $ticket->content = links_add_target(make_clickable(wpautop($ticket->content, false)));
1138 -
1139 - //Get last activity by agent
1140 - $ticket->live_activity = TicketHelper::getActivity($ticket->id, $agent->id);
1141 - //Get all carbon copy customer
1142 - $ccInfo = $ticket->getSettingsValue('cc_email', []);
1143 -
1144 - $ticket->carbon_copy = !empty($ccInfo) ? implode(', ', $ccInfo) : '';
1145 -
1146 - if (defined('FLUENTSUPPORTPRO')) {
1147 - $ticket->custom_fields = $ticket->customData('admin', true);
1148 - }
1149 -
1150 - $data = [
1151 - 'ticket' => $ticket,
1152 - 'responses' => $responses,
1153 - 'agent_id' => $agent->id
1154 - ];
1155 -
1156 - if (defined('FLUENTSUPPORTPRO') && $ticket->watchers) {
1157 - $data['watchers'] = TicketHelper::getWatchers($ticket->watchers);
1158 - }
1159 -
1160 - if (defined('FLUENTCRM') && $isCrmProfileRequested) {
1161 - $data['fluentcrm_profile'] = Helper::getFluentCrmContactData($ticket->customer);
1162 - }
1163 -
1164 - return $data;
1165 -
1166 - }
1167 -
1168 -
1169 - /**
1170 - * This `createResponse` will create a response for a ticket
1171 - * @param array $data
1172 - * @param int $ticketId
1173 - * @return array
1174 - * @throws Exception
1175 - */
1176 - public function createResponse($data, $ticketId)
1177 - {
1178 - $agent = Helper::getAgentByUserId(get_current_user_id());
1179 - $this->checkIfValidAgent($agent);
1180 -
1181 - $ticket = static::findOrFail($ticketId);
1182 - $this->checkAgentPermission($ticket);
1183 -
1184 - $responseData = (new ResponseService())->createResponse($data, $agent, $ticket);
1185 -
1186 - $responseData['response']->content = wp_specialchars_decode(wpautop($responseData['response']->content, false));
1187 -
1188 - return [
1189 - 'message' => __('Response has been added', 'fluent-support'),
1190 - 'response' => $responseData['response'],
1191 - 'ticket' => $responseData['ticket'],
1192 - 'update_data' => $responseData['update_data']
1193 - ];
1194 - }
1195 -
1196 - public function addOrUpdatDraft($data, $ticketId)
1197 - {
1198 - $agent = Helper::getAgentByUserId(get_current_user_id());
1199 - $this->checkIfValidAgent($agent);
1200 -
1201 - $ticket = static::findOrFail($ticketId);
1202 - $this->checkAgentPermission($ticket);
1203 - $key = 'ticket_no_' . $ticketId . '_agent_id_' . $agent->id . '_response_draft';
1204 -
1205 - $previousDraft = Meta::where('key', $key)->first();
1206 -
1207 - if ($data['draftID'] || $previousDraft) {
1208 - return $this->updateDraft($key, $data['draftID'], $data);
1209 - }
1210 -
1211 - $draftID = Meta::insertGetId([
1212 - 'object_type' => '_fs_auto_draft',
1213 - 'object_id' => $ticketId,
1214 - 'key' => $key,
1215 - 'value' => maybe_serialize($data)
1216 - ]);
1217 -
1218 - return [
1219 - 'message' => __('Draft has been added', 'fluent-support'),
1220 - 'draftID' => $draftID
1221 - ];
1222 -
1223 - }
1224 -
1225 - public function updateDraft($key, $draftID, $data)
1226 - {
1227 - Meta::where('key', $key)->update([
1228 - 'value' => maybe_serialize($data)
1229 - ]);
1230 - return [
1231 - 'message' => __('Draft has been updated', 'fluent-support'),
1232 - 'draftID' => $draftID
1233 - ];
1234 - }
1235 -
1236 - public function fetchDraft($ticketId)
1237 - {
1238 - $agent = Helper::getAgentByUserId(get_current_user_id());
1239 - $this->checkIfValidAgent($agent);
1240 -
1241 - $ticket = static::findOrFail($ticketId);
1242 - $this->checkAgentPermission($ticket);
1243 - $key = 'ticket_no_' . $ticketId . '_agent_id_' . $agent->id . '_response_draft';
1244 -
1245 - $draft = Meta::where([
1246 - 'object_type' => '_fs_auto_draft',
1247 - 'key' => $key,
1248 - ])->first();
1249 -
1250 - if ($draft) {
1251 - $draft->value = Helper::safeUnserialize($draft->value);
1252 - }
1253 -
1254 - return [
1255 - 'draft' => $draft
1256 - ];
1257 - }
1258 -
1259 - public function removeDraft($draftID)
1260 - {
1261 - Meta::where('id', $draftID)->delete();
1262 -
1263 - return [
1264 - 'message' => __('Discard draft successfully', 'fluent-support'),
1265 - ];
1266 - }
1267 -
1268 - // This checkIfValidAgent method will check if agent is valid or not
1269 - private function checkIfValidAgent($agent)
1270 - {
1271 - if (!$agent) {
1272 - throw new \Exception('Sorry, You do not have permission. Please add yourself as support agent first');
1273 - } else {
1274 - return true;
1275 - }
1276 - }
1277 -
1278 - /**
1279 - * This `closeTicket` will close a ticket by ticket id
1280 - * @param int $ticketId
1281 - * @return array
1282 - * @throws Exception
1283 - */
1284 - public function closeTicket($ticketId, $closeSilently = false)
1285 - {
1286 - $agent = Helper::getAgentByUserId(get_current_user_id());
1287 -
1288 - $ticket = static::findOrFail($ticketId);
1289 - $this->checkAgentPermission($ticket);
1290 -
1291 - return [
1292 - 'message' => __('Ticket has been closed', 'fluent-support'),
1293 - 'ticket' => (new TicketService())->close($ticket, $agent, '', $closeSilently)
1294 - ];
1295 - }
1296 -
1297 - /**
1298 - * This `reopenTicket` will reopen a ticket by ticket id
1299 - * @param int $ticketId
1300 - * @return array
1301 - * @throws Exception
1302 - */
1303 - public function reOpenTicket($ticketId)
1304 - {
1305 - $agent = Helper::getAgentByUserId(get_current_user_id());
1306 -
1307 - $ticket = static::findOrFail($ticketId);
1308 - $this->checkAgentPermission($ticket);
1309 -
1310 -
1311 - return [
1312 - 'message' => __('Ticket has been opened again', 'fluent-support'),
1313 - 'ticket' => (new TicketService())->reopen($ticket, $agent)
1314 - ];
1315 - }
1316 -
1317 -
1318 - /**
1319 - * This `getTicketWidgets` will load all ticket widgets
1320 - * @param int $ticketId
1321 - * @return array
1322 - * @throws Exception
1323 - */
1324 - public function getTicketWidgets($ticketId)
1325 - {
1326 - $ticket = static::with('customer')->findOrFail($ticketId);
1327 - $this->checkAgentPermission($ticket);
1328 -
1329 - //Get last 10 tickets of this customer except this
1330 - /*
1331 - * Filter ticket limit to show ticket in view ticket page sidebar
1332 - * @since 1.5.6
1333 - * @param int $limit
1334 - */
1335 - $limit = apply_filters('fluent_support/previous_ticket_widgets_limit', 10);
1336 -
1337 - $otherTickets = static::where('id', '!=', $ticketId)
1338 - ->select(['id', 'title', 'status', 'created_at'])
1339 - ->where('customer_id', $ticket->customer_id)
1340 - ->latest('id')
1341 - ->limit($limit)
1342 - ->get();
1343 -
1344 - return [
1345 - 'other_tickets' => $otherTickets,
1346 - 'extra_widgets' => ProfileInfoService::getProfileExtraWidgets($ticket->customer)
1347 - ];
1348 - }
1349 -
1350 - /**
1351 - * This `updateTicketProperty` will update tickets properties
1352 - * @param string $propName
1353 - * @param mixed $propValue
1354 - * @param int $ticketId
1355 - * @return array
1356 - * @throws Exception
1357 - */
1358 - public function updateTicketProperty($propName, $propValue, $ticketId)
1359 - {
1360 - $assigner = Helper::getAgentByUserId(get_current_user_id());
1361 - $ticket = static::findOrFail($ticketId);
1362 -
1363 - $this->checkAgentPermission($ticket);
1364 - $propName = sanitize_text_field($propName);
1365 - $message = sprintf(
1366 - /* translators: %s: The name of the property that was updated */
1367 - __('%s has been updated', 'fluent-support'),
1368 - esc_html(str_replace('_', ' ', ucwords($propName)))
1369 - );
1370 - return [
1371 - 'message' => $message,
1372 - 'update_data' => $this->handlePropertyUpdate($propName, $propValue, $ticket, $assigner)
1373 - ];
1374 - }
1375 -
1376 - // This will handle ticket property update
1377 - private function handlePropertyUpdate($propName, $propValue, $ticket, $assigner)
1378 - {
1379 - $prevValue = $ticket->{$propName};
1380 -
1381 - if ($propName === 'agent_id') {
1382 - if (!PermissionManager::currentUserCan('fst_assign_agents')) {
1383 - throw new \Exception('Permission denied to assign agent', 403);
1384 - }
1385 -
1386 - $agent = Agent::findOrFail($propValue);
1387 - $restrictions = $agent->getMeta('agent_restrictions', []);
1388 -
1389 - if (!empty($restrictions['restrictedBusinessBoxes'])) {
1390 - $mailboxId = (int)$ticket->mailbox_id;
1391 -
1392 - if (in_array($mailboxId, $restrictions['restrictedBusinessBoxes'], true)) {
1393 - throw new \Exception('Agent is restricted for this mailbox ticket', 403);
1184 + $customerData = Arr::only($maybeNewCustomer, (new Customer())->getFillable());
1185 + $customerData = array_filter($customerData);
1186 + $createCustomer = Customer::create($customerData);
1187 + if (!$createCustomer) {
1188 + return new \WP_Error('error', 'Customer could not be created');
1394 1189 }
1190 + $ticketData['customer_id'] = $createCustomer->id;
1395 1191 }
1396 1192 }
1397 1193
1398 - if ($propName && $propValue && $prevValue != $propValue) {
1399 - $ticket->{$propName} = $propValue;
1400 - $ticket->save();
1194 + if (empty($ticketData['customer_id'])) {
1195 + return new \WP_Error('error', 'Ticket could not be created');
1401 1196 }
1402 1197
1403 - $updateData = [];
1198 + $customer = Customer::findOrFail($ticketData['customer_id']);
1404 1199
1405 - if ($propName == 'product_id') {
1406 - $ticket->load('product');
1407 - $updateData['product'] = $ticket->product;
1408 - } else if ($propName == 'agent_id') {
1409 - $ticket->load('agent');
1410 - $updateData['agent'] = $ticket->agent;
1411 - $updateData['assigner'] = (new TicketService())->onAgentChange($ticket, $assigner);
1412 - if ($prevValue != $ticket->{$propName}) {
1413 - do_action('fluent_support/agent_assigned_to_ticket', $ticket->agent, $ticket, $assigner);
1414 - }
1415 - }
1416 -
1417 - return $updateData;
1200 + return (new TicketService())->storeTicket($ticketData, $customer);
1418 1201 }
1419 1202
1420 - /**
1421 - * This `handleBulkActions` will handle bulk actions
1422 - * @param string $action
1423 - * @param array $ticketIds
1424 - * @return array
1425 - * @throws Exception
1426 - */
1427 - public function handleBulkActions($action, $ticketIds)
1428 - {
1429 - $hasAllPermission = PermissionManager::currentUserCan('fst_manage_other_tickets');
1430 - $agent = Helper::getAgentByUserId();
1431 - $query = Ticket::whereIn('id', $ticketIds);
1432 -
1433 - if (!$hasAllPermission) {
1434 - //Filter ticket by agent_id
1435 - $query->where('agent_id', $agent->id);
1436 - }
1437 -
1438 - return $this->handleAction($action, $query, $agent);
1439 - }
1440 -
1441 - // This will handle ticket bulk action
1442 - private function handleAction($action, $query, $agent)
1443 - {
1444 - if ($action == 'close_tickets') {
1445 - return $this->bulkCloseTickets($query->get(), $agent);
1446 - } else if ($action == 'delete_tickets') {
1447 - return (new TicketService)->deleteTickets($query->get());
1448 - } else if ($action == 'assign_agent') {
1449 - return $this->bulkAssignAgent($query);
1450 - } else if ($action == 'assign_tags') {
1451 - return $this->bulkAssignTag($query->get());
1452 - } else {
1453 - throw new \Exception('Sorry no action found as available');
1454 - }
1455 - }
1456 -
1457 - /**
1458 - * This `bulkCloseTickets` will close all given or selected tickets
1459 - * @param object $tickets
1460 - * @param object $agent
1461 - * @return array
1462 - */
1463 - public function bulkCloseTickets($tickets, $agent)
1464 - {
1465 - $tickets->each(function ($ticket) use ($agent) {
1466 - (new TicketService())->close($ticket, $agent);
1467 - });
1468 -
1469 - // translators: %d represents the number of tickets that have been closed.
1470 - return [
1471 - 'message' => sprintf(
1472 - /* translators: %d represents the number of closed tickets. */
1473 - __('%d tickets have been closed.', 'fluent-support'),
1474 - count($tickets)
1475 - )
1476 - ];
1477 - }
1478 -
1479 - /**
1480 - * This `bulkAssignAgent` will assign all given or selected tickets to given agent
1481 - * @param object $tickets
1482 - * @return array
1483 - */
1484 - public function bulkAssignAgent($query)
1485 - {
1486 - $request = \FluentSupport\App\App::getInstance('request');
1487 -
1488 - if (!$request->has('agent_id')) {
1489 - throw new \Exception('agent_id param is required');
1490 - }
1491 -
1492 - $agent = Agent::findOrFail($request->get('agent_id'));
1493 -
1494 - $query->where(function ($q) use ($agent) {
1495 - $q->where('agent_id', '!=', $agent->id)
1496 - ->orWhereNull('agent_id');
1497 - });
1498 -
1499 - $tickets = $query->get();
1500 -
1501 - $assignedCount = 0;
1502 - $skippedCount = 0;
1503 -
1504 - $tickets->each(function ($ticket) use ($agent, &$assignedCount, &$skippedCount) {
1505 - $assigner = Helper::getCurrentAgent();
1506 - $restrictions = $agent->getMeta('agent_restrictions', []);
1507 -
1508 - // Skip ticket if mailbox is restricted for the agent
1509 - if (!empty($restrictions) && in_array($ticket->mailbox_id, $restrictions['restrictedBusinessBoxes'])) {
1510 - $skippedCount++;
1511 - return;
1512 - }
1513 -
1514 - $ticket->agent_id = $agent->id;
1515 - $ticket->save();
1516 - $assignedCount++;
1517 -
1518 - do_action('fluent_support/agent_assigned_to_ticket', $agent, $ticket, $assigner);
1519 - });
1520 -
1521 - // translators: %1$d is the number of assigned tickets, %2$s is the agent's full name.
1522 - $assignedMessage = sprintf(
1523 - /* translators: %1$d is the number of tickets assigned, %2$s is the agent's name. */
1524 - __('%1$d tickets have been assigned to %2$s.', 'fluent-support'),
1525 - $assignedCount,
1526 - $agent->full_name
1527 - );
1528 -
1529 - // translators: %1$d is the number of skipped tickets.
1530 - $skippedMessage = $skippedCount > 0
1531 - ? sprintf(
1532 - /* translators: %1$d is the number of skipped tickets due to mailbox restrictions. */
1533 - __('%1$d tickets were skipped due to mailbox restrictions or already being assigned.', 'fluent-support'),
1534 - $skippedCount
1535 - )
1536 - : "";
1537 -
1538 - return [
1539 - 'message' => trim($assignedMessage . ' ' . $skippedMessage)
1540 - ];
1541 - }
1542 -
1543 -
1544 -
1545 - /**
1546 - * This `bulkAssignTag` will assign all given or selected tickets to given tag
1547 - * @param object $tickets
1548 - * @return array
1549 - */
1550 - public function bulkAssignTag($query)
1551 - {
1552 - $request = \FluentSupport\App\App::getInstance('request');
1553 -
1554 - if (!$request->has('tag_ids')) {
1555 - throw new \Exception('tag_ids param is required');
1556 - }
1557 -
1558 - $tags = array_filter(array_map('absint', $request->get('tag_ids', [])));
1559 -
1560 - $query->each(function ($ticket) use ($tags) {
1561 - $ticket->applyTags($tags);
1562 - });
1563 -
1564 - return [
1565 - 'message' => __('Selected tags has been added to tickets', 'fluent-support')
1566 - ];
1567 - }
1568 -
1569 - // This checkAgentPermission method will validate if agent has permission to view ticket
1570 - private function checkAgentPermission($ticket)
1571 - {
1572 - if (!PermissionManager::hasTicketPermission($ticket)) {
1573 - throw new \Exception('Sorry, You do not have permission to this ticket');
1574 - }
1575 - }
1576 -
1577 1203 public static function countTicketByMailBoxId($mailbox_id)
1578 1204 {
1579 1205 return self::where('mailbox_id', $mailbox_id)->count();
1580 1206 }
@@ -1660,5 +1286,4 @@
1660 1286
1661 1287 }
1662 1288
1663 1289 }
1664 -