PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.0
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.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
← All changes | app/Models/Ticket.php +247 -674 1.10.22.4.0 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,19 @@
19 16
20 17 protected $dates = ['waiting_since'];
21 18
22 19 /**
20 + * The ticket hash is a bearer credential for the signed public ticket view,
21 + * so it must never be serialized into an API response. PHP property access
22 + * is unaffected, which is what Helper::getTicketViewSignedUrl() relies on.
23 + *
24 + * @var array
25 + */
26 + protected $hidden = ['hash', 'content_hash'];
27 +
28 + protected $appends = ['display_ticket_number'];
29 +
30 + /**
23 31 * The attributes that are mass assignable.
24 32 *
25 33 * @var array
26 34 */
@@ -46,9 +54,12 @@
46 54 'response_count',
47 55 'first_response_time',
48 56 'total_close_time',
49 57 'resolved_at',
50 - 'closed_by'
58 + 'closed_by',
59 + 'created_by',
60 + 'serial_number',
61 + 'ticket_number'
51 62 ];
52 63
53 64 public static function boot()
54 65 {
@@ -58,9 +69,9 @@
58 69 if (empty($model->slug)) {
59 70 $model->slug = static::slugify($model->title);
60 71 }
61 72
62 - $model->hash = substr(md5(time() . wp_generate_uuid4()), 0, 8) . wp_rand(1, 99);
73 + $model->hash = bin2hex(random_bytes(16));
63 74 $model->content_hash = md5($model->content);
64 75
65 76 $model->last_customer_response = current_time('mysql');
66 77 $model->created_at = current_time('mysql');
@@ -68,8 +79,23 @@
68 79 $model->waiting_since = current_time('mysql');
69 80
70 81 });
71 82
83 + static::updating(function ($model) {
84 + // A hash handed out for one customer must stop working the moment
85 + // the ticket belongs to somebody else, otherwise every link already
86 + // emailed for it keeps authorizing read, reply, close and reopen.
87 + if ($model->isDirty('customer_id')) {
88 + $model->hash = bin2hex(random_bytes(16));
89 + }
90 + });
91 +
92 + static::created(function ($model) {
93 + if (empty($model->serial_number) || empty($model->ticket_number)) {
94 + $model->assignTicketNumber();
95 + }
96 + });
97 +
72 98 static::deleting(function ($model) {
73 99 //Delete the ticket meta
74 100 Meta::where('object_type', 'ticket_meta')->where('object_id', $model->id)->delete();
75 101 //Delete all cc info for the ticket
@@ -75,10 +101,17 @@
75 101 //Delete all cc info for the ticket
76 102 Meta::where('object_type', 'ticket')->where('object_id', $model->id)->delete();
77 103 //Delete draft info
78 104 Meta::where('object_type', '_fs_auto_draft')->where('object_id', $model->id)->delete();
79 - //delete the responses first
105 + //Delete internal notifications and notification recipient rows for the ticket
106 + Notification::deleteByTicketId($model->id);
107 + //delete the responses first (their attachments are cleaned up by Conversation::deleting)
80 108 Conversation::deleteAll($model->id);
109 + // Delete ticket-level attachments (conversation_id IS NULL) and remove the ticket upload directory
110 + $class = __NAMESPACE__ . '\Attachment';
111 + $ticketAttachments = $class::where('ticket_id', $model->id)->whereNull('conversation_id')->get();
112 + $class::purgeAttachments($ticketAttachments, $model->id);
113 + $class::where('ticket_id', $model->id)->whereNull('conversation_id')->delete();
81 114 });
82 115 }
83 116
84 117 /**
@@ -88,9 +121,11 @@
88 121 protected $searchable = [
89 122 'content',
90 123 'title',
91 124 'slug',
92 - 'id'
125 + 'id',
126 + 'serial_number',
127 + 'ticket_number'
93 128 ];
94 129
95 130 /**
96 131 * Local scope to filter tickets by search/query string
@@ -99,10 +134,15 @@
99 134 * @return ModelQueryBuilder
100 135 */
101 136 public function scopeSearchBy($query, $search)
102 137 {
138 +
139 + if(!$search) {
140 + return $query;
141 + }
142 +
103 143 if (strpos($search, ':')) {
104 - $array = explode(':', $search);
144 + $array = explode(':', (string) $search);
105 145 $column = $array[0];
106 146 $value = $array[1];
107 147 $columns = $this->fillable;
108 148 $columns[] = 'id';
@@ -197,8 +237,41 @@
197 237 return $query;
198 238 }
199 239
200 240 /**
241 + * Who replied last on this ticket, derived from the already-loaded
242 + * last_agent_response / last_customer_response timestamp columns.
243 + *
244 + * Returns 'agent', 'customer', or null. This is the per-ticket value behind
245 + * the `waiting_for_reply` filter and mirrors the timestamp comparison in
246 + * scopeWaitingOnly() (which lives in SQL, so it can't share this PHP code).
247 + *
248 + * Note: boot() seeds last_customer_response on creation, so a brand-new
249 + * ticket with no agent reply correctly resolves to 'customer' (awaiting an
250 + * agent). null is reserved for the rare case where neither timestamp is set.
251 + *
252 + * @return string|null
253 + */
254 + public function getLastReplyByAttribute()
255 + {
256 + $agentAt = $this->last_agent_response;
257 + $customerAt = $this->last_customer_response;
258 +
259 + if (!$agentAt && !$customerAt) {
260 + return null;
261 + }
262 + if (!$agentAt) {
263 + return 'customer';
264 + }
265 + if (!$customerAt) {
266 + return 'agent';
267 + }
268 +
269 + // Tie (same second) resolves to 'customer' — the waiting bias used by scopeWaitingOnly.
270 + return strtotime($customerAt) >= strtotime($agentAt) ? 'customer' : 'agent';
271 + }
272 +
273 + /**
201 274 * Local scope to filter tickets by not response by agent
202 275 * @param $query
203 276 * @return mixed
204 277 */
@@ -222,9 +295,9 @@
222 295 public function scopeApplyFilters($query, $filters)
223 296 {
224 297 $supportedColumns = ['product_id', 'client_priority', 'priority', 'mailbox_id'];
225 298 foreach ($filters as $filterKey => $filterValue) {
226 - if (!$filterValue && ($filterValue !== '0' || $filterValue !== 0)) {
299 + if (!$filterValue && ($filterValue !== '0' && $filterValue !== 0)) {
227 300 continue;
228 301 }
229 302 //If filer using status
230 303 if ($filterKey == 'status_type') {
@@ -234,10 +307,10 @@
234 307 //Apply filet where status in
235 308 $query->whereIn('status', $statusArray);
236 309 }
237 310 } 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') {
311 + // Use whereIn for all supported columns (they all now support multi-select)
312 + if (is_array($filterValue)) {
240 313 $query->whereIn($filterKey, $filterValue);
241 314 } else {
242 315 $query->where($filterKey, $filterValue);
243 316 }
@@ -247,22 +320,73 @@
247 320 }
248 321 //Apply filter where no response by agent
249 322 $query = $this->scopeWaitingOnly($query);
250 323 } else if ($filterKey == 'agent_id') {
251 - //Apply filter where ticket is not assigned
252 - if ($filterValue == 'unassigned') {
253 - $query->whereNull($filterKey);
324 + // Handle array of agent IDs for multi-select
325 + if (is_array($filterValue)) {
326 + // Check if 'unassigned' is in the array
327 + $hasUnassigned = in_array('unassigned', $filterValue);
328 + $agentIds = array_filter($filterValue, function($v) {
329 + return $v !== 'unassigned';
330 + });
331 +
332 + if ($hasUnassigned && !empty($agentIds)) {
333 + // Include both unassigned and specific agents
334 + $query->where(function($q) use ($agentIds) {
335 + $q->whereNull('agent_id')
336 + ->orWhereIn('agent_id', $agentIds);
337 + });
338 + } elseif ($hasUnassigned) {
339 + // Only unassigned
340 + $query->whereNull('agent_id');
341 + } elseif (!empty($agentIds)) {
342 + // Only specific agents
343 + if (defined('FLUENTSUPPORTPRO')) {
344 + if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
345 + $watcherTickets = [];
346 + foreach ($agentIds as $agentId) {
347 + $watcherTickets = array_merge($watcherTickets, TicketHelper::getWatcherTicketIds($agentId));
348 + }
349 + $query->whereIn('id', array_unique($watcherTickets));
350 + } else {
351 + $query->whereIn('agent_id', $agentIds);
352 + }
353 + } else {
354 + $query->whereIn('agent_id', $agentIds);
355 + }
356 + }
254 357 } else {
255 - if (defined('FLUENTSUPPORTPRO')) {
256 - if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
257 - $watcherTickets = TicketHelper::getWatcherTicketIds($filterValue);
258 - $query->whereIn('id', $watcherTickets);
358 + // Single value (backward compatibility)
359 + if ($filterValue == 'unassigned') {
360 + $query->whereNull($filterKey);
361 + } else {
362 + if (defined('FLUENTSUPPORTPRO')) {
363 + if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
364 + $watcherTickets = TicketHelper::getWatcherTicketIds($filterValue);
365 + $query->whereIn('id', $watcherTickets);
366 + } else {
367 + //Apply filter, get only assigned ticket
368 + $query->where($filterKey, $filterValue);
369 + }
259 370 } else {
260 - //Apply filter, get only assigned ticket
261 371 $query->where($filterKey, $filterValue);
262 372 }
263 373 }
264 374 }
375 + } else if ($filterKey == 'agent_group') {
376 + $groupIds = is_array($filterValue) ? $filterValue : [$filterValue];
377 + $groupIds = array_filter(array_map('intval', $groupIds));
378 + if (!empty($groupIds)) {
379 + $agentIds = TagPivot::where('source_type', 'agent_group')
380 + ->whereIn('tag_id', $groupIds)
381 + ->pluck('source_id')
382 + ->toArray();
383 + if ($agentIds) {
384 + $query->whereIn('agent_id', $agentIds);
385 + } else {
386 + $query->whereRaw('1 = 0');
387 + }
388 + }
265 389 } else if ($filterKey == 'ticket_tags') {
266 390 if (!$filterValue) {
267 391 continue;
268 392 }
@@ -391,16 +515,16 @@
391 515 break;
392 516
393 517 case 'days_before':
394 518 $filter['operator'] = '<';
395 - $filter['value'] = date('Y-m-d', current_time('timestamp') - $filter['value'] * 24 * 60 * 60);
519 + $filter['value'] = gmdate('Y-m-d', time() - $filter['value'] * 24 * 60 * 60);
396 520 break;
397 521
398 522 case 'days_within':
399 523 $filter['operator'] = 'BETWEEN';
400 524 $filter['value'] = [
401 - date('Y-m-d', current_time('timestamp') - $filter['value'] * 24 * 60 * 60),
402 - date('Y-m-d') . ' 23:59:59'
525 + gmdate('Y-m-d', time() - $filter['value'] * 24 * 60 * 60),
526 + gmdate('Y-m-d') . ' 23:59:59'
403 527 ];
404 528 break;
405 529 case 'date_range':
406 530 $filter['operator'] = 'BETWEEN';
@@ -523,9 +647,9 @@
523 647
524 648 $query->whereHas($provider, function ($query) use ($fields, $search, $operator) {
525 649 $query->where(array_shift($fields), $operator, $search);
526 650
527 - $nameArray = explode(' ', $search);
651 + $nameArray = explode(' ', (string) $search);
528 652
529 653 if (count($nameArray) >= 2) {
530 654 $query->orWhere(function ($q) use ($nameArray, $operator) {
531 655 $firstName = array_shift($nameArray);
@@ -587,10 +711,10 @@
587 711 $class = __NAMESPACE__ . '\Conversation';
588 712
589 713 return $this->hasMany(
590 714 $class, 'ticket_id', 'id'
591 - )->with('person', 'attachments', 'ccinfo')
592 - ->latest('id');
715 + )->orderBy('created_at', 'desc')
716 + ->orderBy('id', 'desc');
593 717 }
594 718
595 719 public function preview_response()
596 720 {
@@ -653,8 +777,17 @@
653 777 $class, 'closed_by', 'id'
654 778 );
655 779 }
656 780
781 + public function created_by_person()
782 + {
783 + $class = __NAMESPACE__ . '\Agent';
784 +
785 + return $this->belongsTo(
786 + $class, 'created_by', 'id'
787 + );
788 + }
789 +
657 790 public function product()
658 791 {
659 792 $class = __NAMESPACE__ . '\Product';
660 793
@@ -685,8 +818,76 @@
685 818 // Delete the ticket
686 819 $this->delete();
687 820 }
688 821
822 + public static function getNextSerialNumber()
823 + {
824 + $businessSettings = Helper::getOption('global_business_settings', []);
825 + $minNumber = (int) ($businessSettings['min_serial_number'] ?? 1);
826 + $minNumber = (int) apply_filters('fluent_support/min_serial_number', $minNumber);
827 +
828 + try {
829 + $lastTicketNumber = self::query()->max('serial_number');
830 + } catch (\Exception $e) {
831 + $lastTicketNumber = null;
832 + }
833 +
834 + $nextNumber = ((int) $lastTicketNumber) + 1;
835 +
836 + return max($nextNumber, $minNumber);
837 + }
838 +
839 + public static function isMinimumSerialNumberEnabled()
840 + {
841 + $businessSettings = Helper::getOption('global_business_settings', []);
842 + return ($businessSettings['enable_min_serial_number'] ?? 'no') === 'yes';
843 + }
844 +
845 + public static function getTicketPrefix($ticket = null)
846 + {
847 + $businessSettings = Helper::getOption('global_business_settings', []);
848 + $prefix = self::isMinimumSerialNumberEnabled() ? trim((string) ($businessSettings['ticket_prefix'] ?? '')) : '';
849 +
850 + $productId = $ticket ? $ticket->product_id : null;
851 +
852 + return apply_filters('fluent_support/ticket_prefix', $prefix, $ticket, $productId);
853 + }
854 +
855 + public function getDisplayTicketNumberAttribute()
856 + {
857 + return $this->ticket_number ?: ($this->serial_number ?: $this->id);
858 + }
859 +
860 + public function scopeWherePublicIdentifier($query, $identifier)
861 + {
862 + return $query->where('serial_number', $identifier);
863 + }
864 +
865 + protected function assignTicketNumber()
866 + {
867 + for ($attempt = 0; $attempt < 5; $attempt++) {
868 + $nextNumber = $this->serial_number ?: (self::isMinimumSerialNumberEnabled() ? self::getNextSerialNumber() : $this->id);
869 + $ticketNumber = $this->ticket_number ?: (self::getTicketPrefix($this) . $nextNumber);
870 +
871 + try {
872 + self::where('id', $this->id)->update([
873 + 'serial_number' => $nextNumber,
874 + 'ticket_number' => $ticketNumber
875 + ]);
876 + $this->serial_number = $nextNumber;
877 + $this->ticket_number = $ticketNumber;
878 +
879 + return $nextNumber;
880 + } catch (\Exception $e) {
881 + if (stripos($e->getMessage(), 'duplicate') === false) {
882 + throw $e;
883 + }
884 + }
885 + }
886 +
887 + throw new \RuntimeException('Could not allocate a unique ticket number.');
888 + }
889 +
689 890 public static function slugify($title)
690 891 {
691 892 $slug = sanitize_title($title, 'support-ticket-' . time(), 'display');
692 893 if (Ticket::where('slug', $slug)->first()) {
@@ -748,9 +949,9 @@
748 949
749 950 if ($value) {
750 951 if (in_array($fieldType, $customRenderers) && $rendered) {
751 952 $value = apply_filters('fluent_support/custom_field_render_' . $fieldType, $value, $scope);
752 - } else if ($fieldType == 'checkbox') {
953 + } else if (in_array($fieldType, ['checkbox', 'date-range'])) {
753 954 $value = array_values(array_filter(explode('|', $value)));
754 955 }
755 956
756 957 if (!is_array($value) && !is_object($value)) {
@@ -903,270 +1104,43 @@
903 1104 return $result;
904 1105 }
905 1106
906 1107 /**
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
1108 + * @deprecated Use TicketService::storeTicket() instead.
912 1109 */
913 -
914 1110 public function createTicket($ticketData, $maybeNewCustomer = false)
915 1111 {
916 - $ticketData = $this->maybeCreateNewCustomer($ticketData, $maybeNewCustomer);
1112 + _deprecated_function(__METHOD__, '2.0.5', 'TicketService::storeTicket()');
917 1113
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);
1114 + if (empty($ticketData['customer_id']) && $maybeNewCustomer) {
1115 + $email = Arr::get($maybeNewCustomer, 'email');
1116 + if (!$email || !is_email($email)) {
1117 + return new \WP_Error('error', 'A valid email is required to create a ticket');
1019 1118 }
1020 - }
1021 1119
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;
1120 + $existingCustomer = Customer::where('email', $email)->first();
1121 + if ($existingCustomer) {
1122 + $ticketData['customer_id'] = $existingCustomer->id;
1123 + } else {
1124 + $customerData = Arr::only($maybeNewCustomer, (new Customer())->getFillable());
1125 + $customerData = array_filter($customerData);
1126 + $createCustomer = Customer::create($customerData);
1127 + if (!$createCustomer) {
1128 + return new \WP_Error('error', 'Customer could not be created');
1082 1129 }
1130 + $ticketData['customer_id'] = $createCustomer->id;
1083 1131 }
1084 1132 }
1085 1133
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 - }
1134 + if (empty($ticketData['customer_id'])) {
1135 + return new \WP_Error('error', 'Ticket could not be created');
1097 1136 }
1098 1137
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
1138 + $customer = Customer::findOrFail($ticketData['customer_id']);
1102 1139
1103 - return $this->getTicketAdditionalData($agent, $ticket->responses, $ticket, $withCrmData);
1140 + return (new TicketService())->storeTicket($ticketData, $customer);
1104 1141 }
1105 1142
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 - }
1132 - } 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 1143 /**
1170 1144 * This `createResponse` will create a response for a ticket
1171 1145 * @param array $data
1172 1146 * @param int $ticketId
@@ -1172,409 +1146,9 @@
1172 1146 * @param int $ticketId
1173 1147 * @return array
1174 1148 * @throws Exception
1175 1149 */
1176 - public function createResponse($data, $ticketId)
1177 - {
1178 - $agent = Helper::getAgentByUserId(get_current_user_id());
1179 - $this->checkIfValidAgent($agent);
1180 1150
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);
1394 - }
1395 - }
1396 - }
1397 -
1398 - if ($propName && $propValue && $prevValue != $propValue) {
1399 - $ticket->{$propName} = $propValue;
1400 - $ticket->save();
1401 - }
1402 -
1403 - $updateData = [];
1404 -
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;
1418 - }
1419 -
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 1151 public static function countTicketByMailBoxId($mailbox_id)
1578 1152 {
1579 1153 return self::where('mailbox_id', $mailbox_id)->count();
1580 1154 }
@@ -1660,5 +1234,4 @@
1660 1234
1661 1235 }
1662 1236
1663 1237 }
1664 -