# fluent-support/2.4.5/app/Services/Tickets/ResponseService.php

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

- Page: https://pluginprobe.com/plugins/fluent-support/2.4.5/code/app/Services/Tickets/ResponseService.php
- Raw: https://pluginprobe.com/plugins/fluent-support/2.4.5/raw/app/Services/Tickets/ResponseService.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/Services/Tickets/ResponseService.php#L10-L20`.

```php
<?php

namespace FluentSupport\App\Services\Tickets;

use FluentSupport\App\Models\Agent;
use FluentSupport\App\Models\Attachment;
use FluentSupport\App\Models\Conversation;
use FluentSupport\App\Services\Helper;
use FluentSupport\App\Services\Includes\UploadService;
use FluentSupport\Framework\Support\Arr;

class ResponseService
{
    /**
     * Add an agent or customer response to a ticket.
     *
     * @param array $data
     * @param \FluentSupport\App\Models\Person $person
     * @param \FluentSupport\App\Models\Ticket $ticket
     * @param bool $silently Skip the assignment/response/close hooks.
     * @return array|false
     */
    public function createResponse($data, $person, $ticket, $silently = false)
    {
        if (empty($data['content'])) {
            return false;
        }

        $cc_emails = Arr::get($data, 'cc_emails', []);

        $informationalReply = Arr::get($data, 'informational_reply', false);
        if ($person->person_type == 'agent') {
            $data['content'] = apply_filters('fluent_support/parse_smartcode_data', $data['content'], [
                'customer' => $ticket->customer,
                'agent'    => $person
            ]);
        }

        $convoType = Arr::get($data, 'conversation_type', 'response');

        // Input is already unslashed (or was never slashed, for API/MCP callers).
        // Don't unslash again or literal backslashes are lost.
        $content = wp_kses_post($data['content']);
        $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content);
        $content = apply_filters('fluent_support/response_content_before_use_anywhere', $content);
        $response = [
            'person_id'         => $person->id,
            'ticket_id'         => $ticket->id,
            'conversation_type' => $convoType,
            'content'           => $content,
            'source'            => Arr::get($data, 'source', 'web'),
            'content_hash'      => md5($content)
        ];

        if (!empty($data['message_id'])) {
            $response['message_id'] = sanitize_text_field($data['message_id']);
        }

        $response = apply_filters('fluent_support/new_' . $person->person_type . '_' . $convoType, $response, $ticket, $person);

        $createdResponse = \FluentSupport\App\Models\Conversation::create($response);

        if (!empty($cc_emails)) {
            $createdResponse->updateSettingsValue('cc_email', $cc_emails);

            // Keep a running list of every CC'd address on the ticket.
            $existingCcEmails = $ticket->getSettingsValue('all_cc_email', []);
            $newData = array_merge($existingCcEmails, $cc_emails);
            $ticket->updateSettingsValue('all_cc_email', array_unique($newData));
        }

        $createdResponse->load('person');

        if ($person->person_type == 'agent' && $ticket->status == 'new' && $convoType == 'response') {
            $ticket->status = 'active';
            if ($ticket->created_at) {
                $ticket->first_response_time = strtotime(current_time('mysql')) - strtotime($ticket->created_at);
            } else {
                $ticket->first_response_time = 300;
            }
        }

        $agentAdded = false;
        $previousAgentId = (int) $ticket->agent_id;
        $updateData = [];

        if ($person->person_type == 'agent') {
            if (!$ticket->agent_id && $convoType == 'response') {
                $ticket->agent_id = $person->id;
                $agentAdded = true;
                $updateData = [
                    'agent_id' => $person->id
                ];
            }

            if ($convoType == 'response') {
                if ($resetWaitingSince && !$informationalReply) {
                    $ticket->last_agent_response = current_time('mysql');
                    $ticket->waiting_since = current_time('mysql');
                }
            }
        } else {

            if ($ticket->last_agent_response && strtotime($ticket->last_agent_response) > strtotime($ticket->last_customer_response)) {
                $ticket->waiting_since = current_time('mysql');
            }

            $ticket->last_customer_response = current_time('mysql');
        }

        if ($convoType == 'response') {
            $ticket->response_count += 1;
        }

        $closed = false;
        if (Arr::get($data, 'close_ticket') == 'yes' && $ticket->status != 'closed') {
            $ticket->status = 'closed';
            $ticket->resolved_at = current_time('mysql');
            $ticket->closed_by = $person->id;
            $ticket->total_close_time = current_time('timestamp') - strtotime($ticket->created_at);
            $closed = true;

            $internalNote = __('Ticket has been closed', 'fluent-support');

            $internalNote = apply_filters('fluent_support/ticket_close_internal_note', $internalNote, $ticket);

            if ($internalNote) {
                Conversation::create([
                    'ticket_id'         => $ticket->id,
                    'person_id'         => $person->id,
                    'conversation_type' => 'internal_info',
                    'content'           => $internalNote
                ]);
            }
        }

        $ticket->save();

        Helper::tempImageMoveUploadDir($ticket->id, 'conversation', $createdResponse->id, $person->id);

        // Finalize files uploaded ahead of the response (status in-active).
        if ($attachmentHashes = Arr::get($data, 'attachments', [])) {
            $attachments = Attachment::where('ticket_id', $ticket->id)
                ->whereIn('file_hash', $attachmentHashes)
                ->where('status', 'in-active')
                ->get();

            if (!$attachments->isEmpty()) {
                $storageDriver = Helper::getUploadDriverKey();
                foreach ($attachments as $attachment) {
                    if ($storageDriver != 'local') {
                        do_action_ref_array('fluent_support/finalize_file_upload_' . $storageDriver, [&$attachment, $ticket->id]);
                    }

                    if ($attachment->driver == 'local') {
                        $newFileInfo = UploadService::copyFileTicketFolder($attachment->file_path, $ticket->id);
                        if ($newFileInfo && !empty($newFileInfo['file_path'])) {
                            $attachment->file_path = $newFileInfo['file_path'];
                            $attachment->full_url = $newFileInfo['url'];
                        }
                    }

                    $attachment->ticket_id = $ticket->id;
                    $attachment->person_id = $createdResponse->person_id;
                    $attachment->conversation_id = $createdResponse->id;
                    $attachment->status = 'active';
                    $attachment->save();
                }
                $createdResponse->load('attachments');
            }
        }

        if ($silently) {
            // The caller fires the hooks itself (fireResponseHooks()), e.g. after
            // its transaction commits; 'closed' / 'agent_added' tell it what happened.
            return [
                'response'          => $createdResponse,
                'ticket'            => $ticket,
                'update_data'       => $updateData,
                'closed'            => $closed,
                'agent_added'       => $agentAdded,
                'previous_agent_id' => $previousAgentId,
            ];
        }

        $updateData = $this->fireResponseHooks($createdResponse, $ticket, $person, $closed, $agentAdded, $previousAgentId, $updateData);

        return [
            'response'    => $createdResponse,
            'ticket'      => $ticket,
            'update_data' => $updateData
        ];
    }

    /**
     * Fire the hooks createResponse() fires after a response is stored.
     *
     * Split out so a caller that wrapped createResponse(..., $silently = true) in
     * a transaction can fire them once the rows are committed: the customer
     * email must not leave for a reply that is about to be rolled back.
     *
     * @param Conversation $createdResponse
     * @param \FluentSupport\App\Models\Ticket $ticket
     * @param \FluentSupport\App\Models\Person $person
     * @param bool $closed The response closed the ticket.
     * @param bool $agentAdded The response assigned the ticket to $person.
     * @param int $previousAgentId
     * @param array $updateData
     * @return array $updateData, with 'agent' when the assignment hook fired.
     */
    public function fireResponseHooks($createdResponse, $ticket, $person, $closed, $agentAdded = false, $previousAgentId = 0, $updateData = [])
    {
        $convoType = $createdResponse->conversation_type;

        if ($agentAdded) {
            $assigner = Helper::getCurrentAgent();
            $ticket->load('agent');

            /*
             * Fires when a ticket is assigned to an agent.
             *
             * @since v1.0.0
             * @param object $person
             * @param object $ticket
             * @param object $assigner
             * @param int $previousAgentId
             */
            do_action('fluent_support/agent_assigned_to_ticket', $person, $ticket, $assigner, $previousAgentId);
            $updateData['agent'] = $ticket->agent;
        }

        /*
         * Fires after a response or note is added.
         *
         * @since v1.0.0
         * @param string $convoType
         * @param string $personType
         * @param object $createdResponse
         * @param object $ticket
         * @param object $person
         */
        do_action('fluent_support/' . $convoType . '_added_by_' . $person->person_type, $createdResponse, $ticket, $person);


        if ($closed) {
            /*
             * Fires when a ticket is closed.
             *
             * @since v1.0.0
             * @param object $ticket
             * @param object $person
             */
            do_action('fluent_support/ticket_closed', $ticket, $person);

            /*
             * Fires when a ticket is closed, per closer type.
             *
             * @since v1.0.0
             * @param string $personType
             * @param object $ticket
             * @param object $person
             */
            do_action('fluent_support/ticket_closed_by_' . $person->person_type, $ticket, $person);
        }

        return $updateData;
    }

    /**
     * Publish a draft reply: it becomes a sent response and the customer is notified.
     * Shared by the admin "Approve" action and MCP's publish-draft-reply.
     *
     * @param \FluentSupport\App\Models\Ticket $ticket
     * @param \FluentSupport\App\Models\Conversation $response The draft.
     * @param \FluentSupport\App\Models\Person $person The agent publishing it.
     * @param string $content Final content, already sanitized.
     * @return \FluentSupport\App\Models\Conversation
     */
    public function publishDraft($ticket, $response, $person, $content)
    {
        $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content);

        $response->content = $content;
        $response->conversation_type = 'response';
        $response->created_at = current_time('mysql');
        $response->save();

        if ($person->person_type == 'agent' && $ticket->status == 'new') {
            $ticket->status = 'active';
            if ($ticket->created_at) {
                $ticket->first_response_time = strtotime(current_time('mysql')) - strtotime($ticket->created_at);
            } else {
                $ticket->first_response_time = 300;
            }
        }

        if ($resetWaitingSince) {
            $ticket->last_agent_response = current_time('mysql');
            $ticket->waiting_since = current_time('mysql');
        }

        $ticket->response_count += 1;
        $ticket->save();

        $author = $response->person_id ? Agent::find($response->person_id) : null;
        $sender = $author ?: $person;

        do_action('fluent_support/response_added_by_' . $sender->person_type, $response, $ticket, $sender);

        return $response;
    }

    /**
     * @deprecated 2.4.1 Replies no longer carry the signature. Email templates add it
     *             with {{agent.signature}}, so the template decides the sign-off.
     */
    public function maybeAppendSignature($content, $person)
    {
        $signature = self::getAppendedSignature($person);

        if ($signature === '') {
            return $content;
        }

        return $content . '<div class="fs_agent_signature">' . $signature . '</div>';
    }

    /**
     * The signature appended to this agent's replies, or '' when none is.
     *
     * @param \FluentSupport\App\Models\Person $person
     * @return string HTML
     */
    public static function getAppendedSignature($person)
    {
        if (!$person || $person->getMeta('agent_signature_enabled', 'no') !== 'yes') {
            return '';
        }

        $signature = (string) $person->getMeta('agent_signature', '');
        $hasText = !empty(trim(strip_tags($signature)));
        $hasImage = (bool) preg_match('/<img[\s>]/i', $signature);

        return ($hasText || $hasImage) ? $signature : '';
    }
}

```
