| @@ -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 | + | |