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 / Integrations / FluentBooking / FluentBookingService.php

FluentBookingService.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.4.5, at app/Services/Integrations/FluentBooking/FluentBookingService.php

399 lines 13.4 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\Integrations\FluentBooking;
4
5 use FluentSupport\App\Models\Conversation;
6 use FluentSupport\App\Models\Ticket;
7 use FluentSupport\App\Services\Helper;
8 use FluentSupport\Framework\Support\Arr;
9
10 class FluentBookingService
11 {
12
13 /**
14 * @return void
15 */
16 public function init()
17 {
18 add_action('fluent_booking/after_booking_scheduled', [$this, 'handleBookingScheduled'], 20, 3);
19 add_action('fluent_support/response_added_by_agent', [$this, 'promoteGeneratedBookingLinksFromResponse'], 20, 3);
20 }
21
22 /**
23 * Pass $eventTypes if already loaded to skip the query.
24 *
25 * @param array|null $eventTypes
26 * @return array
27 */
28 public function getStatus($eventTypes = null)
29 {
30 $active = $this->isActive();
31 $eventTypes = is_array($eventTypes) ? $eventTypes : ($active ? $this->getEventTypes() : []);
32
33 return [
34 'active' => $active,
35 'configured' => $active && !empty($eventTypes),
36 'admin_url' => esc_url_raw(admin_url('admin.php?page=fluent-booking#/')),
37 'message' => $active
38 ? ''
39 : __('FluentBooking is not installed or active.', 'fluent-support')
40 ];
41 }
42
43 /**
44 * Active event types whose landing page is public, for the frontend selector.
45 *
46 * @return array
47 */
48 public function getEventTypes()
49 {
50 if (!$this->isActive()) {
51 return [];
52 }
53
54 $events = \FluentBooking\App\Models\CalendarSlot::query()
55 ->with('calendar')
56 ->where('status', 'active')
57 ->orderBy('id', 'DESC')
58 ->get();
59
60 $eventTypes = [];
61
62 foreach ($events as $event) {
63 if (!$event->calendar || !self::canUseEvent($event)) {
64 continue;
65 }
66
67 $eventTypes[] = [
68 'id' => (int) $event->id,
69 'title' => sanitize_text_field($event->title),
70 'duration' => (int) $event->duration,
71 'calendar_title' => sanitize_text_field($event->calendar->title),
72 ];
73 }
74
75 return $eventTypes;
76 }
77
78 /**
79 * Build the reply content (HTML and plain text) with a tokenised booking link.
80 * Selected slots are re-checked against live availability.
81 *
82 * @param Ticket $ticket
83 * @param int $eventId
84 * @param string $message
85 * @param array $selectedSlots
86 * @param string $timezone
87 * @throws \Exception
88 * @return array
89 */
90 public function createBookingLink(Ticket $ticket, $eventId, $message = '', $selectedSlots = [], $timezone = '')
91 {
92 if (!$this->isActive()) {
93 throw new \Exception(esc_html__('FluentBooking is not installed or active.', 'fluent-support'));
94 }
95
96 $event = \FluentBooking\App\Models\CalendarSlot::with('calendar')->find($eventId);
97
98 if (!$event || !$event->calendar || !self::canUseEvent($event)) {
99 throw new \Exception(esc_html__('Selected FluentBooking event type is not available.', 'fluent-support'));
100 }
101
102 $linkToken = bin2hex(random_bytes(16));
103 $bookingUrl = self::addBookingLinkToken(self::getEventUrl($event, $ticket), $linkToken);
104 $message = trim(wp_unslash($message));
105 $availability = new BookingAvailabilityHelper();
106
107 $selectedSlots = $availability->sanitizeSelectedSlots($selectedSlots, $event, $ticket, (int) $event->getDuration());
108 $selectedSlots = $availability->addTokenToSlots($selectedSlots, $linkToken);
109 $slotsHtml = $availability->formatSlotsHtml($selectedSlots, $event, $timezone);
110
111 $htmlParts = [];
112
113 if ($message !== '') {
114 $htmlParts[] = '<p>' . esc_html($message) . '</p>';
115 }
116
117 if ($slotsHtml) {
118 $htmlParts[] = $slotsHtml;
119 }
120
121 $htmlParts[] = '<p><a href="' . esc_url($bookingUrl) . '" target="_blank" rel="noopener">' . esc_html__('See all available times', 'fluent-support') . '</a></p>';
122
123 return [
124 'html' => wp_kses_post(implode('', $htmlParts)),
125 'plain_text' => $availability->formatSlotsPlainText($message, $selectedSlots, $bookingUrl),
126 ];
127 }
128
129 /**
130 * Available slots for the event in the given range, grouped by day.
131 *
132 * @param int $eventId
133 * @param string $range
134 * @param string $timezone
135 * @param int|null $duration
136 * @param Ticket|null $ticket
137 * @param array $selectedDates
138 * @param string $calendarMonth
139 * @throws \Exception
140 * @return array
141 */
142 public function getAvailabilitySlots($eventId, $range = 'next_3_days', $timezone = '', $duration = null, ?Ticket $ticket = null, $selectedDates = [], $calendarMonth = '')
143 {
144 if (!$this->isActive()) {
145 throw new \Exception(esc_html__('FluentBooking is not installed or active.', 'fluent-support'));
146 }
147
148 $event = \FluentBooking\App\Models\CalendarSlot::with('calendar')->find($eventId);
149
150 if (!$event || !$event->calendar || !self::canUseEvent($event)) {
151 throw new \Exception(esc_html__('Selected FluentBooking event type is not available.', 'fluent-support'));
152 }
153
154 $availability = new BookingAvailabilityHelper();
155 $timezone = $availability->sanitizeTimezone($timezone, $event->calendar->author_timezone);
156 $rangeData = $availability->getAvailabilityRange($range, $timezone, $selectedDates, $calendarMonth);
157
158 $days = array_map(function ($day) {
159 $day['slots'] = array_map(function ($slot) {
160 return [
161 'id' => $slot['id'],
162 'start' => $slot['start'],
163 'time_label' => $slot['time_label'],
164 ];
165 }, $day['slots']);
166
167 return $day;
168 }, $availability->fetchFormattedDays($event, $rangeData, $timezone, $ticket, (int) $event->getDuration($duration)));
169
170 return [
171 'timezone' => $timezone,
172 'max_lookup_date' => $event->getMaxLookUpDate(),
173 'days' => $days,
174 ];
175 }
176
177 /**
178 * Upcoming and past bookings for the ticket's customer, scoped to the
179 * events and dates of the ticket's stored links when there are any.
180 *
181 * @param Ticket $ticket
182 * @return array
183 */
184 public function getTicketMeetings(Ticket $ticket)
185 {
186 $links = new BookingLinkManager();
187 $generatedLinks = $links->getLinksForTicket($ticket);
188
189 $meetings = [
190 'upcoming' => [],
191 'past' => [],
192 'message' => '',
193 ];
194
195 if (!$this->isActive() || !$ticket->customer || !$ticket->customer->email) {
196 $meetings['message'] = __('FluentBooking is not installed or the ticket customer has no email address.', 'fluent-support');
197 return $meetings;
198 }
199
200 $eventIds = array_values(array_unique(array_filter(array_column($generatedLinks, 'event_type_id'))));
201 $earliestLinkDate = $links->getEarliestLinkDate($generatedLinks);
202
203 $bookingQuery = \FluentBooking\App\Models\Booking::query()
204 ->with(['calendar', 'calendar_event'])
205 ->where('email', sanitize_email($ticket->customer->email))
206 ->where('status', '!=', 'reserved');
207
208 if ($eventIds) {
209 $bookingQuery->whereIn('event_id', $eventIds);
210 }
211
212 if ($earliestLinkDate) {
213 $bookingQuery->where('created_at', '>=', $earliestLinkDate);
214 }
215
216 $bookings = $bookingQuery->orderBy('start_time', 'DESC')->limit(10)->get();
217
218 foreach ($bookings as $booking) {
219 $formatted = $links->formatBooking($booking);
220
221 if (strtotime($booking->end_time) >= time() && $booking->status === 'scheduled') {
222 $meetings['upcoming'][] = $formatted;
223 } else {
224 $meetings['past'][] = $formatted;
225 }
226 }
227
228 usort($meetings['upcoming'], function ($a, $b) {
229 return strtotime($a['start_time']) <=> strtotime($b['start_time']);
230 });
231
232 if (!$bookings->count() && $generatedLinks) {
233 $meetings['message'] = __('No meeting booked yet from these links.', 'fluent-support');
234 } elseif (!$bookings->count()) {
235 $meetings['message'] = __('No FluentBooking meetings found for this customer.', 'fluent-support');
236 }
237
238 return $meetings;
239 }
240
241 /**
242 * Add a timeline note when the customer books through a link sent from the ticket.
243 *
244 * @param object $booking
245 * @param object $calendarSlot
246 * @param array $bookingData
247 * @return void
248 */
249 public function handleBookingScheduled($booking, $calendarSlot, $bookingData = [])
250 {
251 $links = new BookingLinkManager();
252 $ticket = $links->getTicketFromBooking($booking, $bookingData);
253
254 if (!$ticket || $links->hasTimelineNote($booking)) {
255 return;
256 }
257
258 $formatted = $links->formatBooking($booking);
259 $personId = (int) $ticket->agent_id ?: (int) $ticket->customer_id;
260 $eventTitle = Arr::get($formatted, 'event_title') ?: __('Meeting', 'fluent-support');
261
262 $content = sprintf(
263 '<strong>%1$s</strong><br>%2$s<br>%3$s',
264 esc_html__('Meeting scheduled via FluentBooking', 'fluent-support'),
265 esc_html($eventTitle),
266 esc_html(Arr::get($formatted, 'date_text'))
267 );
268
269 if ($timezone = Arr::get($formatted, 'timezone')) {
270 $content .= '<br><span>' . esc_html($timezone) . '</span>';
271 }
272
273 if ($adminUrl = Arr::get($formatted, 'admin_url')) {
274 $content .= '<br><a href="' . esc_url($adminUrl) . '" target="_blank" rel="noopener">' . esc_html__('View booking', 'fluent-support') . '</a>';
275 }
276
277 Conversation::create([
278 'ticket_id' => $ticket->id,
279 'person_id' => $personId,
280 'message_id' => 'fluent_booking_scheduled_' . (int) $booking->id,
281 'conversation_type' => 'internal_info',
282 'source' => 'fluent_booking',
283 'content' => wp_kses_post($content)
284 ]);
285 }
286
287 /**
288 * Store any tokenised booking links found in an agent response.
289 *
290 * @param object $response
291 * @param Ticket $ticket
292 * @param object $person
293 * @return void
294 */
295 public function promoteGeneratedBookingLinksFromResponse($response, Ticket $ticket, $person)
296 {
297 if (empty($response->content) || $response->conversation_type !== 'response') {
298 return;
299 }
300
301 $links = new BookingLinkManager();
302 $bookingLinks = $links->extractLinksFromContent($response->content);
303
304 if (!$bookingLinks) {
305 return;
306 }
307
308 foreach ($bookingLinks as $bookingLink) {
309 $links->storeLinkFromUrl($ticket, $person, $bookingLink);
310 }
311 }
312
313 /**
314 * FluentBooking is installed and the integration is enabled.
315 *
316 * @return bool
317 */
318 public function isActive()
319 {
320 if (!defined('FLUENT_BOOKING_VERSION')) {
321 return false;
322 }
323 return Helper::getBusinessSettings('enable_fluent_booking_integration', 'yes') === 'yes';
324 }
325
326 /**
327 * The calendar's public landing page is enabled and includes this event.
328 *
329 * @param object $event
330 * @return bool
331 */
332 public static function canUseEvent($event)
333 {
334 $settings = \FluentBooking\App\Services\LandingPage\LandingPageHelper::getSettings($event->calendar, 'public');
335
336 if (Arr::get($settings, 'enabled') !== 'yes') {
337 return false;
338 }
339
340 if (Arr::get($settings, 'show_type') === 'all') {
341 return true;
342 }
343
344 return in_array((int) $event->id, array_map('intval', Arr::get($settings, 'enabled_slots', [])), true);
345 }
346
347 /**
348 * Landing-page URL for an event, prefilled with the customer and ticket ID when given.
349 * Forces getLandingPageUrl() because callers have already checked canUseEvent().
350 *
351 * @param object $event
352 * @param Ticket|null $ticket
353 * @return string
354 */
355 public static function getEventUrl($event, ?Ticket $ticket = null)
356 {
357 $calendarBaseUrl = $event->calendar->getLandingPageUrl(true);
358
359 if (!$calendarBaseUrl) {
360 return '';
361 }
362
363 $url = defined('FLUENT_BOOKING_LANDING_SLUG')
364 ? $calendarBaseUrl . '/' . $event->slug
365 : $calendarBaseUrl . '&event=' . $event->slug;
366
367 if (!$ticket || !$ticket->customer) {
368 return esc_url_raw($url);
369 }
370
371 $customer = $ticket->customer;
372
373 return esc_url_raw(add_query_arg(array_filter([
374 'invitee_name' => sanitize_text_field($customer->full_name),
375 'invitee_email' => sanitize_email($customer->email),
376 'fs_ticket_id' => (int) $ticket->id
377 ]), $url));
378 }
379
380 /**
381 * Add fs_booking_link_token so a booking can be traced back to the link that was sent.
382 *
383 * @param string $url
384 * @param string $linkToken
385 * @return string
386 */
387 public static function addBookingLinkToken($url, $linkToken)
388 {
389 $linkToken = sanitize_key($linkToken);
390
391 if (!$url || !$linkToken) {
392 return esc_url_raw($url);
393 }
394
395 return esc_url_raw(add_query_arg(['fs_booking_link_token' => $linkToken], $url));
396 }
397
398 }
399