PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 1.7.2
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v1.7.2
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
fluent-booking / app / Services / BookingService.php

BookingService.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 1.7.2, at app/Services/BookingService.php

451 lines 16.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBooking\App\Services;
4
5 use FluentBooking\App\App;
6 use FluentBooking\App\Models\Booking;
7 use FluentBooking\App\Models\CalendarSlot;
8 use FluentBooking\Framework\Support\Arr;
9
10 class BookingService
11 {
12 public static function createBooking($data = [], $calendarSlot = null, $customFieldsData = [])
13 {
14 if (empty($data['email']) || empty($data['start_time']) || empty($data['person_time_zone'])) {
15 throw new \Exception(esc_html__('Email, Start Time and timezone are required to create a booking', 'fluent-booking'), 422);
16 }
17
18 if (!$calendarSlot) {
19 $calendarSlot = CalendarSlot::findOrFail($data['event_id']);
20 }
21
22 $defaults = [
23 'event_id' => $calendarSlot->id,
24 'calendar_id' => $calendarSlot->calendar_id,
25 'host_user_id' => $calendarSlot->user_id
26 ];
27
28 $data = self::prepareBookingData($data, $calendarSlot);
29
30 $guests = Arr::get($data, 'additional_guests', []);
31
32 $bookingData = Arr::only(wp_parse_args($data, $defaults), (new Booking())->getFillable());
33
34 $bookingData['group_id'] = self::getGroupId($calendarSlot, $bookingData);
35
36 $bookingData['event_type'] = $calendarSlot->event_type;
37
38 $bookingData = apply_filters('fluent_booking/booking_data', $bookingData, $calendarSlot, $customFieldsData);
39
40 if (is_wp_error($bookingData)) {
41 return $bookingData;
42 }
43
44 return self::createSingleOrMultiBooking($bookingData, $calendarSlot, $customFieldsData, $guests);
45 }
46
47 public static function createSingleOrMultiBooking($bookingData, $calendarSlot, $customFieldsData, $guests = [], $bookingIds = [])
48 {
49 if (is_array($bookingData['start_time'])) {
50 return self::createMultiTimeBooking($bookingData, $calendarSlot, $customFieldsData, $guests);
51 }
52
53 if (is_array($bookingData['email'])) {
54 return self::createMultiGuestBooking($bookingData, $calendarSlot, $customFieldsData);
55 }
56
57 do_action('fluent_booking/before_booking', $bookingData, $calendarSlot);
58
59 $booking = Booking::create($bookingData);
60
61 self::attachHosts($booking, $calendarSlot);
62 self::updateParentInfo($booking, $bookingIds);
63 self::updateMetas($booking, $bookingData, $guests, $customFieldsData);
64
65 $booking->load('calendar');
66
67 // this pre hook is for early actions that require for remote calendars and locations
68 do_action('fluent_booking/pre_after_booking_' . $booking->status, $booking, $calendarSlot, $bookingData);
69
70 // We are just renewing this as this may have been changed by the pre hook
71 $booking = Booking::find($booking->id);
72 do_action('fluent_booking/after_booking_' . $booking->status, $booking, $calendarSlot, $bookingData);
73
74 return $booking;
75 }
76
77 public static function createMultiTimeBooking($data, $calendarSlot, $customFieldsData, $guests)
78 {
79 $booking = [];
80 $bookingIds = [];
81 $createdBookingIds = [];
82 $lastBooking = end($data['start_time']);
83 $totalBooking = count($data['start_time']);
84 $bookingTimes = array_combine($data['start_time'], $data['end_time']);
85
86 foreach ($bookingTimes as $startTime => $endTime) {
87 $bookingData = $data;
88
89 $bookingData['start_time'] = $startTime;
90 $bookingData['end_time'] = $endTime;
91
92 $isConfRequired = $calendarSlot->isConfirmationRequired($startTime);
93 $bookingData['status'] = $isConfRequired ? 'pending' : $data['status'];
94
95 if ($startTime == $lastBooking) {
96 $createdBookingIds = $bookingIds;
97 }
98
99 if (Arr::get($data, 'payment_method')) {
100 if ($startTime == $lastBooking) {
101 $bookingData['quantity'] = $totalBooking;
102 } else {
103 $bookingData['payment_status'] = '';
104 $bookingData['payment_method'] = '';
105 }
106 }
107
108 $booking = self::createSingleOrMultiBooking($bookingData, $calendarSlot, $customFieldsData, $guests, $createdBookingIds);
109
110 $bookingIds[] = $booking->id;
111 }
112
113 return $booking;
114 }
115
116 public static function createMultiGuestBooking($data, $calendarSlot, $customFieldsData)
117 {
118 $booking = [];
119 $bookingIds = [];
120 $createdBookingIds = [];
121 $lastBooking = end($data['email']);
122 $totalBooking = count($data['email']);
123 $guests = array_combine($data['email'], $data['first_name']);
124
125 foreach ($guests as $email => $name) {
126 $bookingData = $data;
127
128 $bookingData['email'] = $email;
129
130 $nameArray = explode(' ', trim($name));
131 $bookingData['first_name'] = array_shift($nameArray);
132 $bookingData['last_name'] = implode(' ', $nameArray);
133
134 if ($user = get_user_by('email', $email)) {
135 $bookingData['person_user_id'] = $user->ID;
136 }
137
138 $bookingData['group_id'] = self::getGroupId($calendarSlot, $bookingData);
139
140 if ($email == $lastBooking) {
141 $createdBookingIds = $bookingIds;
142 }
143
144 if (Arr::get($data, 'payment_method')) {
145 if ($email == $lastBooking) {
146 $bookingData['quantity'] = $totalBooking;
147 } else {
148 $bookingData['payment_status'] = '';
149 $bookingData['payment_method'] = '';
150 }
151 }
152
153 $booking = self::createSingleOrMultiBooking($bookingData, $calendarSlot, $customFieldsData, [], $createdBookingIds);
154
155 $bookingIds[] = $booking->id;
156 }
157
158 return $booking;
159 }
160
161 private static function prepareBookingData($data, $calendarSlot)
162 {
163 if (empty($data['first_name']) && !empty($data['name'])) {
164 $nameArray = explode(' ', trim($data['name']));
165 $data['first_name'] = array_shift($nameArray);
166 $data['last_name'] = implode(' ', $nameArray);
167 }
168
169 if (empty($data['slot_minutes'])) {
170 $data['slot_minutes'] = $calendarSlot->duration;
171 }
172
173 if (empty($data['end_time'])) {
174 $data['end_time'] = gmdate('Y-m-d H:i:s', strtotime($data['start_time']) + ($data['slot_minutes'] * 60));
175 }
176
177 if (!isset($data['person_user_id'])) {
178 $user = get_user_by('email', $data['email']);
179 if ($user) {
180 $data['person_user_id'] = $user->ID;
181 if (empty($data['first_name'])) {
182 $data['first_name'] = $user->first_name;
183 $data['last_name'] = $user->last_name;
184 }
185 }
186 }
187
188 if (empty($data['location_details'])) {
189 $data['location_details'] = LocationService::getLocationDetails($calendarSlot, [], []);
190 }
191
192 if ($additionalGuests = Arr::get($data, 'additional_guests', [])) {
193 if ($calendarSlot->isMultiGuestEvent()) {
194 $guestEmails = array_map(function ($guest) {
195 return $guest['email'];
196 }, $additionalGuests);
197 $data['email'] = array_merge($guestEmails, (array) $data['email']);
198
199 $guestNames = array_map(function ($guest) {
200 return $guest['name'];
201 }, $additionalGuests);
202 $data['first_name'] = array_merge($guestNames, (array) $data['first_name']);
203
204 $data['additional_guests'] = [];
205 }
206 }
207
208 return $data;
209 }
210
211 private static function attachHosts($booking, $calendarSlot)
212 {
213 $hosts = [$booking->host_user_id];
214 if ($calendarSlot->isMultiHostsEvent()) {
215 $hosts = $calendarSlot->getHostIds();
216 }
217
218 $hostData = [];
219 foreach ($hosts as $hostId) {
220 $hostData[$hostId] = ['status' => 'confirmed'];
221 }
222
223 $booking->hosts()->attach($hostData);
224 }
225
226 protected static function getGroupId($calendarSlot, $bookingData)
227 {
228 if (!$calendarSlot->isMultiGuestEvent()) {
229 return null;
230 }
231
232 $event = Booking::select('group_id')
233 ->where('event_id', $calendarSlot->id)
234 ->where('calendar_id', $calendarSlot->calendar_id)
235 ->where('start_time', $bookingData['start_time'])
236 ->first();
237
238 return $event ? $event->group_id : null;
239 }
240
241 private static function updateMetas($booking, $bookingData, $guests, $customFieldsData)
242 {
243 if ($customFieldsData) {
244 Helper::updateBookingMeta($booking->id, 'custom_fields_data', $customFieldsData);
245 }
246
247 if ($guests) {
248 Helper::updateBookingMeta($booking->id, 'additional_guests', $guests);
249 }
250
251 if ($quantity = Arr::get($bookingData, 'quantity')) {
252 Helper::updateBookingMeta($booking->id, 'quantity', $quantity);
253 }
254 }
255
256 private static function updateParentInfo($booking, $bookingIds)
257 {
258 if (!$bookingIds) {
259 return;
260 }
261
262 Booking::whereIn('id', $bookingIds)->update(['parent_id' => $booking->id]);
263 }
264
265 public static function getBookingConfirmationHtml(Booking $booking, $actionType = 'confirmation')
266 {
267 $validActions = [
268 'confirmation',
269 'cancel',
270 'reschedule'
271 ];
272
273 if (!in_array($actionType, $validActions)) {
274 $actionType = 'confirmation';
275 }
276
277 $calendarSlot = $booking->calendar_event;
278
279 $author = $booking->getHostDetails(false);
280
281 $bookingTitle = $booking->getBookingTitle();
282
283 $sections = [
284 'what' => [
285 'title' => __('What', 'fluent-booking'),
286 'content' => $bookingTitle
287 ],
288 'when' => [
289 'title' => __('When', 'fluent-booking'),
290 'content' => $booking->getFullBookingDateTimeText($booking->person_time_zone, true) . ' (' . $booking->person_time_zone . ')'
291 ],
292 'who' => [
293 'title' => __('Who', 'fluent-booking'),
294 'content' => $booking->getHostAndGuestDetailsHtml()
295 ],
296 'where' => [
297 'title' => __('Where', 'fluent-booking'),
298 'content' => $booking->getLocationDetailsHtml()
299 ]
300 ];
301
302 if ($guests = $booking->getAdditionalGuests(true)) {
303 $sections['guests'] = [
304 'title' => __('Additional Guests', 'fluent-booking'),
305 'content' => $guests
306 ];
307 }
308
309 if ($booking->status == 'cancelled') {
310 // add cancellation reason at the beginning
311 $sections = array_merge([
312 'cancellation_reason' => [
313 'title' => __('Cancellation Reason', 'fluent-booking'),
314 'content' => $booking->getCancelReason(false, true)
315 ]
316 ], $sections);
317 }
318
319 if ($booking->status == 'rejected') {
320 // add rejection reason at the beginning
321 $sections = array_merge([
322 'cancellation_reason' => [
323 'title' => __('Rejection Reason', 'fluent-booking'),
324 'content' => $booking->getRejectReason(false, true)
325 ]
326 ], $sections);
327 }
328
329 if ($booking->message) {
330 $sections['note'] = [
331 'title' => __('Additional Note', 'fluent-booking'),
332 'content' => wpautop($booking->message)
333 ];
334 }
335
336 $customFieldsData = $booking->getCustomFormData(true, true);
337
338 foreach ($customFieldsData as $dataKey => $data) {
339 if (!empty($data['value'])) {
340 $sections[$dataKey] = [
341 'title' => $data['label'],
342 'content' => $data['value']
343 ];
344 }
345 }
346
347 $bookingStatus = $booking->getBookingStatus();
348
349 $subHeading = '';
350 if ($booking->status == 'scheduled') {
351 // translators: %s is the name of the person scheduled
352 $subHeading = sprintf(__('You are scheduled with %s', 'fluent-booking'), $author['name']);
353
354 if ($actionType == 'confirmation' && $calendarSlot->allowMultiBooking()) {
355 $bookingTime = (array) $sections['when']['content'];
356 $sections['when']['content'] = array_merge($bookingTime, $booking->getOtherBookingTimes());
357 }
358 }
359
360 // translators: %s is the status of the meeting
361 $title = sprintf(__('Your meeting has been %s', 'fluent-booking'), $bookingStatus);
362 if ($booking->status == 'pending' && $booking->payment_status != 'pending') {
363 $title = __('Your booking has been submitted', 'fluent-booking');
364 $subHeading = __('Please wait for the host to confirm your booking', 'fluent-booking');
365 }
366
367 $assetsUrl = App::getInstance('url.assets');
368
369 $confirmationData = [
370 'author' => $author,
371 'title' => $title,
372 'sub_heading' => $subHeading,
373 'sections' => $sections,
374 'slot' => $calendarSlot,
375 'booking' => $booking,
376 'message' => __('A confirmation has been sent to your email address along with meeting location details.', 'fluent-booking'),
377 'action_type' => $actionType,
378 'can_cancel' => $booking->canCancel(),
379 'bookmarks' => [],
380 'confirm_icon' => '',
381 'action_url' => '',
382 'extra_html' => ''
383 ];
384
385 if ($booking->payment_status) {
386 $confirmationData['extra_html'] = EditorShortCodeParser::parse('{{payment.receipt_html}}', $booking);
387 }
388
389 if ($actionType == 'cancel') {
390 $confirmationData['title'] = __('Booking Cancellation', 'fluent-booking');
391 $confirmationData['sub_heading'] = __('Confirm and cancel the scheduled booking', 'fluent-booking');
392 $confirmationData['cancel_field'] = BookingFieldService::getBookingFieldByName($calendarSlot, 'cancellation_reason');
393 $confirmationData['action_url'] = add_query_arg([
394 'action' => 'fcal_cancel_meeting',
395 'meeting_hash' => $booking->hash,
396 'scope' => Arr::get($_REQUEST, 'scope') // phpcs:ignore WordPress.Security.NonceVerification.Recommended
397 ], admin_url('admin-ajax.php'));
398 }
399
400 if ($booking->status == 'scheduled' && $actionType == 'confirmation') {
401 $confirmationData['confirm_icon'] = $assetsUrl . '/images/check-mark.png';
402 $confirmationData['bookmarks'] = $booking->getMeetingBookmarks($assetsUrl);
403 }
404
405 if ($booking->status == 'cancelled' || $booking->status == 'rejected') {
406 $confirmationData['confirm_icon'] = $assetsUrl . '/images/cancel-mark.png';
407 }
408
409 $confirmationData = apply_filters('fluent_booking/schedule_receipt_data', $confirmationData, $booking);
410
411 return (string)App::make('view')->make('public.booking_confirmation', $confirmationData);
412 }
413
414 public static function generateBookingICS(Booking $booking)
415 {
416 $author = $booking->getHostDetails(false);
417
418 // Initialize the ICS content
419 $icsContent = "BEGIN:VCALENDAR\r\n";
420 $icsContent .= "VERSION:2.0\r\n";
421 $icsContent .= "PRODID:-//Google Inc//Fluent Booking//EN\r\n";
422 $icsContent .= "METHOD:REQUEST\r\n";
423 $icsContent .= "STATUS:CONFIRMED\r\n";
424
425 $icsContent .= "BEGIN:VEVENT\r\n";
426 $icsContent .= "UID:" . md5($booking->hash) . "\r\n"; // Unique ID for the event
427
428 // Event details
429 $icsContent .= "SUMMARY:" . $booking->getBookingTitle() . "\r\n";
430 $icsContent .= "DESCRIPTION:" . $booking->getIcsBookingDescription() . "\r\n";
431
432 // Date and time formatting (assuming eventStart and eventEnd are DateTime objects)
433 $icsContent .= "DTSTART:" . gmdate('Ymd\THis\Z', strtotime($booking->start_time)) . "\r\n";
434 $icsContent .= "DTEND:" . gmdate('Ymd\THis\Z', strtotime($booking->end_time)) . "\r\n";
435
436 $icsContent .= "LOCATION:" . $booking->getLocationAsText() . "\r\n";
437
438 $icsContent .= "ORGANIZER;CN=\"" . $author['name'] . "\":mailto:" . $author['email'] . "\r\n";
439
440 $icsContent .= "ATTENDEE;CN=\"" . $booking->email . "\";ROLE=REQ-PARTICIPANT;RSVP=TRUE;PARTSTAT=ACCEPTED:mailto:" . $booking->email . "\r\n";
441
442 $icsContent .= "END:VEVENT\r\n";
443
444 // Close the VCALENDAR component
445 $icsContent .= "END:VCALENDAR\r\n";
446
447 return $icsContent;
448 }
449
450 }
451