# fluent-support/2.4.5/app/Models/Ticket.php

Fluent Support – Helpdesk &amp; Customer Support Ticket System, version 2.4.5. 1,290 lines.

- Page: https://pluginprobe.com/plugins/fluent-support/2.4.5/code/app/Models/Ticket.php
- Raw: https://pluginprobe.com/plugins/fluent-support/2.4.5/raw/app/Models/Ticket.php
- Modified: 2026-10-09T15:31:16+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/fluent-support/2.4.5/code/app/Models/Ticket.php#L10-L20`.

```php
<?php

namespace FluentSupport\App\Models;

use Exception;
use FluentSupport\App\Modules\PermissionManager;
use FluentSupport\App\Services\Helper;
use FluentSupport\App\Services\TicketHelper;
use FluentSupport\App\Services\TicketQueryService;
use FluentSupport\App\Services\Tickets\TicketService;
use FluentSupport\Framework\Support\Arr;

class Ticket extends Model
{
    protected $table = 'fs_tickets';

    protected $dates = ['waiting_since'];

    /**
     * The hash is a bearer credential for the public ticket view, so keep it
     * out of API responses. Property access still works for signed URLs.
     *
     * @var array
     */
    protected $hidden = ['hash', 'content_hash'];

    protected $appends = ['display_ticket_number'];

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'customer_id',
        'agent_id',
        'product_id',
        'mailbox_id',
        'product_source',
        'privacy',
        'priority',
        'client_priority',
        'status',
        'title',
        'slug',
        'hash',
        'source',
        'message_id',
        'content',
        'last_agent_response',
        'last_customer_response',
        'waiting_since',
        'response_count',
        'first_response_time',
        'total_close_time',
        'resolved_at',
        'closed_by',
        'created_by',
        'serial_number',
        'ticket_number'
    ];

    public static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            if (empty($model->slug)) {
                $model->slug = static::slugify($model->title);
            }

            $model->hash = bin2hex(random_bytes(16));
            $model->content_hash = md5($model->content);

            $model->last_customer_response = current_time('mysql');
            $model->created_at = current_time('mysql');
            $model->updated_at = current_time('mysql');
            $model->waiting_since = current_time('mysql');

        });

        static::updating(function ($model) {
            // Rotate the hash on customer change so links already emailed to
            // the previous customer stop working.
            if ($model->isDirty('customer_id')) {
                $model->hash = bin2hex(random_bytes(16));
            }
        });

        static::created(function ($model) {
            if (empty($model->serial_number) || empty($model->ticket_number)) {
                $model->assignTicketNumber();
            }
        });

        static::deleting(function ($model) {
            Meta::where('object_type', 'ticket_meta')->where('object_id', $model->id)->delete();
            // CC info
            Meta::where('object_type', 'ticket')->where('object_id', $model->id)->delete();
            // Drafts
            Meta::where('object_type', '_fs_auto_draft')->where('object_id', $model->id)->delete();
            Notification::deleteByTicketId($model->id);
            // Response attachments are cleaned up by Conversation::deleting
            Conversation::deleteAll($model->id);
            // Ticket-level attachments, plus the ticket upload directory
            $class = __NAMESPACE__ . '\Attachment';
            $ticketAttachments = $class::where('ticket_id', $model->id)->whereNull('conversation_id')->get();
            $class::purgeAttachments($ticketAttachments, $model->id);
            $class::where('ticket_id', $model->id)->whereNull('conversation_id')->delete();
        });
    }

    /**
     * @var array
     */
    protected $searchable = [
        'content',
        'title',
        'slug',
        'id',
        'serial_number',
        'ticket_number'
    ];

    /**
     * Supports `column:value` to match a single fillable column.
     *
     * @param ModelQueryBuilder $query
     * @param string $search
     * @return ModelQueryBuilder
     */
    public function scopeSearchBy($query, $search)
    {

        if(!$search) {
            return $query;
        }

        if (strpos($search, ':')) {
            $array = explode(':', (string) $search);
            $column = $array[0];
            $value = $array[1];
            $columns = $this->fillable;
            $columns[] = 'id';

            if (in_array($column, $columns) && $value) {
                if (is_numeric($value)) {
                    $query->where($column, $value);
                } else {
                    $query->where($column, 'LIKE', "%$value%");
                }
                return $query;
            }
        }

        $fields = $this->searchable;
        $query->where(function ($query) use ($fields, $search) {
            $query->where(array_shift($fields), 'LIKE', "%$search%");
            foreach ($fields as $field) {
                $query->orWhere($field, 'LIKE', "%$search%");
            }
        });

        return $query;
    }

    /**
     * @param ModelQueryBuilder $query
     * @param array $search
     * @return ModelQueryBuilder
     */

    public function doSearchForAdvancedFilter($query, $search)
    {
        foreach ($search as $s) {
            $operator = $s['operator'];
            if (in_array($s['property'], ['title', 'content'])) {
                if ($operator == 'contains') {
                    $query = $query->where(function ($query) use ($s) {
                        $query->where($s['property'], 'LIKE', "%" . $s['value'] . "%");
                    });
                } elseif ($operator == 'not_contains') {
                    $query = $query->where(function ($query) use ($s) {
                        $query->where($s['property'], 'NOT LIKE', '%' . $s['value'] . '%');
                    });
                }
            }

            if ($s['property'] == 'conversation_content') {
                $operator = $s['operator'];
                if ($operator == 'contains') {
                    $query = $query->whereHas('responses', function ($q) use ($s) {
                        $q->where('content', 'LIKE', "%" . $s['value'] . "%");
                    });

                } else if ($operator == 'not_contains') {
                    $query = $query->whereHas('responses', function ($q) use ($s) {
                        $q->where('content', 'NOT LIKE', "%" . $s['value'] . "%");
                    });
                }
            }

            if ($s['property'] == 'source') {
                $query = static::buildSourceFilterQuery($query, $s);
            }

            if (in_array($s['property'], ['created_at', 'updated_at', 'waiting_since', 'last_agent_response', 'last_customer_response'])) {
                $query = (new \FluentSupport\App\Models\Ticket())->buildDateBaseFilterQuery($query, $s);
            }

            if (in_array($s['property'], ['status', 'client_priority', 'priority', 'tags', 'product', 'waiting_for_reply', 'agent_id', 'mailbox_id'])) {
                $query = (new \FluentSupport\App\Models\Ticket())->buildPropertiesFilterQuery($query, $s);
            }
        }
        return $query;
    }

    /** Advanced-filter value for tickets with no recorded source (older tickets). */
    const SOURCE_NONE = '__none__';

    /**
     * Where tickets come from, for the advanced filter's Source field.
     *
     * @return array<string,string> source => label
     */
    public static function sourceFilterOptions()
    {
        return apply_filters('fluent_support/ticket_source_filter_options', [
            'web'                          => __('Web', 'fluent-support'),
            'email'                        => __('Email', 'fluent-support'),
            self::SOURCE_AGENT_INITIATED   => __('Agent outreach', 'fluent-support'),
            'mcp'                          => __('MCP (AI agents)', 'fluent-support'),
            'telegram'                     => __('Telegram', 'fluent-support'),
            'slack'                        => __('Slack', 'fluent-support'),
            'whatsapp'                     => __('WhatsApp', 'fluent-support'),
            'awesome-support'              => __('Imported: Awesome Support', 'fluent-support'),
            'freshdesk'                    => __('Imported: Freshdesk', 'fluent-support'),
            'helpscout'                    => __('Imported: Help Scout', 'fluent-support'),
            'js-helpdesk'                  => __('Imported: JS Help Desk', 'fluent-support'),
            'support-candy'                => __('Imported: SupportCandy', 'fluent-support'),
            'zendesk'                      => __('Imported: Zendesk', 'fluent-support'),
            self::SOURCE_NONE              => __('No source', 'fluent-support'),
        ]);
    }

    /**
     * Source "in" / "not in". SOURCE_NONE stands for NULL or empty, which SQL's
     * IN and NOT IN would otherwise skip.
     *
     * @param \FluentSupport\Framework\Database\Orm\Builder $query
     * @param array $filter
     * @return \FluentSupport\Framework\Database\Orm\Builder
     */
    public static function buildSourceFilterQuery($query, $filter)
    {
        $values = array_values(array_filter(array_map('sanitize_text_field', (array) Arr::get($filter, 'value', [])), 'strlen'));
        if (!$values) {
            return $query;
        }

        $includeNone = in_array(self::SOURCE_NONE, $values, true);
        $sources = array_values(array_diff($values, [self::SOURCE_NONE]));

        if (Arr::get($filter, 'operator') === 'not_in') {
            return $query->where(function ($q) use ($sources, $includeNone) {
                if ($includeNone) {
                    $q->whereNotNull('source')->where('source', '!=', '');
                    if ($sources) {
                        $q->whereNotIn('source', $sources);
                    }
                } else {
                    $q->whereNotIn('source', $sources)->orWhereNull('source');
                }
            });
        }

        return $query->where(function ($q) use ($sources, $includeNone) {
            if ($sources) {
                $q->whereIn('source', $sources);
            }
            if ($includeNone) {
                $q->orWhereNull('source')->orWhere('source', '');
            }
        });
    }

    /**
     * @param ModelQueryBuilder $query
     * @param array $statuses
     * @return ModelQueryBuilder
     */
    public function scopeFilterByStatues($query, $statuses)
    {
        if ($statuses) {
            $query->whereIn('status', $statuses);
        }

        return $query;
    }

    /**
     * Who replied last: 'agent', 'customer', or null when neither timestamp is
     * set. PHP counterpart of the timestamp check in scopeWaitingOnly(). A new
     * ticket resolves to 'customer' because boot() seeds last_customer_response.
     *
     * @return string|null
     */
    public function getLastReplyByAttribute()
    {
        $agentAt    = $this->last_agent_response;
        $customerAt = $this->last_customer_response;

        if (!$agentAt && !$customerAt) {
            return null;
        }
        if (!$agentAt) {
            return 'customer';
        }
        if (!$customerAt) {
            return 'agent';
        }

        // A tie resolves to 'customer'. scopeWaitingOnly() uses a strict <, so SQL treats a tie as answered.
        return strtotime($customerAt) >= strtotime($agentAt) ? 'customer' : 'agent';
    }

    /**
     * Tickets awaiting an agent reply.
     * @param $query
     * @return mixed
     */
    public function scopeWaitingOnly($query)
    {
        $query->where(function ($q) {
            $q->whereColumn('last_agent_response', '<', 'last_customer_response')
                ->orWhereNull('last_agent_response')
                ->orWhere('status', 'new');
        });
        return $query;
    }

    /**
     * @param $query
     * @param $filters
     * @return ModelQueryBuilder
     */
    public function scopeApplyFilters($query, $filters)
    {
        $supportedColumns = ['product_id', 'client_priority', 'priority', 'mailbox_id'];
        foreach ($filters as $filterKey => $filterValue) {
            if (!$filterValue && ($filterValue !== '0' && $filterValue !== 0)) {
                continue;
            }
            if ($filterKey == 'status_type') {
                $statusArray = Helper::getTkStatusesByGroupName($filterValue);
                if ($statusArray) {
                    $query->whereIn('status', $statusArray);
                }
            } else if (in_array($filterKey, $supportedColumns)) {
                if (is_array($filterValue)) {
                    $query->whereIn($filterKey, $filterValue);
                } else {
                    $query->where($filterKey, $filterValue);
                }
            } else if ($filterKey == 'waiting_for_reply') {
                if ($filterValue != 'yes') {
                    continue;
                }
                $query = $this->scopeWaitingOnly($query);
            } else if ($filterKey == 'agent_id') {
                if (is_array($filterValue)) {
                    $hasUnassigned = in_array('unassigned', $filterValue);
                    $agentIds = array_filter($filterValue, function($v) {
                        return $v !== 'unassigned';
                    });

                    if ($hasUnassigned && !empty($agentIds)) {
                        $query->where(function($q) use ($agentIds) {
                            $q->whereNull('agent_id')
                              ->orWhereIn('agent_id', $agentIds);
                        });
                    } elseif ($hasUnassigned) {
                        $query->whereNull('agent_id');
                    } elseif (!empty($agentIds)) {
                        if (defined('FLUENTSUPPORTPRO')) {
                            if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
                                $watcherTickets = [];
                                foreach ($agentIds as $agentId) {
                                    $watcherTickets = array_merge($watcherTickets, TicketHelper::getWatcherTicketIds($agentId));
                                }
                                $query->whereIn('id', array_unique($watcherTickets));
                            } else {
                                $query->whereIn('agent_id', $agentIds);
                            }
                        } else {
                            $query->whereIn('agent_id', $agentIds);
                        }
                    }
                } else {
                    // Single value (legacy)
                    if ($filterValue == 'unassigned') {
                        $query->whereNull($filterKey);
                    } else {
                        if (defined('FLUENTSUPPORTPRO')) {
                            if (isset($filters['watcher']) && $filters['watcher'] == 'watcher') {
                                $watcherTickets = TicketHelper::getWatcherTicketIds($filterValue);
                                $query->whereIn('id', $watcherTickets);
                            } else {
                                $query->where($filterKey, $filterValue);
                            }
                        } else {
                            $query->where($filterKey, $filterValue);
                        }
                    }
                }
            } else if ($filterKey == 'agent_group') {
                $groupIds = is_array($filterValue) ? $filterValue : [$filterValue];
                $groupIds = array_filter(array_map('intval', $groupIds));
                if (!empty($groupIds)) {
                    $agentIds = TagPivot::where('source_type', 'agent_group')
                        ->whereIn('tag_id', $groupIds)
                        ->pluck('source_id')
                        ->toArray();
                    if ($agentIds) {
                        $query->whereIn('agent_id', $agentIds);
                    } else {
                        $query->whereRaw('1 = 0');
                    }
                }
            } else if ($filterKey == 'ticket_tags') {
                if (!$filterValue) {
                    continue;
                }
                $query->whereHas('tags', function ($q) use ($filterValue) {
                    $q->whereIn('tag_id', $filterValue);
                });
            }
        }

        return $query;
    }

    /**
     * @param ModelQueryBuilder $query
     * @param int $agentId
     * @return ModelQueryBuilder
     */
    public function scopeFilterByAgentId($query, $agentId)
    {
        if ($agentId) {
            $query->where('agent_id', $agentId);
        }

        return $query;
    }

    /**
     * @param ModelQueryBuilder $query
     * @param int $customerId
     * @return ModelQueryBuilder
     */
    public function scopeFilterByCustomerId($query, $customerId)
    {
        $query->where('customer_id', $customerId);

        return $query;
    }

    /**
     * @param ModelQueryBuilder $query
     * @param int $productId
     * @return ModelQueryBuilder
     */
    public function scopeFilterByProductId($query, $productId)
    {
        if ($productId) {
            $query->where('product_id', $productId);
        }

        return $query;
    }

    /**
     * @param ModelQueryBuilder $query
     * @param array $priorities
     * @return ModelQueryBuilder
     */
    public function scopeFilterByPriorities($query, $priorities)
    {
        if ($priorities) {
            $query->whereIn('priority', $priorities);
        }

        return $query;
    }

    /**
     * @param $filter
     * @return string[]
     */

    public static function parseRelationalFilterQueryMethods($filter)
    {
        // default operator = in
        $method = 'whereHas';
        $subMethod = 'whereIn';

        switch ($filter['operator']) {
            case 'not_in':
                $method = 'whereDoesntHave';
                $subMethod = 'whereIn';

                break;
            case 'in_all':
                $method = 'whereHas';
                $subMethod = 'where';

                break;
            case 'not_in_all':
                $method = 'whereDoesntHave';
                $subMethod = 'where';

                break;
        }

        return [$method, $subMethod];
    }

    /**
     * Parse filter to set proper operator and value for the filter query.
     *
     * @param array $filter
     * @return array
     */
    public static function filterParser($filter)
    {
        switch ($filter['operator']) {
            case 'before':
                $filter['operator'] = '<';
                $filter['value'] = $filter['value'] . ' 23:59:59';
                break;

            case 'after':
                $filter['operator'] = '>';
                $filter['value'] = $filter['value'] . ' 23:59:59';
                break;

            case 'date_equal':
                $filter['operator'] = 'LIKE';
                $filter['value'] = '%' . $filter['value'] . '%';
                break;

            case 'days_before':
                $filter['operator'] = '<';
                $filter['value'] = gmdate('Y-m-d', time() - $filter['value'] * 24 * 60 * 60);
                break;

            case 'days_within':
                $filter['operator'] = 'BETWEEN';
                $filter['value'] = [
                    gmdate('Y-m-d', time() - $filter['value'] * 24 * 60 * 60),
                    gmdate('Y-m-d') . ' 23:59:59'
                ];
                break;
            case 'date_range':
                $filter['operator'] = 'BETWEEN';
                if (isset($filter['value'][0]))
                    $filter['value'][0] .= ' 00:00:00';
                if (isset($filter['value'][1]))
                    $filter['value'][1] .= ' 23:59:59';
                break;
        }

        return $filter;
    }

    /**
     * @param \FluentSupport\Framework\Database\Orm\Builder|\FluentSupport\Framework\Database\Query\Builder $query
     * @param array $filters
     * @return ModelQueryBuilder
     */
    public function buildDateBaseFilterQuery($query, $filters)
    {
        $filter = static::filterParser($filters);
        $query->where(function ($dateQuery) use ($filter) {

            if ($filter['operator'] == 'BETWEEN') {
                $dateQuery->whereBetween($filter['property'], $filter['value']);
            } else {
                $dateQuery->where($filter['property'], $filter['operator'], $filter['value']);
            }
        });

        return $query;
    }

    /**
     * @param $relation
     * @param $query
     * @param $method
     * @param $subMethod
     * @param $subField
     * @param $filter
     * @param false $provider
     * @return ModelQueryBuilder
     */

    public static function buildRelationFilterQuery($relation, $query, $method, $subMethod, $subField, $filter, $provider = false)
    {
        if (in_array($filter['operator'], ['in_all', 'not_in_all']) && $filter['value']) {
            foreach ($filter['value'] as $item) {
                $query = static::buildRelationFilterQuery($relation, $query, $method, $subMethod, $subField, ['value' => $item, 'operator' => ''], $provider);
            }
        } else {
            $query = $query->{$method}($relation, function ($relationQuery) use ($subMethod, $subField, $filter, $provider) {
                $relationQuery = $relationQuery->{$subMethod}($subField, $filter['value']);

                if ($provider) {
                    $relationQuery = $relationQuery->where('provider', $provider);
                }

                return $relationQuery;
            });
        }

        return $query;
    }

    /**
     * @param $query
     * @param $filter
     * @return ModelQueryBuilder
     */

    public function buildPropertiesFilterQuery($query, $filter)
    {
        if (in_array($filter['property'], ['tags', 'product'])) {
            $subField = $filter['property'] == 'tags' ? 'tag_id' : 'product_id';
            list($method, $subMethod) = static::parseRelationalFilterQueryMethods($filter);
            $query = static::buildRelationFilterQuery($filter['property'], $query, $method, $subMethod, $subField, $filter);
        } elseif ($filter['property'] == 'waiting_for_reply') {
            if (($filter['value'] == 'yes' && $filter['operator'] == 'in') || ($filter['value'] == 'no' && $filter['operator'] == 'not_in')) {
                $query = $query->where(function ($q) {
                    $q->whereColumn('last_agent_response', '<', 'last_customer_response')
                        ->orWhereNull('last_agent_response')
                        ->orWhere('status', 'new');
                });
            } else {
                $query = $query->where(function ($q) {
                    $q->whereColumn('last_customer_response', '<', 'last_agent_response');
                });
            }
        } else {
            $method = $filter['operator'] == 'in' ? 'whereIn' : 'whereNotIn';
            $query = $query->{$method}($filter['property'], (array)$filter['value']);
        }
        return $query;
    }

    /**
     * @param $provider
     * @param $query
     * @param $search
     * @param string $operator
     * @return ModelQueryBuilder
     */
    public function buildSearchableQuery($provider, $query, $search, $operator = 'LIKE')
    {
        switch ($provider) {
            case 'customer':
                $fields = (new Customer())->getSearchableFields();
                break;
            case 'agent':
                $fields = (new Agent())->getSearchableFields();
                break;
            default:
                $fields = $this->searchable;
                break;
        }

        $query->whereHas($provider, function ($query) use ($fields, $search, $operator) {
            $query->where(array_shift($fields), $operator, $search);

            $nameArray = explode(' ', (string) $search);

            if (count($nameArray) >= 2) {
                $query->orWhere(function ($q) use ($nameArray, $operator) {
                    $firstName = array_shift($nameArray);
                    $lastName = implode(' ', $nameArray);

                    $q->where('first_name', $operator, $firstName);
                    $q->where('last_name', $operator, $lastName);
                });
            }

            foreach ($fields as $field) {
                $query->orWhere($field, $operator, $search);
            }
        });

        return $query;
    }

    /**
     * Filter by fields on the related customer or agent.
     * @param $provider
     * @param $query
     * @param $filters
     * @return ModelQueryBuilder
     */
    public function filterTicketByUser($provider, $query, $filters)
    {
        foreach ($filters as $filter) {
            if ($filter['operator'] == 'in' || $filter['operator'] == 'not_in') {
                $method = $filter['operator'] == 'in' ? 'whereIn' : 'whereNotIn';
                $query = $query->whereHas($provider, function ($q) use ($method, $filter) {
                    $q->{$method}($filter['property'], $filter['value']);
                });
            }

            if ($filter['operator'] == 'contains' || $filter['operator'] == 'not_contains') {
                $operator = $filter['operator'] == 'contains' ? 'LIKE' : 'NOT LIKE';
                $query->whereHas($provider, function ($q) use ($operator, $filter) {
                    $q->where($filter['property'], $operator, '%' . $filter['value'] . '%');
                });
            }

            if ($filter['operator'] == '=' || $filter['operator'] == '!=') {
                $operator = $filter['operator'];
                $query->whereHas($provider, function ($q) use ($operator, $filter) {
                    $q->where($filter['property'], $operator, $filter['value']);
                });
            }
        }
        return $query;
    }

    /**
     * Newest first.
     *
     * @return Model Collection
     */
    public function responses()
    {
        $class = __NAMESPACE__ . '\Conversation';

        return $this->hasMany(
            $class, 'ticket_id', 'id'
        )->orderBy('created_at', 'desc')
            ->orderBy('id', 'desc');
    }

    /** source value for a ticket an agent opened to reach out to the customer. */
    const SOURCE_AGENT_INITIATED = 'agent_initiated';

    /**
     * Whether an agent opened this ticket to reach out. The message is then the
     * agent's first response, and the ticket body is only a placeholder.
     *
     * @return bool
     */
    public function isAgentInitiated()
    {
        if ($this->source === self::SOURCE_AGENT_INITIATED) {
            return true;
        }

        // Before 2.4.1 only the "initialized this ticket" placeholder body marked it.
        return $this->created_by && self::hasLegacyAgentInitiatedMarker($this->content);
    }

    /**
     * @param string $content
     * @return bool
     */
    public static function hasLegacyAgentInitiatedMarker($content)
    {
        $content = (string) $content;

        return strpos($content, ' initialized this ticket') !== false
            || strpos($content, __(' initialized this ticket', 'fluent-support')) !== false;
    }

    public function preview_response()
    {
        $class = __NAMESPACE__ . '\Conversation';

        return $this->hasOne(
            $class, 'ticket_id', 'id'
        );
    }

    public function tags()
    {
        $class = __NAMESPACE__ . '\TicketTag';

        return $this->belongsToMany(
            $class, 'fs_tag_pivot', 'source_id', 'tag_id'
        )->wherePivot('source_type', 'ticket_tag');
    }

    public function watchers()
    {
        $class = __NAMESPACE__ . '\TagPivot';

        return $this->hasMany($class, 'source_id', 'id')
            ->where('source_type', 'ticket_watcher')
            ->select(['tag_id']);
    }

    /**
     * @return Model Collection
     */
    public function customer()
    {
        $class = __NAMESPACE__ . '\Customer';

        return $this->belongsTo(
            $class, 'customer_id', 'id'
        );
    }

    /**
     * @return Model Collection
     */
    public function agent()
    {
        $class = __NAMESPACE__ . '\Agent';

        return $this->belongsTo(
            $class, 'agent_id', 'id'
        );
    }

    public function closed_by_person()
    {
        $class = __NAMESPACE__ . '\Person';

        return $this->belongsTo(
            $class, 'closed_by', 'id'
        );
    }

    public function created_by_person()
    {
        $class = __NAMESPACE__ . '\Agent';

        return $this->belongsTo(
            $class, 'created_by', 'id'
        );
    }

    public function product()
    {
        $class = __NAMESPACE__ . '\Product';

        return $this->belongsTo(
            $class, 'product_id', 'id'
        );
    }

    public function mailbox()
    {
        $class = __NAMESPACE__ . '\MailBox';

        return $this->belongsTo(
            $class, 'mailbox_id', 'id'
        );
    }


    public function deleteTicket()
    {
        /*
         * Action on ticket deleting
         *
         * @since v1.0.0
         * @param object $ticket
         */
        do_action('fluent_support/deleting_ticket', $this);
        $this->delete();
    }

    public static function getNextSerialNumber()
    {
        $businessSettings = Helper::getOption('global_business_settings', []);
        $minNumber = (int) ($businessSettings['min_serial_number'] ?? 1);
        $minNumber = (int) apply_filters('fluent_support/min_serial_number', $minNumber);

        try {
            $lastTicketNumber = self::query()->max('serial_number');
        } catch (\Exception $e) {
            $lastTicketNumber = null;
        }

        $nextNumber = ((int) $lastTicketNumber) + 1;

        return max($nextNumber, $minNumber);
    }

    public static function isMinimumSerialNumberEnabled()
    {
        $businessSettings = Helper::getOption('global_business_settings', []);
        return ($businessSettings['enable_min_serial_number'] ?? 'no') === 'yes';
    }

    public static function getTicketPrefix($ticket = null)
    {
        $businessSettings = Helper::getOption('global_business_settings', []);
        $prefix = self::isMinimumSerialNumberEnabled() ? trim((string) ($businessSettings['ticket_prefix'] ?? '')) : '';

        $productId = $ticket ? $ticket->product_id : null;

        return apply_filters('fluent_support/ticket_prefix', $prefix, $ticket, $productId);
    }

    public function getDisplayTicketNumberAttribute()
    {
        return $this->ticket_number ?: ($this->serial_number ?: $this->id);
    }

    public function scopeWherePublicIdentifier($query, $identifier)
    {
        return $query->where('serial_number', $identifier);
    }

    protected function assignTicketNumber()
    {
        for ($attempt = 0; $attempt < 5; $attempt++) {
            $nextNumber = $this->serial_number ?: (self::isMinimumSerialNumberEnabled() ? self::getNextSerialNumber() : $this->id);
            $ticketNumber = $this->ticket_number ?: (self::getTicketPrefix($this) . $nextNumber);

            try {
                self::where('id', $this->id)->update([
                    'serial_number' => $nextNumber,
                    'ticket_number' => $ticketNumber
                ]);
                $this->serial_number = $nextNumber;
                $this->ticket_number = $ticketNumber;

                return $nextNumber;
            } catch (\Exception $e) {
                if (stripos($e->getMessage(), 'duplicate') === false) {
                    throw $e;
                }
            }
        }

        throw new \RuntimeException('Could not allocate a unique ticket number.');
    }

    public static function slugify($title)
    {
        $slug = sanitize_title($title, 'support-ticket-' . time(), 'display');
        if (Ticket::where('slug', $slug)->first()) {
            $slug .= '-' . time();
        }
        return $slug;
    }

    public function hasTag($tagId)
    {
        $tags = $this->tags;
        foreach ($tags as $tag) {
            if ($tag->id == $tagId) {
                return true;
            }
        }

        return false;
    }

    public function attachments()
    {
        $class = __NAMESPACE__ . '\Attachment';
        return $this->hasMany($class, 'ticket_id', 'id')->where('conversation_id', NULL);
    }

    public function customData($scope = 'admin', $rendered = false)
    {
        if (!defined('FLUENTSUPPORTPRO')) {
            return [];
        }

        $fields = \FluentSupportPro\App\Services\CustomFieldsService::getFieldLabels($scope);

        if (!$fields) {
            return [];
        }

        $keys = array_keys($fields);

        $customRows = Meta::where('object_type', 'ticket_meta')->where('object_id', $this->id)
            ->whereIn('key', $keys)
            ->get();

        if (!$customRows) {
            return [];
        }

        $formattedData = [];

        $customRenderers = \FluentSupportPro\App\Services\CustomFieldsService::getCustomerRenderers();

        foreach ($customRows as $row) {
            $dataKey = $row->key;

            $value = $row->value;

            $fieldType = $fields[$dataKey]['type'];

            if ($value) {
                if (in_array($fieldType, $customRenderers) && $rendered) {
                    $value = apply_filters('fluent_support/custom_field_render_' . $fieldType, $value, $scope);
                } else if (in_array($fieldType, ['checkbox', 'date-range'])) {
                    $value = array_values(array_filter(explode('|', $value)));
                }

                if (!is_array($value) && !is_object($value)) {
                    $formattedData[$dataKey] = links_add_target(make_clickable($value));
                } else {
                    $formattedData[$dataKey] = $value;
                }
            }
        }

        return $formattedData;
    }

    /**
     * @param array $data Custom field values keyed by field key.
     * @return bool
     */
    public function syncCustomFields($data)
    {
        if (!is_array($data)) {
            return false;
        }

        $fields = apply_filters('fluent_support/ticket_custom_fields', []);

        if (!$fields) {
            return false;
        }

        $keys = array_keys($fields);

        $validData = Arr::only($data, $keys);

        foreach ($validData as $dataKey => $validDatum) {
            if (empty($validDatum)) {
                Meta::where('object_type', 'ticket_meta')
                    ->where('object_id', $this->id)
                    ->where('key', $dataKey)
                    ->delete();
                continue;
            }

            if ($fields[$dataKey]['type'] == 'checkbox' || is_array($validDatum)) {
                $validDatum = implode('|', $validDatum);
                $validDatum = '|' . $validDatum . '|';
            }

            $exist = Meta::where('object_type', 'ticket_meta')
                ->where('object_id', $this->id)
                ->where('key', $dataKey)
                ->first();

            if ($exist) {
                $exist->value = $validDatum;
                $exist->save();
            } else {
                Meta::insert([
                    'object_type' => 'ticket_meta',
                    'object_id'   => $this->id,
                    'key'         => $dataKey,
                    'value'       => $validDatum
                ]);
            }
        }

        return true;
    }

    public function getLastAgentResponse()
    {
        $query = \FluentSupport\App\App::db()->table('fs_conversations')
            ->select(['fs_conversations.*'])
            ->where('fs_conversations.conversation_type', 'response')
            ->where('fs_conversations.ticket_id', $this->id)
            ->where('fs_persons.person_type', '=', 'agent')
            ->join('fs_persons', 'fs_persons.id', '=', 'fs_conversations.person_id')
            ->orderBy('fs_conversations.id', 'DESC');

        return $query->first();
    }

    public function getLastResponse()
    {
        return \FluentSupport\App\App::db()->table('fs_conversations')
            ->where('ticket_id', $this->id)
            ->where('conversation_type', 'response')
            ->latest('id')
            ->first();
    }

    /**
     * @param int|array $tagIds
     * @return bool True if any tag was added.
     */
    public function applyTags($tagIds)
    {
        $result = false;

        if (!is_array($tagIds)) {
            $tagIds = array($tagIds);
        }

        foreach ($tagIds as $tagId) {
            if (!$this->hasTag($tagId)) {
                $this->tags()->attach($tagId, ['source_type' => 'ticket_tag']);
                $result = true;

                /*
                 * Action while tag added to ticket
                 *
                 * @since v1.0.0
                 * @param integer $tagId
                 * @param object  $ticket
                 */
                do_action('fluent_support/ticket_tag_added', $tagId, $this);
            }
        }
        return $result;
    }

    /**
     * @param int|array $tagIds
     * @return bool True if any tag was removed.
     */
    public function detachTags($tagIds)
    {
        $result = false;

        if (!is_array($tagIds)) {
            $tagIds = array($tagIds);
        }

        foreach ($tagIds as $tagId) {
            if ($this->hasTag($tagId)) {
                $this->tags()->detach($tagId);

                /*
                 * Action while tag removed from ticket
                 *
                 * @since v1.0.0
                 * @param integer $tagId
                 * @param object  $ticket
                 */
                do_action('fluent_support/ticket_tag_removed', $tagId, $this);
                $result = true;
            }
        }
        return $result;
    }

    /**
     * @deprecated Use TicketService::storeTicket() instead.
     */
    public function createTicket($ticketData, $maybeNewCustomer = false)
    {
        _deprecated_function(__METHOD__, '2.0.5', 'TicketService::storeTicket()');

        if (empty($ticketData['customer_id']) && $maybeNewCustomer) {
            $email = Arr::get($maybeNewCustomer, 'email');
            if (!$email || !is_email($email)) {
                return new \WP_Error('error', 'A valid email is required to create a ticket');
            }

            $existingCustomer = Customer::where('email', $email)->first();
            if ($existingCustomer) {
                $ticketData['customer_id'] = $existingCustomer->id;
            } else {
                $customerData = Arr::only($maybeNewCustomer, (new Customer())->getFillable());
                $customerData = array_filter($customerData);
                $createCustomer = Customer::create($customerData);
                if (!$createCustomer) {
                    return new \WP_Error('error', 'Customer could not be created');
                }
                $ticketData['customer_id'] = $createCustomer->id;
            }
        }

        if (empty($ticketData['customer_id'])) {
            return new \WP_Error('error', 'Ticket could not be created');
        }

        $customer = Customer::findOrFail($ticketData['customer_id']);

        return (new TicketService())->storeTicket($ticketData, $customer);
    }

    public static function countTicketByMailBoxId($mailbox_id)
    {
        return self::where('mailbox_id', $mailbox_id)->count();
    }

    public static function syncMailBoxId($mailbox_id, $fallback_id)
    {
        return self::where('mailbox_id', $mailbox_id)
            ->update([
                'mailbox_id' => $fallback_id
            ]);
    }

    public static function getTicketsQuery()
    {
        return self::with([
            'customer'         => function ($query) {
                $query->select(['first_name', 'last_name', 'email', 'id', 'avatar']);
            }, 'agent'         => function ($query) {
                $query->select(['first_name', 'last_name', 'id']);
            },
            'product',
            'tags',
            'preview_response' => function ($query) {
                $query->latest('id');
            }
        ]);
    }

    public function getSettingsValue($valueKey = false, $default = false)
    {
        $exist = Meta::where('object_type', 'ticket')
            ->where('key', 'settings')
            ->where('object_id', $this->id)
            ->first();

        if ($exist) {
            $value = Helper::safeUnserialize($exist->value);
            if ($valueKey) {
                if (!is_array($value)) {
                    return $default;
                }
                return Arr::get($value, $valueKey, $default);
            }
            return $value;
        }

        return $default;
    }

    public function updateSettingsValue($valueKey, $value)
    {
        $exist = Meta::where('object_type', 'ticket')
            ->where('key', 'settings')
            ->where('object_id', $this->id)
            ->first();

        if ($exist) {
            $existingValue = Helper::safeUnserialize($exist->value);

            if (!is_array($existingValue)) {
                $existingValue = [];
            }

            $existingValue[$valueKey] = $value;

            $exist->value = maybe_serialize($existingValue);
            $exist->save();
            return $this;
        }

        $settings = [
            'object_type' => 'ticket',
            'key'         => 'settings',
            'object_id'   => $this->id,
            'value'       => maybe_serialize([
                $valueKey => $value
            ])
        ];

        Meta::create($settings);

        return $this;

    }

}

```
