PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 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 All 34 releases
fluent-booking / app / Http / Controllers / BookingController.php

BookingController.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.5.0, at app/Http/Controllers/BookingController.php

499 lines 20.0 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\Http\Controllers;
4
5 use FluentBooking\App\App;
6 use FluentBooking\App\Models\Booking;
7 use FluentBooking\App\Models\CalendarSlot;
8 use FluentBooking\App\Services\BookingService;
9 use FluentBooking\App\Services\RescheduleService;
10 use FluentBooking\App\Services\DateTimeHelper;
11 use FluentBooking\App\Services\BookingFieldService;
12 use FluentBooking\App\Hooks\Handlers\FrontEndHandler;
13 use FluentBooking\App\Hooks\Handlers\TimeSlotServiceHandler;
14 use FluentBooking\Framework\Http\Request\Request;
15 use FluentBooking\App\Services\Helper;
16 use FluentBooking\Framework\Support\Arr;
17
18 class BookingController extends Controller
19 {
20 public function getSlots(Request $request, $slotId)
21 {
22 $slot = CalendarSlot::findOrfail($slotId);
23
24 if ($slot->status != 'active') {
25 return $this->sendError([
26 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
27 ]);
28 }
29
30 $calendar = $slot->calendar;
31 $startDate = $request->get('start_date', gmdate('Y-m-d H:i:s'));
32 $timeZone = $request->get('timezone', 'UTC');
33
34 if (!$timeZone) {
35 $timeZone = 'UTC';
36 }
37
38 $timeSlotService = TimeSlotServiceHandler::initService($calendar, $slot);
39
40 if (is_wp_error($timeSlotService)) {
41 return TimeSlotServiceHandler::sendError($timeSlotService, $slot, $timeZone);
42 }
43
44 $availableSpots = $timeSlotService->getAvailableSpots($startDate, $timeZone);
45
46 if(is_wp_error($availableSpots)) {
47 return [
48 'available_slots' => [],
49 'timezone' => $timeZone,
50 'invalid_dates' => true,
51 'max_lookup_date' => $slot->getMaxLookUpDate(),
52 ];
53 }
54
55 return [
56 'available_slots' => array_filter($availableSpots),
57 'timezone' => $timeZone,
58 'max_lookup_date' => $slot->getMaxLookUpDate(),
59 ];
60 }
61
62 public function createBooking(Request $request, $eventId)
63 {
64 $calendarEvent = CalendarSlot::findOrfail($eventId);
65
66 if ($calendarEvent->status != 'active') {
67 return $this->sendError([
68 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
69 ]);
70 }
71
72 $postedData = $request->all();
73
74 $rules = [
75 'name' => 'required',
76 'email' => 'required|email',
77 'timezone' => 'required',
78 'event_time' => 'required'
79 ];
80
81 $messages = [
82 'name.required' => __('Please enter attendee\'s name', 'fluent-booking'),
83 'email.required' => __('Please enter attendee\'s email address', 'fluent-booking'),
84 'email.email' => __('Please provide a valid email address', 'fluent-booking'),
85 'timezone.required' => __('Please select the timezone', 'fluent-booking'),
86 'event_time.required' => __('Please select a date and time', 'fluent-booking')
87 ];
88
89 $locationType = Arr::get($postedData, 'location_type');
90
91 if ($calendarEvent->isPhoneRequired()) {
92 $rules['location_description'] = ['required', $this->validPhoneNumberRule()];
93 $messages['location_description.required'] = __('Please provide attendee\'s phone number', 'fluent-booking');
94 } else if ($calendarEvent->isAddressRequired()) {
95 $rules['location_description'] = 'required';
96 $messages['location_description.required'] = __('Please provide attendee\'s address', 'fluent-booking');
97 } else if ($locationType === 'phone_guest') {
98 $rules['location_description'] = [$this->validPhoneNumberRule()];
99 }
100
101 $additionalGuests = Arr::get($postedData, 'guests', []);
102 if (!empty($additionalGuests)) {
103 if ($calendarEvent->isMultiGuestEvent()) {
104 $additionalGuests = $this->sanitize_mapped_data($additionalGuests);
105 $additionalGuests = array_values(array_filter($additionalGuests, function ($guest) {
106 return Arr::get($guest, 'name') && Arr::get($guest, 'email');
107 }));
108 } else {
109 $additionalGuests = array_filter(array_map('sanitize_email', $additionalGuests));
110 }
111 }
112
113 $postedData['guests'] = $additionalGuests;
114
115 $requiredFields = array_filter($calendarEvent->getMeta('booking_fields', []), function ($field) {
116 return Arr::isTrue($field, 'required') && Arr::isTrue($field, 'enabled') && (Arr::get($field, 'name') == 'message' || Arr::get($field, 'name') == 'guests');
117 });
118
119 foreach ($requiredFields as $field) {
120 if (empty($rules[$field['name']])) {
121 $rules[$field['name']] = 'required';
122 $messages[$field['name'] . '.required'] = __('This field is required', 'fluent-booking');
123 }
124 }
125
126 $validationConfig = apply_filters('fluent_booking/schedule_validation_rules_data', [
127 'rules' => $rules,
128 'messages' => $messages
129 ], $postedData, $calendarEvent);
130
131 $app = App::getInstance();
132
133 $validator = $app->validator->make($postedData, $validationConfig['rules'], $validationConfig['messages']);
134 if ($validator->validate()->fails()) {
135 wp_send_json([
136 'message' => __('Please fill up the required data', 'fluent-booking'),
137 'errors' => $validator->errors()
138 ], 422);
139 return;
140 }
141
142 $customFieldsData = BookingFieldService::getCustomFieldsData(Arr::get($postedData, 'custom_fields', []), $calendarEvent);
143 $customFieldsData = apply_filters('fluent_booking/schedule_custom_field_data', $customFieldsData, $calendarEvent);
144
145 if (is_wp_error($customFieldsData)) {
146 wp_send_json([
147 'message' => $customFieldsData->get_error_message(),
148 'errors' => $customFieldsData->get_error_data()
149 ], 422);
150 return;
151 }
152
153 $duration = $calendarEvent->getDuration(Arr::get($postedData, 'duration', null));
154 $timezone = sanitize_text_field($postedData['timezone']);
155
156 $startDateTime = DateTimeHelper::convertToUtc(sanitize_text_field($postedData['event_time']), $timezone);
157 $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startDateTime) + ($duration * 60));
158
159 $bookingData = apply_filters('fluent_booking/initialize_booking_data', [
160 'person_time_zone' => $timezone,
161 'start_time' => $startDateTime,
162 'name' => sanitize_text_field($postedData['name']),
163 'email' => sanitize_email($postedData['email']),
164 'message' => sanitize_textarea_field(wp_unslash(Arr::get($postedData, 'message', ''))),
165 'phone' => sanitize_text_field(Arr::get($postedData, 'phone_number', '')),
166 'address' => sanitize_textarea_field(Arr::get($postedData, 'address', '')),
167 'ip_address' => Helper::getIp(),
168 'status' => sanitize_text_field(Arr::get($postedData, 'status', 'scheduled')),
169 'source' => Arr::get($postedData, 'source') == 'admin' ? 'admin' : 'web',
170 'event_type' => $calendarEvent->event_type,
171 'slot_minutes' => $duration
172 ], $postedData, $calendarEvent);
173
174 $eventLocations = [];
175 $locationSettings = $calendarEvent->location_settings;
176 foreach ($locationSettings as $index => $location) {
177 $eventLocations[$location['type']] = $location;
178 }
179
180 $locationDetails['type'] = $locationType;
181 if ($locationType == 'phone_organizer') {
182 $locationDetails['description'] = $eventLocations[$locationType]['host_phone_number'];
183 } else if ($locationType == 'phone_guest') {
184 $bookingData['phone'] = sanitize_text_field(Arr::get($postedData, 'location_description', ''));
185 } else if ($locationType == 'in_person_guest') {
186 $locationDetails['description'] = sanitize_textarea_field(Arr::get($postedData, 'location_description', ''));
187 } else if (in_array($locationType, ['custom', 'in_person_organizer'])) {
188 $locationDetails['description'] = $eventLocations[$locationType]['description'];
189 } else if (in_array($locationType, ['google_meet', 'online_meeting', 'zoom_meeting', 'ms_teams'])) {
190 $locationDetails['description'] = Arr::get($eventLocations[$locationType], 'meeting_link', '');
191 $locationDetails['online_platform_link'] = $locationDetails['description'];
192 }
193
194 $bookingData['location_details'] = $locationDetails;
195
196 if ($sourceUrl = Arr::get($postedData, 'source_url', '')) {
197 $bookingData['source_url'] = sanitize_url($sourceUrl);
198 }
199
200 if ($hostUserId = Arr::get($postedData, 'host_user_id', null)) {
201 $hostUserId = (int) $hostUserId;
202
203 if (!in_array($hostUserId, array_map('intval', $calendarEvent->getHostIds()), true)) {
204 return $this->sendError([
205 'message' => __('The selected host is not a host of this event', 'fluent-booking')
206 ], 422);
207 }
208
209 $bookingData['host_user_id'] = $hostUserId;
210 }
211
212 $hostIds = null;
213 if ($calendarEvent->isRoundRobin() && !$hostUserId) {
214 $hostIds = $calendarEvent->getHostIdsSortedByBookings($startDateTime);
215 $bookingData['host_user_id'] = $hostIds[0];
216 }
217
218 $availableSpot = false;
219 if (!Arr::isTrue($postedData, 'ignore_availability')) {
220 $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent);
221
222 if (is_wp_error($timeSlotService)) {
223 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timezone);
224 }
225
226 $availableSpot = $timeSlotService->isSpotAvailable($startDateTime, $endDateTime, $duration, $hostUserId);
227
228 if (!$availableSpot) {
229 wp_send_json([
230 'message' => __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking')
231 ], 422);
232 }
233
234 if ($calendarEvent->isRoundRobin() && !$hostUserId) {
235 $bookingData['host_user_id'] = $timeSlotService->hostUserId;
236 }
237 }
238
239 if ($additionalGuests) {
240 $guestField = BookingFieldService::getBookingFieldByName($calendarEvent, 'guests');
241 $guestLimit = Arr::get($guestField, 'limit', 10);
242 if ($calendarEvent->isMultiGuestEvent() && $availableSpot) {
243 $remaining = Arr::get($availableSpot, 'remaining', $calendarEvent->getMaxBookingPerSlot());
244 $guestLimit = min($remaining, $guestLimit) - 1;
245 }
246 $bookingData['additional_guests'] = array_slice($additionalGuests, 0, $guestLimit);
247 }
248
249 do_action('fluent_booking/before_creating_schedule', $bookingData, $postedData, $calendarEvent);
250
251 try {
252 $booking = BookingService::createBooking($bookingData, $calendarEvent, $customFieldsData);
253
254 do_action('fluent_booking/after_creating_schedule', $booking, $postedData, $calendarEvent);
255 } catch (\Exception $e) {
256 wp_send_json([
257 'message' => $e->getMessage()
258 ], $e->getCode());
259 }
260
261 return [
262 'booking' => $booking,
263 'message' => __('Booking has been created', 'fluent-booking'),
264 ];
265 }
266
267 /**
268 * @return \Closure
269 */
270 private function validPhoneNumberRule()
271 {
272 return function ($attribute, $value) {
273 if (!empty($value) && !Helper::isValidPhoneNumber($value)) {
274 return __('Please provide a valid phone number', 'fluent-booking');
275 }
276 };
277 }
278
279 public function getEvent(Request $request, $eventId)
280 {
281 $calendarEvent = CalendarSlot::find($eventId);
282
283 if (!$calendarEvent || $calendarEvent->status != 'active') {
284 wp_send_json([
285 'message' => __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking')
286 ], 422);
287 }
288
289 $calendar = $calendarEvent->calendar;
290
291 if (!$calendar) {
292 return $this->sendError([
293 'message' => __('Calendar not found', 'fluent-booking')
294 ]);
295 }
296
297 $calendarEventVars = (new FrontEndHandler())->getCalendarEventVars($calendar, $calendarEvent);
298
299 $startDate = $request->get('start_date');
300
301 if (!$startDate) {
302 $startDate = gmdate('Y-m-d H:i:s');
303 }
304
305 $timeZone = $request->get('timezone');
306
307 if (!$timeZone) {
308 $timeZone = wp_timezone_string();
309 }
310
311 if (!in_array($timeZone, \DateTimeZone::listIdentifiers())) {
312 $timeZone = $calendar->author_timezone;
313 }
314
315 $duration = $calendarEvent->getDuration($request->get('duration'));
316
317 $hostId = $request->get('host_id', null);
318
319 if ($hostId) {
320 $hostId = (int) $hostId;
321
322 if (!in_array($hostId, array_map('intval', $calendarEvent->getHostIds()), true)) {
323 wp_send_json([
324 'message' => __('The selected host is not a host of this event', 'fluent-booking')
325 ], 422);
326 }
327 }
328
329 $timeSlotService = TimeSlotServiceHandler::initService($calendar, $calendarEvent);
330
331 if (is_wp_error($timeSlotService)) {
332 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timeZone);
333 }
334
335 $availableSpots = $timeSlotService->getAvailableSpots($startDate, $timeZone, $duration, $hostId);
336
337 if (is_wp_error($availableSpots)) {
338 wp_send_json([
339 'available_slots' => [],
340 'calendar_event' => $calendarEventVars,
341 'timezone' => $timeZone,
342 'invalid_dates' => true
343 ], 200);
344 }
345
346 $availableSpots = apply_filters('fluent_booking/available_slots_for_view', array_filter($availableSpots), $calendarEvent, $calendar, $timeZone, $duration);
347
348 return [
349 'calendar_event' => $calendarEventVars,
350 'available_slots' => $availableSpots
351 ];
352 }
353
354 public function getBookings(Request $request)
355 {
356 $userData = get_userdata(get_current_user_id());
357
358 $userEmail = $userData ? $userData->user_email : null;
359
360 if (!$userEmail) {
361 return $this->sendError([
362 'message' => __('Please login to view your bookings', 'fluent-booking')
363 ]);
364 }
365
366 $perPage = intval($request->get('per_page', 10));
367
368 $bookingPeriod = sanitize_text_field($request->get('period', 'all'));
369
370 $bookingQuery = Booking::query()->with('calendar_event')
371 ->where('email', $userEmail)
372 ->applyComputedStatus($bookingPeriod)
373 ->applyBookingOrderByStatus($bookingPeriod);
374
375 $calendarIds = $request->get('calendar_ids', []);
376
377 if (!in_array('all', $calendarIds)) {
378 $calendarIds = array_map('intval', $calendarIds);
379 $bookingQuery->whereIn('calendar_id', $calendarIds);
380 }
381
382 do_action_ref_array('fluent_booking/bookings_query', [&$bookingQuery]);
383
384 $totalBookings = $bookingQuery->count();
385
386 $bookings = $bookingQuery->limit($perPage)->get();
387
388 $formattedBookings = [];
389 foreach ($bookings as $booking) {
390 $formattedBookings[] = [
391 'id' => $booking->id,
392 'person_time_zone' => $booking->person_time_zone,
393 'status' => ucfirst($booking->status),
394 'payment_status' => $booking->payment_status,
395 'booking_title' => $booking->getBookingTitle(true),
396 'author_name' => $booking->getHostDetails(false)['name'],
397 'booking_date' => DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'date'),
398 'booking_time' => DateTimeHelper::formatToLocale($booking->getAttendeeStartTime(), 'time') . ' - ' . DateTimeHelper::formatToLocale($booking->getAttendeeEndTime(), 'time'),
399 ];
400 }
401
402 return [
403 'bookings' => $formattedBookings,
404 'total' => $totalBookings
405 ];
406 }
407
408 public function getRescheduleSlots(Request $request, $eventId, $bookingId)
409 {
410 return $this->getEvent($request, $eventId);
411 }
412
413 public function rescheduleBooking(Request $request, $eventId, $bookingId)
414 {
415 $booking = Booking::with(['calendar_event'])->findOrFail($bookingId);
416
417 $data = $request->all();
418
419 $this->validate($data, [
420 'event_time' => 'required',
421 'timezone' => 'required'
422 ], [
423 'event_time.required' => __('Please select a date and time', 'fluent-booking'),
424 'timezone.required' => __('Please select the timezone', 'fluent-booking')
425 ]);
426
427 $calendarEvent = $booking->calendar_event;
428
429 if (!$calendarEvent || (int) $calendarEvent->id !== (int) $eventId) {
430 return $this->sendError(['message' => __('The event of this booking was not found', 'fluent-booking')], 422);
431 }
432
433 if (!in_array($booking->status, ['scheduled', 'pending', 'approved', 'rescheduled'], true)) {
434 return $this->sendError(['message' => __('This booking can not be rescheduled', 'fluent-booking')], 422);
435 }
436
437 $timezone = sanitize_text_field(Arr::get($data, 'timezone'));
438
439 if (!in_array($timezone, \DateTimeZone::listIdentifiers(), true)) {
440 return $this->sendError(['message' => __('Please select a valid timezone', 'fluent-booking')], 422);
441 }
442
443 $eventTime = sanitize_text_field(Arr::get($data, 'event_time'));
444
445 if (!\DateTime::createFromFormat('Y-m-d H:i:s', $eventTime)) {
446 return $this->sendError(['message' => __('Please select a valid date and time', 'fluent-booking')], 422);
447 }
448
449 $startTime = DateTimeHelper::convertToUtc($eventTime, $timezone);
450 $endTime = gmdate('Y-m-d H:i:s', strtotime($startTime) + ($booking->slot_minutes * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
451
452 $hostUserId = null;
453
454 if (!Arr::isTrue($data, 'ignore_availability')) {
455 $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent);
456
457 if (is_wp_error($timeSlotService)) {
458 return $this->sendError(['message' => $timeSlotService->get_error_message()], 422);
459 }
460
461 $isSlotLocked = Helper::lockRoundRobinSlot($calendarEvent, $startTime, $endTime);
462
463 if (!$isSlotLocked || !$timeSlotService->isSpotAvailable($startTime, $endTime, $booking->slot_minutes)) {
464 return $this->sendError([
465 'message' => __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking')
466 ], 422);
467 }
468
469 if ($calendarEvent->isRoundRobin()) {
470 $hostUserId = $timeSlotService->hostUserId;
471 }
472 }
473
474 $result = RescheduleService::reschedule($booking, $calendarEvent, $startTime, $timezone, [
475 'reason' => Arr::get($data, 'reason', ''),
476 'host_user_id' => $hostUserId,
477 'source' => __('Admin Panel', 'fluent-booking')
478 ]);
479
480 if (is_wp_error($result)) {
481 return $this->sendError(['message' => $result->get_error_message()], 422);
482 }
483
484 return [
485 'message' => __('Booking has been rescheduled', 'fluent-booking')
486 ];
487 }
488
489 private static function sanitize_mapped_data($settings)
490 {
491 $sanitizerMap = [
492 'name' => 'sanitize_text_field',
493 'email' => 'sanitize_email',
494 ];
495
496 return Helper::fcal_backend_sanitizer($settings, $sanitizerMap);
497 }
498 }
499