PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 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 All 69 releases
fluent-support / app / Services / Tickets / ResponseService.php

ResponseService.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.4.5, at app/Services/Tickets/ResponseService.php

347 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Services\Tickets;
4
5 use FluentSupport\App\Models\Agent;
6 use FluentSupport\App\Models\Attachment;
7 use FluentSupport\App\Models\Conversation;
8 use FluentSupport\App\Services\Helper;
9 use FluentSupport\App\Services\Includes\UploadService;
10 use FluentSupport\Framework\Support\Arr;
11
12 class ResponseService
13 {
14 /**
15 * Add an agent or customer response to a ticket.
16 *
17 * @param array $data
18 * @param \FluentSupport\App\Models\Person $person
19 * @param \FluentSupport\App\Models\Ticket $ticket
20 * @param bool $silently Skip the assignment/response/close hooks.
21 * @return array|false
22 */
23 public function createResponse($data, $person, $ticket, $silently = false)
24 {
25 if (empty($data['content'])) {
26 return false;
27 }
28
29 $cc_emails = Arr::get($data, 'cc_emails', []);
30
31 $informationalReply = Arr::get($data, 'informational_reply', false);
32 if ($person->person_type == 'agent') {
33 $data['content'] = apply_filters('fluent_support/parse_smartcode_data', $data['content'], [
34 'customer' => $ticket->customer,
35 'agent' => $person
36 ]);
37 }
38
39 $convoType = Arr::get($data, 'conversation_type', 'response');
40
41 // Input is already unslashed (or was never slashed, for API/MCP callers).
42 // Don't unslash again or literal backslashes are lost.
43 $content = wp_kses_post($data['content']);
44 $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content);
45 $content = apply_filters('fluent_support/response_content_before_use_anywhere', $content);
46 $response = [
47 'person_id' => $person->id,
48 'ticket_id' => $ticket->id,
49 'conversation_type' => $convoType,
50 'content' => $content,
51 'source' => Arr::get($data, 'source', 'web'),
52 'content_hash' => md5($content)
53 ];
54
55 if (!empty($data['message_id'])) {
56 $response['message_id'] = sanitize_text_field($data['message_id']);
57 }
58
59 $response = apply_filters('fluent_support/new_' . $person->person_type . '_' . $convoType, $response, $ticket, $person);
60
61 $createdResponse = \FluentSupport\App\Models\Conversation::create($response);
62
63 if (!empty($cc_emails)) {
64 $createdResponse->updateSettingsValue('cc_email', $cc_emails);
65
66 // Keep a running list of every CC'd address on the ticket.
67 $existingCcEmails = $ticket->getSettingsValue('all_cc_email', []);
68 $newData = array_merge($existingCcEmails, $cc_emails);
69 $ticket->updateSettingsValue('all_cc_email', array_unique($newData));
70 }
71
72 $createdResponse->load('person');
73
74 if ($person->person_type == 'agent' && $ticket->status == 'new' && $convoType == 'response') {
75 $ticket->status = 'active';
76 if ($ticket->created_at) {
77 $ticket->first_response_time = strtotime(current_time('mysql')) - strtotime($ticket->created_at);
78 } else {
79 $ticket->first_response_time = 300;
80 }
81 }
82
83 $agentAdded = false;
84 $previousAgentId = (int) $ticket->agent_id;
85 $updateData = [];
86
87 if ($person->person_type == 'agent') {
88 if (!$ticket->agent_id && $convoType == 'response') {
89 $ticket->agent_id = $person->id;
90 $agentAdded = true;
91 $updateData = [
92 'agent_id' => $person->id
93 ];
94 }
95
96 if ($convoType == 'response') {
97 if ($resetWaitingSince && !$informationalReply) {
98 $ticket->last_agent_response = current_time('mysql');
99 $ticket->waiting_since = current_time('mysql');
100 }
101 }
102 } else {
103
104 if ($ticket->last_agent_response && strtotime($ticket->last_agent_response) > strtotime($ticket->last_customer_response)) {
105 $ticket->waiting_since = current_time('mysql');
106 }
107
108 $ticket->last_customer_response = current_time('mysql');
109 }
110
111 if ($convoType == 'response') {
112 $ticket->response_count += 1;
113 }
114
115 $closed = false;
116 if (Arr::get($data, 'close_ticket') == 'yes' && $ticket->status != 'closed') {
117 $ticket->status = 'closed';
118 $ticket->resolved_at = current_time('mysql');
119 $ticket->closed_by = $person->id;
120 $ticket->total_close_time = current_time('timestamp') - strtotime($ticket->created_at);
121 $closed = true;
122
123 $internalNote = __('Ticket has been closed', 'fluent-support');
124
125 $internalNote = apply_filters('fluent_support/ticket_close_internal_note', $internalNote, $ticket);
126
127 if ($internalNote) {
128 Conversation::create([
129 'ticket_id' => $ticket->id,
130 'person_id' => $person->id,
131 'conversation_type' => 'internal_info',
132 'content' => $internalNote
133 ]);
134 }
135 }
136
137 $ticket->save();
138
139 Helper::tempImageMoveUploadDir($ticket->id, 'conversation', $createdResponse->id, $person->id);
140
141 // Finalize files uploaded ahead of the response (status in-active).
142 if ($attachmentHashes = Arr::get($data, 'attachments', [])) {
143 $attachments = Attachment::where('ticket_id', $ticket->id)
144 ->whereIn('file_hash', $attachmentHashes)
145 ->where('status', 'in-active')
146 ->get();
147
148 if (!$attachments->isEmpty()) {
149 $storageDriver = Helper::getUploadDriverKey();
150 foreach ($attachments as $attachment) {
151 if ($storageDriver != 'local') {
152 do_action_ref_array('fluent_support/finalize_file_upload_' . $storageDriver, [&$attachment, $ticket->id]);
153 }
154
155 if ($attachment->driver == 'local') {
156 $newFileInfo = UploadService::copyFileTicketFolder($attachment->file_path, $ticket->id);
157 if ($newFileInfo && !empty($newFileInfo['file_path'])) {
158 $attachment->file_path = $newFileInfo['file_path'];
159 $attachment->full_url = $newFileInfo['url'];
160 }
161 }
162
163 $attachment->ticket_id = $ticket->id;
164 $attachment->person_id = $createdResponse->person_id;
165 $attachment->conversation_id = $createdResponse->id;
166 $attachment->status = 'active';
167 $attachment->save();
168 }
169 $createdResponse->load('attachments');
170 }
171 }
172
173 if ($silently) {
174 // The caller fires the hooks itself (fireResponseHooks()), e.g. after
175 // its transaction commits; 'closed' / 'agent_added' tell it what happened.
176 return [
177 'response' => $createdResponse,
178 'ticket' => $ticket,
179 'update_data' => $updateData,
180 'closed' => $closed,
181 'agent_added' => $agentAdded,
182 'previous_agent_id' => $previousAgentId,
183 ];
184 }
185
186 $updateData = $this->fireResponseHooks($createdResponse, $ticket, $person, $closed, $agentAdded, $previousAgentId, $updateData);
187
188 return [
189 'response' => $createdResponse,
190 'ticket' => $ticket,
191 'update_data' => $updateData
192 ];
193 }
194
195 /**
196 * Fire the hooks createResponse() fires after a response is stored.
197 *
198 * Split out so a caller that wrapped createResponse(..., $silently = true) in
199 * a transaction can fire them once the rows are committed: the customer
200 * email must not leave for a reply that is about to be rolled back.
201 *
202 * @param Conversation $createdResponse
203 * @param \FluentSupport\App\Models\Ticket $ticket
204 * @param \FluentSupport\App\Models\Person $person
205 * @param bool $closed The response closed the ticket.
206 * @param bool $agentAdded The response assigned the ticket to $person.
207 * @param int $previousAgentId
208 * @param array $updateData
209 * @return array $updateData, with 'agent' when the assignment hook fired.
210 */
211 public function fireResponseHooks($createdResponse, $ticket, $person, $closed, $agentAdded = false, $previousAgentId = 0, $updateData = [])
212 {
213 $convoType = $createdResponse->conversation_type;
214
215 if ($agentAdded) {
216 $assigner = Helper::getCurrentAgent();
217 $ticket->load('agent');
218
219 /*
220 * Fires when a ticket is assigned to an agent.
221 *
222 * @since v1.0.0
223 * @param object $person
224 * @param object $ticket
225 * @param object $assigner
226 * @param int $previousAgentId
227 */
228 do_action('fluent_support/agent_assigned_to_ticket', $person, $ticket, $assigner, $previousAgentId);
229 $updateData['agent'] = $ticket->agent;
230 }
231
232 /*
233 * Fires after a response or note is added.
234 *
235 * @since v1.0.0
236 * @param string $convoType
237 * @param string $personType
238 * @param object $createdResponse
239 * @param object $ticket
240 * @param object $person
241 */
242 do_action('fluent_support/' . $convoType . '_added_by_' . $person->person_type, $createdResponse, $ticket, $person);
243
244
245 if ($closed) {
246 /*
247 * Fires when a ticket is closed.
248 *
249 * @since v1.0.0
250 * @param object $ticket
251 * @param object $person
252 */
253 do_action('fluent_support/ticket_closed', $ticket, $person);
254
255 /*
256 * Fires when a ticket is closed, per closer type.
257 *
258 * @since v1.0.0
259 * @param string $personType
260 * @param object $ticket
261 * @param object $person
262 */
263 do_action('fluent_support/ticket_closed_by_' . $person->person_type, $ticket, $person);
264 }
265
266 return $updateData;
267 }
268
269 /**
270 * Publish a draft reply: it becomes a sent response and the customer is notified.
271 * Shared by the admin "Approve" action and MCP's publish-draft-reply.
272 *
273 * @param \FluentSupport\App\Models\Ticket $ticket
274 * @param \FluentSupport\App\Models\Conversation $response The draft.
275 * @param \FluentSupport\App\Models\Person $person The agent publishing it.
276 * @param string $content Final content, already sanitized.
277 * @return \FluentSupport\App\Models\Conversation
278 */
279 public function publishDraft($ticket, $response, $person, $content)
280 {
281 $resetWaitingSince = apply_filters('fluent_support/reset_waiting_since', true, $content);
282
283 $response->content = $content;
284 $response->conversation_type = 'response';
285 $response->created_at = current_time('mysql');
286 $response->save();
287
288 if ($person->person_type == 'agent' && $ticket->status == 'new') {
289 $ticket->status = 'active';
290 if ($ticket->created_at) {
291 $ticket->first_response_time = strtotime(current_time('mysql')) - strtotime($ticket->created_at);
292 } else {
293 $ticket->first_response_time = 300;
294 }
295 }
296
297 if ($resetWaitingSince) {
298 $ticket->last_agent_response = current_time('mysql');
299 $ticket->waiting_since = current_time('mysql');
300 }
301
302 $ticket->response_count += 1;
303 $ticket->save();
304
305 $author = $response->person_id ? Agent::find($response->person_id) : null;
306 $sender = $author ?: $person;
307
308 do_action('fluent_support/response_added_by_' . $sender->person_type, $response, $ticket, $sender);
309
310 return $response;
311 }
312
313 /**
314 * @deprecated 2.4.1 Replies no longer carry the signature. Email templates add it
315 * with {{agent.signature}}, so the template decides the sign-off.
316 */
317 public function maybeAppendSignature($content, $person)
318 {
319 $signature = self::getAppendedSignature($person);
320
321 if ($signature === '') {
322 return $content;
323 }
324
325 return $content . '<div class="fs_agent_signature">' . $signature . '</div>';
326 }
327
328 /**
329 * The signature appended to this agent's replies, or '' when none is.
330 *
331 * @param \FluentSupport\App\Models\Person $person
332 * @return string HTML
333 */
334 public static function getAppendedSignature($person)
335 {
336 if (!$person || $person->getMeta('agent_signature_enabled', 'no') !== 'yes') {
337 return '';
338 }
339
340 $signature = (string) $person->getMeta('agent_signature', '');
341 $hasText = !empty(trim(strip_tags($signature)));
342 $hasImage = (bool) preg_match('/<img[\s>]/i', $signature);
343
344 return ($hasText || $hasImage) ? $signature : '';
345 }
346 }
347