PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 1.5.20
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v1.5.20
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.5.20, at app/Services/BookingService.php

444 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 (Arr::get($data, 'payment_method')) {
96 if ($startTime == $lastBooking) {
97 $createdBookingIds = $bookingIds;
98 $bookingData['quantity'] = $totalBooking;
99 } else {
100 $bookingData['payment_status'] = '';
101 $bookingData['payment_method'] = '';
102 }
103 }
104
105 $booking = self::createSingleOrMultiBooking($bookingData, $calendarSlot, $customFieldsData, $guests, $createdBookingIds);
106
107 $bookingIds[] = $booking->id;
108 }
109
110 return $booking;
111 }
112
113 public static function createMultiGuestBooking($data, $calendarSlot, $customFieldsData)
114 {
115 $booking = [];
116 $bookingIds = [];
117 $createdBookingIds = [];
118 $lastBooking = end($data['email']);
119 $totalBooking = count($data['email']);
120 $guests = array_combine($data['email'], $data['first_name']);
121
122 foreach ($guests as $email => $name) {
123 $bookingData = $data;
124
125 $bookingData['email'] = $email;
126
127 $nameArray = explode(' ', trim($name));
128 $bookingData['first_name'] = array_shift($nameArray);
129 $bookingData['last_name'] = implode(' ', $nameArray);
130
131 if ($user = get_user_by('email', $email)) {
132 $bookingData['person_user_id'] = $user->ID;
133 }
134
135 $bookingData['group_id'] = self::getGroupId($calendarSlot, $bookingData);
136
137 if (Arr::get($data, 'payment_method')) {
138 if ($email == $lastBooking) {
139 $createdBookingIds = $bookingIds;
140 $bookingData['quantity'] = $totalBooking;
141 } else {
142 $bookingData['payment_status'] = '';
143 $bookingData['payment_method'] = '';
144 }
145 }
146
147 $booking = self::createSingleOrMultiBooking($bookingData, $calendarSlot, $customFieldsData, [], $createdBookingIds);
148
149 $bookingIds[] = $booking->id;
150 }
151
152 return $booking;
153 }
154
155 private static function prepareBookingData($data, $calendarSlot)
156 {
157 if (empty($data['first_name']) && !empty($data['name'])) {
158 $nameArray = explode(' ', trim($data['name']));
159 $data['first_name'] = array_shift($nameArray);
160 $data['last_name'] = implode(' ', $nameArray);
161 }
162
163 if (empty($data['slot_minutes'])) {
164 $data['slot_minutes'] = $calendarSlot->duration;
165 }
166
167 if (empty($data['end_time'])) {
168 $data['end_time'] = gmdate('Y-m-d H:i:s', strtotime($data['start_time']) + ($data['slot_minutes'] * 60));
169 }
170
171 if (!isset($data['person_user_id'])) {
172 $user = get_user_by('email', $data['email']);
173 if ($user) {
174 $data['person_user_id'] = $user->ID;
175 if (empty($data['first_name'])) {
176 $data['first_name'] = $user->first_name;
177 $data['last_name'] = $user->last_name;
178 }
179 }
180 }
181
182 if (empty($data['location_details'])) {
183 $data['location_details'] = LocationService::getLocationDetails($calendarSlot, [], []);
184 }
185
186 if ($additionalGuests = Arr::get($data, 'additional_guests', [])) {
187 if ($calendarSlot->isMultiGuestEvent()) {
188 $guestEmails = array_map(function ($guest) {
189 return $guest['email'];
190 }, $additionalGuests);
191 $data['email'] = array_merge($guestEmails, (array) $data['email']);
192 $guestNames = array_map(function ($guest) {
193 return $guest['name'];
194 }, $additionalGuests);
195 $data['first_name'] = array_merge($guestNames, (array) $data['first_name']);
196 $data['additional_guests'] = [];
197 }
198 }
199
200 return $data;
201 }
202
203 private static function attachHosts($booking, $calendarSlot)
204 {
205 $hosts = [$booking->host_user_id];
206 if ($calendarSlot->isOneOffEvent()) {
207 $hosts = $calendarSlot->getHostIds();
208 }
209
210 $hostData = [];
211 foreach ($hosts as $hostId) {
212 $hostData[$hostId] = ['status' => 'confirmed'];
213 }
214
215 $booking->hosts()->attach($hostData);
216 }
217
218 protected static function getGroupId($calendarSlot, $bookingData)
219 {
220 if (!$calendarSlot->isMultiGuestEvent()) {
221 return null;
222 }
223
224 $event = Booking::select('group_id')
225 ->where('event_id', $calendarSlot->id)
226 ->where('calendar_id', $calendarSlot->calendar_id)
227 ->where('start_time', $bookingData['start_time'])
228 ->first();
229
230 return $event ? $event->group_id : null;
231 }
232
233 private static function updateMetas($booking, $bookingData, $guests, $customFieldsData)
234 {
235 if ($customFieldsData) {
236 Helper::updateBookingMeta($booking->id, 'custom_fields_data', $customFieldsData);
237 }
238
239 if ($guests) {
240 Helper::updateBookingMeta($booking->id, 'additional_guests', $guests);
241 }
242
243 if ($quantity = Arr::get($bookingData, 'quantity')) {
244 Helper::updateBookingMeta($booking->id, 'quantity', $quantity);
245 }
246 }
247
248 private static function updateParentInfo($booking, $bookingIds)
249 {
250 if (!$bookingIds) {
251 return;
252 }
253
254 Booking::whereIn('id', $bookingIds)->update(['parent_id' => $booking->id]);
255 }
256
257 public static function getBookingConfirmationHtml(Booking $booking, $actionType = 'confirmation')
258 {
259 $validActions = [
260 'confirmation',
261 'cancel',
262 'reschedule'
263 ];
264
265 if (!in_array($actionType, $validActions)) {
266 $actionType = 'confirmation';
267 }
268
269 $calendarSlot = $booking->calendar_event;
270
271 $author = $booking->getHostDetails(false);
272
273 $bookingTitle = $booking->getBookingTitle();
274
275 $sections = [
276 'what' => [
277 'title' => __('What', 'fluent-booking'),
278 'content' => $bookingTitle
279 ],
280 'when' => [
281 'title' => __('When', 'fluent-booking'),
282 'content' => $booking->getFullBookingDateTimeText($booking->person_time_zone, true) . ' (' . $booking->person_time_zone . ')'
283 ],
284 'who' => [
285 'title' => __('Who', 'fluent-booking'),
286 'content' => $booking->getHostAndGuestDetailsHtml()
287 ],
288 'where' => [
289 'title' => __('Where', 'fluent-booking'),
290 'content' => $booking->getLocationDetailsHtml()
291 ]
292 ];
293
294 if ($guests = $booking->getAdditionalGuests(true)) {
295 $sections['guests'] = [
296 'title' => __('Additional Guests', 'fluent-booking'),
297 'content' => $guests
298 ];
299 }
300
301 if ($booking->status == 'cancelled') {
302 // add cancellation reason at the beginning
303 $sections = array_merge([
304 'cancellation_reason' => [
305 'title' => __('Cancellation Reason', 'fluent-booking'),
306 'content' => $booking->getCancelReason(false, true)
307 ]
308 ], $sections);
309 }
310
311 if ($booking->status == 'rejected') {
312 // add rejection reason at the beginning
313 $sections = array_merge([
314 'cancellation_reason' => [
315 'title' => __('Rejection Reason', 'fluent-booking'),
316 'content' => $booking->getRejectReason(false, true)
317 ]
318 ], $sections);
319 }
320
321 if ($booking->message) {
322 $sections['note'] = [
323 'title' => __('Additional Note', 'fluent-booking'),
324 'content' => wpautop($booking->message)
325 ];
326 }
327
328 $customFieldsData = $booking->getCustomFormData(true);
329
330 foreach ($customFieldsData as $dataKey => $data) {
331 if (!empty($data['value'])) {
332 $sections[$dataKey] = [
333 'title' => $data['label'],
334 'content' => $data['value']
335 ];
336 }
337 }
338
339 $bookingStatus = $booking->getBookingStatus();
340
341 $subHeading = '';
342 if ($booking->status == 'scheduled') {
343 // translators: %s is the name of the person scheduled
344 $subHeading = sprintf(__('You are scheduled with %s', 'fluent-booking'), $author['name']);
345
346 $requestType = sanitize_text_field(Arr::get($_REQUEST, 'type'));
347 if ($requestType == 'confirmation' && $calendarSlot->allowMultiBooking()) {
348 $bookingTime = (array) $sections['when']['content'];
349 $sections['when']['content'] = array_merge($bookingTime, $booking->getOtherBookingTimes());
350 }
351 }
352
353 // translators: %s is the status of the meeting
354 $title = sprintf(__('Your meeting has been %s', 'fluent-booking'), $bookingStatus);
355 if ($booking->status == 'pending' && $booking->payment_status != 'pending') {
356 $title = __('Your booking has been submitted', 'fluent-booking');
357 $subHeading = __('Please wait for the host to confirm your booking', 'fluent-booking');
358 }
359
360 $assetsUrl = App::getInstance('url.assets');
361
362 $confirmationData = [
363 'author' => $author,
364 'title' => $title,
365 'sub_heading' => $subHeading,
366 'sections' => $sections,
367 'slot' => $calendarSlot,
368 'booking' => $booking,
369 'message' => __('A confirmation has been sent to your email address along with meeting location details.', 'fluent-booking'),
370 'action_type' => $actionType,
371 'can_cancel' => $booking->canCancel(),
372 'bookmarks' => [],
373 'confirm_icon' => '',
374 'action_url' => '',
375 'extra_html' => ''
376 ];
377
378 if ($booking->payment_status) {
379 $confirmationData['extra_html'] = EditorShortCodeParser::parse('{{payment.receipt_html}}', $booking);
380 }
381
382 if ($actionType == 'cancel') {
383 $confirmationData['title'] = __('Booking Cancellation', 'fluent-booking');
384 $confirmationData['sub_heading'] = __('Confirm and cancel the scheduled booking', 'fluent-booking');
385 $confirmationData['cancel_field'] = BookingFieldService::getBookingFieldByName($calendarSlot, 'cancellation_reason');
386 $confirmationData['action_url'] = add_query_arg([
387 'action' => 'fcal_cancel_meeting',
388 'meeting_hash' => $booking->hash,
389 'scope' => Arr::get($_REQUEST, 'scope') // phpcs:ignore WordPress.Security.NonceVerification.Recommended
390 ], admin_url('admin-ajax.php'));
391 }
392
393 if ($booking->status == 'scheduled' && $actionType == 'confirmation') {
394 $confirmationData['confirm_icon'] = $assetsUrl . '/images/check-mark.png';
395 $confirmationData['bookmarks'] = $booking->getMeetingBookmarks($assetsUrl);
396 }
397
398 if ($booking->status == 'cancelled' || $booking->status == 'rejected') {
399 $confirmationData['confirm_icon'] = $assetsUrl . '/images/cancel-mark.png';
400 }
401
402 $confirmationData = apply_filters('fluent_booking/schedule_receipt_data', $confirmationData, $booking);
403
404 return (string)App::make('view')->make('public.booking_confirmation', $confirmationData);
405 }
406
407 public static function generateBookingICS(Booking $booking)
408 {
409 $author = $booking->getHostDetails(false);
410
411 // Initialize the ICS content
412 $icsContent = "BEGIN:VCALENDAR\r\n";
413 $icsContent .= "VERSION:2.0\r\n";
414 $icsContent .= "PRODID:-//Google Inc//Fluent Booking//EN\r\n";
415 $icsContent .= "METHOD:REQUEST\r\n";
416 $icsContent .= "STATUS:CONFIRMED\r\n";
417
418 $icsContent .= "BEGIN:VEVENT\r\n";
419 $icsContent .= "UID:" . md5($booking->hash) . "\r\n"; // Unique ID for the event
420
421 // Event details
422 $icsContent .= "SUMMARY:" . $booking->getBookingTitle() . "\r\n";
423 $icsContent .= "DESCRIPTION:" . $booking->getIcsBookingDescription() . "\r\n";
424
425 // Date and time formatting (assuming eventStart and eventEnd are DateTime objects)
426 $icsContent .= "DTSTART:" . gmdate('Ymd\THis\Z', strtotime($booking->start_time)) . "\r\n";
427 $icsContent .= "DTEND:" . gmdate('Ymd\THis\Z', strtotime($booking->end_time)) . "\r\n";
428
429 $icsContent .= "LOCATION:" . $booking->getLocationAsText() . "\r\n";
430
431 $icsContent .= "ORGANIZER;CN=\"" . $author['name'] . "\":mailto:" . $author['email'] . "\r\n";
432
433 $icsContent .= "ATTENDEE;CN=\"" . $booking->email . "\";ROLE=REQ-PARTICIPANT;RSVP=TRUE;PARTSTAT=ACCEPTED:mailto:" . $booking->email . "\r\n";
434
435 $icsContent .= "END:VEVENT\r\n";
436
437 // Close the VCALENDAR component
438 $icsContent .= "END:VCALENDAR\r\n";
439
440 return $icsContent;
441 }
442
443 }
444