PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.1.2
Fluent Support – Helpdesk & Customer Support Ticket System v2.1.2
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
← All changes | app/Models/Ticket.php +4 -136 trunk2.1.2 View file →
@@ -16,19 +16,8 @@
16 16
17 17 protected $dates = ['waiting_since'];
18 18
19 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 - /**
31 20 * The attributes that are mass assignable.
32 21 *
33 22 * @var array
34 23 */
@@ -55,11 +44,9 @@
55 44 'first_response_time',
56 45 'total_close_time',
57 46 'resolved_at',
58 47 'closed_by',
59 - 'created_by',
60 - 'serial_number',
61 - 'ticket_number'
48 + 'created_by'
62 49 ];
63 50
64 51 public static function boot()
65 52 {
@@ -79,23 +66,8 @@
79 66 $model->waiting_since = current_time('mysql');
80 67
81 68 });
82 69
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 -
98 70 static::deleting(function ($model) {
99 71 //Delete the ticket meta
100 72 Meta::where('object_type', 'ticket_meta')->where('object_id', $model->id)->delete();
101 73 //Delete all cc info for the ticket
@@ -101,10 +73,8 @@
101 73 //Delete all cc info for the ticket
102 74 Meta::where('object_type', 'ticket')->where('object_id', $model->id)->delete();
103 75 //Delete draft info
104 76 Meta::where('object_type', '_fs_auto_draft')->where('object_id', $model->id)->delete();
105 - //Delete internal notifications and notification recipient rows for the ticket
106 - Notification::deleteByTicketId($model->id);
107 77 //delete the responses first (their attachments are cleaned up by Conversation::deleting)
108 78 Conversation::deleteAll($model->id);
109 79 // Delete ticket-level attachments (conversation_id IS NULL) and remove the ticket upload directory
110 80 $class = __NAMESPACE__ . '\Attachment';
@@ -121,11 +91,9 @@
121 91 protected $searchable = [
122 92 'content',
123 93 'title',
124 94 'slug',
125 - 'id',
126 - 'serial_number',
127 - 'ticket_number'
95 + 'id'
128 96 ];
129 97
130 98 /**
131 99 * Local scope to filter tickets by search/query string
@@ -237,41 +205,8 @@
237 205 return $query;
238 206 }
239 207
240 208 /**
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 - /**
274 209 * Local scope to filter tickets by not response by agent
275 210 * @param $query
276 211 * @return mixed
277 212 */
@@ -818,76 +753,8 @@
818 753 // Delete the ticket
819 754 $this->delete();
820 755 }
821 756
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 -
890 757 public static function slugify($title)
891 758 {
892 759 $slug = sanitize_title($title, 'support-ticket-' . time(), 'display');
893 760 if (Ticket::where('slug', $slug)->first()) {
@@ -949,9 +816,9 @@
949 816
950 817 if ($value) {
951 818 if (in_array($fieldType, $customRenderers) && $rendered) {
952 819 $value = apply_filters('fluent_support/custom_field_render_' . $fieldType, $value, $scope);
953 - } else if (in_array($fieldType, ['checkbox', 'date-range'])) {
820 + } else if ($fieldType == 'checkbox') {
954 821 $value = array_values(array_filter(explode('|', $value)));
955 822 }
956 823
957 824 if (!is_array($value) && !is_object($value)) {
@@ -1234,4 +1101,5 @@
1234 1101
1235 1102 }
1236 1103
1237 1104 }
1105 +