PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 1.10.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v1.10.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 1.7.2 All 33 releases
fluent-booking / app / Models / Booking.php

Booking.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 1.10.0, at app/Models/Booking.php

1,238 lines 39.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\Models;
4
5 use FluentBooking\App\Models\Model;
6 use FluentBooking\App\Services\BookingFieldService;
7 use FluentBooking\App\Services\LocationService;
8 use FluentBooking\App\Services\DateTimeHelper;
9 use FluentBooking\App\Services\Helper;
10 use FluentBooking\Framework\Support\Arr;
11 use FluentBooking\App\Services\PermissionManager;
12 use FluentBooking\App\Services\EditorShortCodeParser;
13
14 class Booking extends Model
15 {
16 protected $table = 'fcal_bookings';
17
18 protected $guarded = ['id'];
19
20 private static $bookingType = 'scheduling';
21
22 protected $fillable = [
23 'calendar_id',
24 'event_id',
25 'parent_id',
26 'group_id',
27 'hash',
28 'person_user_id',
29 'host_user_id',
30 'person_contact_id',
31 'person_time_zone',
32 'start_time',
33 'end_time',
34 'slot_minutes',
35 'first_name',
36 'last_name',
37 'email',
38 'message',
39 'internal_note',
40 'phone',
41 'country',
42 'ip_address',
43 'browser',
44 'device',
45 'other_info',
46 'location_details',
47 'cancelled_by',
48 'status',
49 'payment_method',
50 'payment_status',
51 'event_type',
52 'source',
53 'source_id',
54 'source_url',
55 'utm_source',
56 'utm_medium',
57 'utm_campaign',
58 'utm_term',
59 'utm_content'
60 ];
61
62
63 /**
64 * $searchable Columns in table to search
65 * @var array
66 */
67 protected $searchable = [
68 'email',
69 'first_name',
70 'last_name'
71 ];
72
73 public static function boot()
74 {
75 parent::boot();
76
77 static::creating(function ($model) {
78 if (!isset($model->person_user_id) && $userId = get_current_user_id()) {
79 $model->person_user_id = $userId;
80 }
81
82 if (is_null($model->group_id) || !isset($model->group_id)) {
83 $model->group_id = static::assignNextGroupId();
84 }
85
86 if (defined('FLUENTCRM') && !empty($model->email) && apply_filters('fluent_calender/auto_booking_fluent_crm_sync', true)) {
87 $contact = FluentCrmApi('contacts')->getContact($model->email);
88 if ($contact) {
89 $model->person_contact_id = $contact->id;
90 }
91 }
92
93 if (empty($model->booking_type)) {
94 $model->booking_type = self::$bookingType;
95 }
96
97 $model->hash = md5(wp_generate_uuid4() . time());
98 });
99
100 static::deleting(function ($model) { // before delete() method call this
101 $model->booking_meta()->delete();
102 $model->booking_activities()->delete();
103 });
104
105 static::addGlobalScope('main_bookings', function ($builder) {
106 $builder->where('booking_type', self::$bookingType);
107 });
108 }
109
110 public function calendar()
111 {
112 return $this->belongsTo(Calendar::class, 'calendar_id');
113 }
114
115 public function slot()
116 {
117 return $this->belongsTo(CalendarSlot::class, 'event_id');
118 }
119
120 public function calendar_event()
121 {
122 return $this->belongsTo(CalendarSlot::class, 'event_id');
123 }
124
125 public function booking_meta()
126 {
127 return $this->hasMany(BookingMeta::class, 'booking_id');
128 }
129
130 public function booking_activities()
131 {
132 return $this->hasMany(BookingActivity::class, 'booking_id');
133 }
134
135 public function user()
136 {
137 return $this->belongsTo(User::class, 'host_user_id');
138 }
139
140 public static function assignNextGroupId()
141 {
142 $lastEvent = static::orderBy('group_id', 'desc')->first(['group_id']);
143
144 return $lastEvent ? $lastEvent->group_id + 1 : 1;
145 }
146
147 public function getCustomFormData($isFormatted = true, $isPublic = false)
148 {
149 if ($isFormatted) {
150 return BookingFieldService::getFormattedCustomBookingData($this, true, $isPublic);
151 }
152
153 return $this->getMeta('custom_fields_data', []);
154 }
155
156 public static function getHostTotalBooking($eventId, $hostIds, $ranges)
157 {
158 return self::where('event_id', $eventId)
159 ->whereIn('host_user_id', $hostIds)
160 ->whereBetween('start_time', $ranges)
161 ->whereIn('status', ['scheduled', 'completed'])
162 ->count();
163 }
164
165 public function getAdditionalGuests($isHtml = false)
166 {
167 $additionalGuests = $this->getMeta('additional_guests', []);
168 if (!$additionalGuests) {
169 return [];
170 }
171
172 if ($isHtml) {
173 return wpautop(implode('<br>', $additionalGuests));
174 }
175
176 return $additionalGuests;
177 }
178
179 public function getTotalGuestCount()
180 {
181 $additionalGuests = $this->getAdditionalGuests();
182 $mainGuests = 1;
183 if ($this->isMultiGuestBooking()) {
184 $mainGuests = self::where('group_id', $this->group_id)->where('status', 'scheduled')->count();
185 }
186 return count($additionalGuests) + $mainGuests;
187 }
188
189 public function getHostEmails($excludeHostId = null)
190 {
191 $hostIds = $this->getHostIds();
192
193 $emails = [];
194 foreach ($hostIds as $hostId) {
195 if ($hostId != $excludeHostId) {
196 if ($user = get_user_by('ID', $hostId)) {
197 $emails[] = $user->user_email;
198 }
199 }
200 }
201
202 return $emails;
203 }
204
205 public function hosts()
206 {
207 $class = __NAMESPACE__ . '\User';
208
209 return $this->belongsToMany(
210 $class,
211 'fcal_booking_hosts',
212 'booking_id',
213 'user_id'
214 )
215 ->withPivot('status')
216 ->withTimestamps();
217 }
218
219 public function getHostIds()
220 {
221 return $this->hosts()->pluck('user_id')->toArray();
222 }
223
224 public function scopeUpcoming($query)
225 {
226 return $query->where('end_time', '>=', gmdate('Y-m-d H:i:s')); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
227 }
228
229 public function scopePast($query)
230 {
231 return $query->where('end_time', '<', gmdate('Y-m-d H:i:s')); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
232 }
233
234 public function scopeApplyDateRangeFilter($query, $range)
235 {
236 if (empty($range['start_date']) || empty($range['end_date'])) {
237 return $query;
238 }
239
240 if (!empty($range['time_zone']) && $range['time_zone'] != 'UTC') {
241 $range['start_date'] = gmdate('Y-m-d H:i:s', strtotime($range['start_date'] . ' -1 day'));
242 $range['end_date'] = gmdate('Y-m-d H:i:s', strtotime($range['end_date'] . ' +1 day'));
243 }
244
245 return $query->whereBetween('start_time', [$range['start_date'], $range['end_date']]);
246 }
247
248 public function scopeApplyComputedStatus($query, $status)
249 {
250 $validStatuses = [
251 'upcoming',
252 'completed',
253 'cancelled',
254 'pending',
255 'no_show',
256 'latest_bookings'
257 ];
258
259 if (!in_array($status, $validStatuses)) {
260 return $query;
261 }
262
263 if ($status == 'upcoming') {
264 return $query->where('end_time', '>=', gmdate('Y-m-d H:i:s')) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
265 ->where('status', 'scheduled');
266 }
267
268 if ($status == 'completed') {
269 return $query->where('end_time', '<', gmdate('Y-m-d H:i:s')) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
270 ->where('status', '!=', 'cancelled')
271 ->where('status', '!=', 'rejected')
272 ->orWhere('status', 'completed'); // maybe cron did not mark few as completed yet
273 }
274
275 if ($status == 'cancelled') {
276 return $query->where('status', 'cancelled')
277 ->orWhere('status', 'rejected');
278 }
279
280 if ($status == 'pending') {
281 return $query->whereIn('status', ['pending', 'reserved']);
282 }
283
284 if ($status == 'latest_bookings') {
285 return $query->where('status', '!=', 'reserved');
286 }
287
288 return $query->where('status', $status);
289 }
290
291 public function scopeApplyBookingOrderByStatus($query, $status)
292 {
293 if ($status == 'upcoming') {
294 return $query->orderBy('start_time', 'ASC');
295 }
296
297 if ($status == 'latest_bookings') {
298 return $query->orderBy('created_at', 'DESC');
299 }
300
301 if (in_array($status, ['completed', 'cancelled'])) {
302 return $query->orderBy('updated_at', 'DESC');
303 }
304
305 return $query->orderBy('start_time', 'DESC');
306 }
307
308 public function getFullBookingDateTimeText($timeZone = 'UTC', $isHtml = false)
309 {
310 $startDateTime = DateTimeHelper::convertFromUtc($this->start_time, $timeZone, 'Y-m-d H:i:s');
311 $endDateTime = DateTimeHelper::convertFromUtc($this->end_time, $timeZone, 'Y-m-d H:i:s');
312
313 $html = DateTimeHelper::formatToLocale($startDateTime, 'time') . ' - ' . DateTimeHelper::formatToLocale($endDateTime, 'time') . ', ';
314 $html .= DateTimeHelper::formatToLocale($startDateTime, 'date');
315
316 if ($isHtml && in_array($this->status, ['cancelled', 'rejected'])) {
317 $html = '<del>' . $html . '</del>';
318 }
319
320 return $html;
321 }
322
323 public function getPreviousMeetingDateTimeText($timeZone = 'UTC')
324 {
325 $previousStartTime = $this->getMeta('previous_meeting_time');
326 $previousEndTime = gmdate('Y-m-d H:i:s', strtotime($previousStartTime) + ($this->slot_minutes * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
327
328 $startDateTime = DateTimeHelper::convertFromUtc($previousStartTime, $timeZone, 'Y-m-d H:i:s');
329 $endDateTime = DateTimeHelper::convertFromUtc($previousEndTime, $timeZone, 'Y-m-d H:i:s');
330
331 $text = DateTimeHelper::formatToLocale($startDateTime, 'time') . ' - ' . DateTimeHelper::formatToLocale($endDateTime, 'time') . ', ';
332 $text .= DateTimeHelper::formatToLocale($startDateTime, 'date');
333
334 return $text;
335 }
336
337 protected function formatBookingDateTime($dateTime, $timeZone = 'UTC')
338 {
339 $startDate = DateTimeHelper::convertFromUtc($dateTime, $timeZone, 'D M d, Y');
340 $startTime = DateTimeHelper::convertFromUtc($dateTime, $timeZone, 'h:ia');
341
342 $localDate = date_i18n('D M d, Y', strtotime($startDate));
343 $localTime = date_i18n('h:ia', strtotime($startTime));
344
345 return $localDate . ' ' . $localTime;
346 }
347
348 public function getShortBookingDateTime($timeZone = 'UTC')
349 {
350 return $this->formatBookingDateTime($this->start_time, $timeZone);
351 }
352
353 public function getPreviousMeetingTime($timeZone = 'UTC')
354 {
355 $previousMeetingTime = $this->getMeta('previous_meeting_time');
356 return $this->formatBookingDateTime($previousMeetingTime, $timeZone);
357 }
358
359 public function getAttendeeStartTime($format = 'Y-m-d H:i:s')
360 {
361 return DateTimeHelper::convertFromUtc($this->start_time, $this->person_time_zone, $format);
362 }
363
364 public function getAttendeeEndTime($format = 'Y-m-d H:i:s')
365 {
366 return DateTimeHelper::convertFromUtc($this->end_time, $this->person_time_zone, $format);
367 }
368
369 public function getOtherBookingTimes()
370 {
371 $otherBookings = self::where('parent_id', $this->id)->get();
372
373 return $otherBookings->map(function ($otherBooking) {
374 return $otherBooking->getFullBookingDateTimeText($this->person_time_zone, true) . ' (' . $this->person_time_zone . ')';
375 })->toArray();
376 }
377
378 public function getAllBookingShortTimes($timeZone = 'UTC')
379 {
380 $otherBookings = self::where('parent_id', $this->id)->get();
381
382 $otherTimes = $otherBookings->map(function ($otherBooking) use ($timeZone) {
383 return $otherBooking->formatBookingDateTime($otherBooking->start_time, $timeZone);
384 })->toArray();
385
386 return array_merge($otherTimes, [
387 $this->formatBookingDateTime($this->start_time, $timeZone)
388 ]);
389 }
390
391 public function getHostAndGuestDetailsHtml()
392 {
393 $authors = $this->getHostsDetails();
394
395 $guestNames = (array) trim($this->first_name . ' ' . $this->last_name);
396
397 if ($this->isMultiGuestBooking()) {
398 $otherGuests = self::where('parent_id', $this->id)->get()->map(function ($guest) {
399 return trim($guest->first_name . ' ' . $guest->last_name);
400 })->toArray();
401 $guestNames = array_merge($guestNames, $otherGuests);
402 }
403
404 $hostUserId = $this->host_user_id;
405
406 $authorListHtml = '<ul class="fcal_listed">';
407
408 foreach ($authors as $author) {
409 $authorBadge = ($author['id'] == $hostUserId) ? '<span class="fcal_host_badge">' . __('Host', 'fluent-booking') . '</span>' : '';
410 $authorListHtml .= '<li class="fcal_host_name">' . $author['name'] . $authorBadge . '</li>';
411 }
412
413 foreach ($guestNames as $guestName) {
414 $authorListHtml .= '<li class="fcal_guest_name">' . $guestName . '</li>';
415 }
416 $authorListHtml .= '</ul>';
417
418 return $authorListHtml;
419 }
420
421 public function getLocationDetailsHtml()
422 {
423 $details = $this->location_details;
424 $locationType = Arr::get($details, 'type');
425
426 $html = '';
427 if ($locationType === 'in_person_guest') {
428 $html = '<b>' . __('Invitee Address:', 'fluent-booking') . ' </b>' . Arr::get($details, 'description');
429 } else if ($locationType === 'in_person_organizer') {
430 $html = '<b>' . Arr::get($details, 'title') . ' </b>';
431 $description = Arr::get($details, 'description');
432 if ($description) {
433 $html .= wpautop($description);
434 }
435 } else if ($locationType === 'phone_guest') {
436 $html = '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . $this->phone;
437 } else if ($locationType === 'phone_organizer') {
438 $html = '<b>' . __('Phone Call:', 'fluent-booking') . ' </b>' . Arr::get($details, 'description') . __(' (Host phone number)', 'fluent-booking');
439 } else if ($locationType === 'custom') {
440 $html = '<b>' . Arr::get($details, 'title') . '</b>';
441 $html .= wpautop(Arr::get($details, 'description'));
442 } else if (in_array($locationType, ['google_meet', 'online_meeting', 'zoom_meeting', 'ms_teams'])) {
443 $platformLabels = [
444 'google_meet' => __('Google Meet', 'fluent-booking'),
445 'online_meeting' => __('Online Meeting', 'fluent-booking'),
446 'zoom_meeting' => __('Zoom Video', 'fluent-booking'),
447 'ms_teams' => __('MS Teams', 'fluent-booking'),
448 ];
449
450 $html = '<b>' . $platformLabels[$locationType] . '</b> ';
451 $meetingLink = Arr::get($details, 'online_platform_link');
452 if ($meetingLink) {
453 $html .= '<a target="_blank" href="' . esc_url($meetingLink) . '">' . __('Join Meeting', 'fluent-booking') . '</a>';
454 }
455 } else {
456 return '--';
457 }
458
459 return apply_filters('fluent_booking/location_details_html', $html, $details);
460 }
461
462 public function getLocationAsText()
463 {
464 $details = $this->location_details;
465
466 $locationType = Arr::get($details, 'type');
467 $meetingLink = Arr::get($details, 'online_platform_link');
468
469 $onlinePlatforms = ['google_meet', 'zoom_meeting', 'online_meeting', 'ms_teams'];
470
471 if ($meetingLink && in_array($locationType, $onlinePlatforms)) {
472 return $meetingLink;
473 }
474
475 if ($locationType == 'phone_organizer') {
476 return Arr::get($details, 'description');
477 }
478
479 if ($locationType == 'phone_guest') {
480 return $this->phone;
481 }
482
483 $text = wp_strip_all_tags($this->getLocationDetailsHtml());
484
485 return apply_filters('fluent_booking/location_details_text', $text, $details);
486 }
487
488 public function getMessage()
489 {
490 if (empty($this->message)) {
491 return 'n/a';
492 }
493 return $this->message;
494 }
495
496 public function setLocationDetailsAttribute($locationDetails)
497 {
498 $this->attributes['location_details'] = \maybe_serialize($locationDetails);
499 }
500
501 public function getLocationDetailsAttribute($locationDetails)
502 {
503 return \maybe_unserialize($locationDetails);
504 }
505
506 public function getOngoingStatus()
507 {
508 if ($this->status != 'scheduled') {
509 return [];
510 }
511
512 $currentTime = time();
513 $startTime = strtotime($this->start_time);
514 $endTime = strtotime($this->end_time);
515
516 if ($currentTime > $startTime && $currentTime < $endTime) {
517 return ['happening_now' => __('Happening Now', 'fluent-booking')];
518 }
519
520 if (($startTime - $currentTime) < 1800 && ($startTime - $currentTime) > 0) {
521 return ['starting_soon' => __('Starting Soon', 'fluent-booking')];
522 }
523
524 if (($endTime - $currentTime) > -3600 && ($endTime - $currentTime) < 0) {
525 return ['recently_happened' => __('Recently Happened', 'fluent-booking')];
526 }
527
528 return [];
529 }
530
531 public function getBookingStatus()
532 {
533 $status = $this->status;
534
535 $statusLabels = [
536 'scheduled' => __('Scheduled', 'fluent-booking'),
537 'rescheduled' => __('Rescheduled', 'fluent-booking'),
538 'completed' => __('Completed', 'fluent-booking'),
539 'pending' => __('Pending', 'fluent-booking'),
540 'cancelled' => __('Cancelled', 'fluent-booking'),
541 'rejected' => __('Rejected', 'fluent-booking')
542 ];
543
544 return Arr::get($statusLabels, $status, $status);
545 }
546
547 public function getPaymentStatus()
548 {
549 $status = $this->payment_status;
550
551 $statusLabels = [
552 'pending' => __('Pending', 'fluent-booking'),
553 'paid' => __('Paid', 'fluent-booking'),
554 'failed' => __('Failed', 'fluent-booking'),
555 'refunded' => __('Refunded', 'fluent-booking'),
556 'partially-paid' => __('Partially Paid', 'fluent-booking'),
557 'partially-refunded' => __('Partially Refunded', 'fluent-booking')
558 ];
559
560 return Arr::get($statusLabels, $status, $status);
561 }
562
563 public function payment_order()
564 {
565 if (defined('FLUENT_BOOKING_PRO_DIR_FILE')) {
566 return $this->hasOne(\FluentBookingPro\App\Models\Order::class, 'parent_id');
567 }
568 return $this->belongsTo(static::class, 'parent_id')->whereNull('id');
569 }
570
571 public function getCancelReason($isText = false, $isHtml = false)
572 {
573 $row = BookingActivity::where('booking_id', $this->id)
574 ->where('type', 'cancel_reason')
575 ->first();
576
577 if ($row) {
578 if ($isText) {
579 return $row->description;
580 }
581 if ($isHtml) {
582 return wp_unslash($row->description);
583 }
584 }
585
586 return $row;
587 }
588
589 public function getRejectReason($isText = false, $isHtml = false)
590 {
591 $row = BookingActivity::where('booking_id', $this->id)
592 ->where('type', 'reject_reason')
593 ->first();
594
595 if ($row) {
596 if ($isText) {
597 return $row->description;
598 }
599 if ($isHtml) {
600 return wp_unslash($row->description);
601 }
602 }
603
604 return $row;
605 }
606
607 public function addCancelOrRejectReason($title, $reason, $type = 'cancel_reason')
608 {
609 if (!$reason && !$title) {
610 return null;
611 }
612
613 if ($type == 'cancel_reason') {
614 $exist = $this->getCancelReason();
615 } else {
616 $exist = $this->getRejectReason();
617 }
618
619 if ($exist) {
620 $exist->title = $title;
621 $exist->description = $reason;
622 $exist->save();
623 return $exist;
624 }
625
626 return BookingActivity::create([
627 'booking_id' => $this->id,
628 'type' => $type,
629 'title' => $title,
630 'description' => $reason
631 ]);
632 }
633
634 public function cancelMeeting($reason = '', $cancelledByType = 'guest', $cancelledByUserId = null)
635 {
636 if ($this->status == 'cancelled') {
637 return $this;
638 }
639
640 $cancellableStatuses = [
641 'scheduled',
642 'pending'
643 ];
644
645 if (!in_array($this->status, $cancellableStatuses)) {
646 return new \WP_Error('invalid_status', __('This booking is not cancellable.', 'fluent-booking'));
647 }
648
649 $this->status = 'cancelled';
650 if ($cancelledByUserId) {
651 $this->cancelled_by = $cancelledByUserId;
652 }
653
654 if (!$cancelledByUserId) {
655 $cancelledByUserId = get_current_user_id();
656 }
657
658 $this->save();
659 $this->updateMeta('cancelled_by_type', $cancelledByType);
660
661 $userName = $cancelledByType;
662 if ($cancelledByUserId && $user = get_user_by('ID', $cancelledByUserId)) {
663 $userName = $user->display_name;
664 }
665
666 if ($reason) {
667 /* translators: Name of the user who cancelled the meeting */
668 $this->addCancelOrRejectReason(sprintf(__('Meeting has been cancelled by %s', 'fluent-booking'), $userName), $reason);
669 do_action('fluent_booking/booking_schedule_cancelled', $this, $this->calendar_event);
670 return;
671 }
672
673 BookingActivity::create([
674 'booking_id' => $this->id,
675 'status' => 'closed',
676 'type' => 'error',
677 'title' => __('Meeting Cancelled', 'fluent-booking'),
678 /* translators: Name of the user who cancelled the meeting */
679 'description' => sprintf(__('Meeting has been cancelled by %s', 'fluent-booking'), $userName)
680 ]);
681
682 do_action('fluent_booking/booking_schedule_cancelled', $this, $this->calendar_event);
683 }
684
685 public function rejectMeeting($reason = '', $rejectByUserId = null)
686 {
687 if ($this->status != 'pending') {
688 return;
689 }
690
691 $this->status = 'rejected';
692 $this->save();
693
694 $rejectByUserId = $rejectByUserId ?: get_current_user_id();
695
696 if ($reason) {
697 $userName = 'host';
698 if ($rejectByUserId && $user = get_user_by('ID', $rejectByUserId)) {
699 $userName = $user->display_name;
700 }
701 /* translators: Name of the user who rejected the booking */
702 $this->addCancelOrRejectReason(sprintf(__('Booking request has been rejected by %s', 'fluent-booking'), $userName), $reason, 'reject_reason');
703 }
704
705 do_action('fluent_booking/booking_schedule_rejected', $this, $this->calendar_event);
706 }
707
708 public function getRescheduleReason($html = false)
709 {
710 $rescheduleReason = $this->getMeta('reschedule_reason', '');
711
712 if ($rescheduleReason && $html) {
713 return wp_unslash($rescheduleReason);
714 }
715
716 return $rescheduleReason;
717 }
718
719 private function generateBookingTitle($eventTitle, $authorName, $guestName)
720 {
721 /* translators: 1: Calendar slot title, 2: Author name, 3: Full name of the gueset */
722 $bookingTitle = sprintf(__('%1$s meeting between %2$s and %3$s', 'fluent-booking'), $eventTitle, $authorName, $guestName);
723
724 return $bookingTitle;
725 }
726
727 public function getBookingTitle($html = false)
728 {
729 $calendarEvent = $this->calendar_event;
730
731 $eventTitle = $calendarEvent->title;
732
733 $authorName = $this->getHostDetails(false)['name'];
734
735 $guestName = trim($this->first_name . ' ' . $this->last_name);
736
737 $bookingTitle = Arr::get($calendarEvent, 'settings.booking_title');
738
739 $bookingTitle = EditorShortCodeParser::parse($bookingTitle, $this);
740
741 $bookingTitle = $bookingTitle ?: $this->generateBookingTitle($eventTitle, $authorName, $guestName);
742
743 if ($html && strpos($bookingTitle, $eventTitle) !== false) {
744 $bookingTitle = str_replace($eventTitle, "<strong>{$eventTitle}</strong>", $bookingTitle);
745 }
746
747 return apply_filters('fluent_booking/booking_meeting_title', $bookingTitle, $authorName, $guestName, $calendarEvent, $this);
748 }
749
750 public function getActivities()
751 {
752 return BookingActivity::where('booking_id', $this->id)
753 ->orderBy('id', 'DESC')
754 ->get();
755 }
756
757 public function updateMeta($key, $value)
758 {
759 $exist = BookingMeta::where('booking_id', $this->id)
760 ->where('meta_key', $key)
761 ->first();
762
763 if ($exist) {
764 $exist->value = $value;
765 $exist->save();
766 return $exist;
767 }
768
769 return BookingMeta::create([
770 'booking_id' => $this->id,
771 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
772 'value' => $value
773 ]);
774 }
775
776 public function deleteMeta($key)
777 {
778 return BookingMeta::where('booking_id', $this->id)
779 ->where('meta_key', $key)
780 ->delete();
781 }
782
783 public function getMeta($key, $default = '')
784 {
785 $exist = BookingMeta::where('booking_id', $this->id)
786 ->where('meta_key', $key)
787 ->first();
788
789 if ($exist) {
790 return $exist->value;
791 }
792
793 return $default;
794 }
795
796
797 /**
798 * Local scope to filter hosts by search/query string
799 * @param string $search
800 */
801 public function scopeSearchBy($query, $search)
802 {
803 if ($search) {
804 $fields = $this->searchable;
805 $query->where(function ($query) use ($fields, $search) {
806 $query->where(array_shift($fields), 'LIKE', "%$search%");
807
808 $nameArray = explode(' ', $search);
809 if (count($nameArray) >= 2) {
810 $query->orWhere(function ($q) use ($nameArray) {
811 $fname = array_shift($nameArray);
812 $lastName = implode(' ', $nameArray);
813 $q->where('first_name', 'LIKE', "%$fname%")
814 ->orWhere('last_name', 'LIKE', "%$lastName%");
815 });
816 }
817
818 foreach ($fields as $field) {
819 $query->orWhere($field, 'LIKE', "%$search%");
820 }
821 });
822 }
823
824 return $query;
825 }
826
827 public function getRedirectUrlWithQuery()
828 {
829 $settings = $this->calendar_event->settings;
830
831 $isEnabled = Arr::isTrue($settings, 'custom_redirect.enabled');
832 $redirectUrl = Arr::get($settings, 'custom_redirect.redirect_url', '');
833 $queryString = Arr::get($settings, 'custom_redirect.query_string', '');
834 $isQueryString = Arr::get($settings, 'custom_redirect.is_query_string', 'no') == 'yes';
835
836 if ($isQueryString && $queryString) {
837 if (strpos($redirectUrl, '?')) {
838 $redirectUrl .= '&' . $queryString;
839 } else {
840 $redirectUrl .= '?' . $queryString;
841 }
842 }
843
844 if (!$isEnabled || empty($redirectUrl)) {
845 return '';
846 }
847
848 $redirectUrl = EditorShortCodeParser::parse($redirectUrl, $this);
849
850 $isUrlParser = apply_filters('fluent_booking/will_parse_redirect_url_value', true, $this);
851
852 if ($isUrlParser) {
853 if (strpos($redirectUrl, '=&') || '=' == substr($redirectUrl, -1)) {
854 $urlArray = explode('?', $redirectUrl);
855 $baseUrl = array_shift($urlArray);
856 $query = wp_parse_url($redirectUrl)['query'];
857 $queryParams = explode('&', $query);
858
859 $params = [];
860 foreach ($queryParams as $queryParam) {
861 $paramArray = explode('=', $queryParam);
862 if (!empty($paramArray[1])) {
863 $params[$paramArray[0]] = $paramArray[1];
864 }
865 }
866 $redirectUrl = add_query_arg($params, $baseUrl);
867 }
868 }
869
870 return $redirectUrl;
871 }
872
873 public function getConfirmationUrl()
874 {
875 return add_query_arg([
876 'fluent-booking' => 'booking',
877 'meeting_hash' => $this->hash,
878 'type' => 'confirmation',
879 ], Helper::getBookingReceiptLandingBaseUrl());
880 }
881
882 public function getAdminViewUrl()
883 {
884 return Helper::getAppBaseUrl('scheduled-events?period=upcoming&booking_id=' . $this->id);
885 }
886
887 public function getIcsDownloadUrl()
888 {
889 return add_query_arg([
890 'fluent-booking' => 'booking',
891 'meeting_hash' => $this->hash,
892 'type' => 'confirmation',
893 'ics' => 'download',
894 ], Helper::getBookingReceiptLandingBaseUrl());
895 }
896
897 public function getRescheduleUrl()
898 {
899 return add_query_arg([
900 'fluent-booking' => 'booking',
901 'meeting_hash' => $this->hash,
902 'type' => 'reschedule',
903 ], Helper::getBookingReceiptLandingBaseUrl());
904 }
905
906 public function getCancelUrl()
907 {
908 return add_query_arg([
909 'fluent-booking' => 'booking',
910 'meeting_hash' => $this->hash,
911 'type' => 'cancel',
912 ], Helper::getBookingReceiptLandingBaseUrl());
913 }
914
915 public function hasBookingAccess()
916 {
917 $hostIds = $this->getHostIds();
918 $hasAccess = PermissionManager::userCan('manage_all_bookings');
919 return in_array(get_current_user_id(), $hostIds) || $hasAccess;
920 }
921
922 private function canPerformAction($settings)
923 {
924 if (!in_array($this->status, ['scheduled', 'pending'])) {
925 return false;
926 }
927
928 if (!Arr::isTrue($settings, 'enabled')) {
929 return true;
930 }
931
932 if (Arr::get($settings, 'type') == 'conditional') {
933 $conditionUnit = Arr::get($settings, 'condition.unit');
934 $conditionValue = Arr::get($settings, 'condition.value');
935
936 $bookingStartTime = strtotime($this->start_time);
937 $currentTime = time();
938
939 $conditionTime = $conditionValue * 60;
940 if ($conditionUnit == 'hours') {
941 $conditionTime = $conditionTime * 60;
942 }
943
944 return $bookingStartTime - $currentTime > $conditionTime;
945 }
946
947 return false;
948 }
949
950 public function canCancel()
951 {
952 $settings = $this->calendar_event->getCanNotCancelSettings();
953
954 return $this->canPerformAction($settings);
955 }
956
957 public function canReschedule()
958 {
959 $settings = $this->calendar_event->getCanNotRescheduleSettings();
960
961 return $this->canPerformAction($settings);
962 }
963
964 public function isMultiGuestBooking()
965 {
966 return $this->event_type == 'group' || $this->event_type == 'group_event';
967 }
968
969 public function isRoundRobinBooking()
970 {
971 return $this->event_type == 'round_robin';
972 }
973
974 public function isMultiHostBooking()
975 {
976 return in_array($this->event_type, ['single_event', 'group_event', 'collective']);
977 }
978
979 public function getHostProfiles($public = true)
980 {
981 $hostIds = $this->getHostIds();
982
983 $hosts = [];
984 foreach ($hostIds as $hostId) {
985 $calendar = Calendar::where('user_id', $hostId)->where('type', 'simple')->first();
986 if ($calendar) {
987 $hosts[] = $calendar->getAuthorProfile($public);
988 }
989 }
990
991 return $hosts;
992 }
993
994 public function getInviteePhoneNumber($calendarEvent)
995 {
996 $customFormData = $this->getCustomFormData(false);
997
998 $customFields = BookingFieldService::getBookingFields($calendarEvent, true);
999
1000 foreach ($customFields as $field) {
1001 $fieldValue = Arr::get($customFormData, $field['name']);
1002 if ($fieldValue && $field['type'] == 'phone' && Arr::isTrue($field, 'is_sms_number')) {
1003 return $fieldValue;
1004 }
1005 }
1006
1007 return $this->phone;
1008 }
1009
1010 public function getCancellationMessage()
1011 {
1012 $message = Arr::get($this->calendar_event->settings, 'can_not_cancel.message');
1013
1014 $message = EditorShortCodeParser::parse($message, $this);
1015
1016 if ($message) {
1017 return $message;
1018 }
1019
1020 return __('Sorry! you can not cancel this', 'fluent-booking');
1021 }
1022
1023 public function getRescheduleMessage()
1024 {
1025 $message = Arr::get($this->calendar_event->settings, 'can_not_reschedule.message');
1026
1027 $message = EditorShortCodeParser::parse($message, $this);
1028
1029 if ($message) {
1030 return $message;
1031 }
1032
1033 return __('Sorry! you can not reschedule this', 'fluent-booking');
1034 }
1035
1036 public function getHostDetails($isPublic = true, $hostId = null)
1037 {
1038 $hostId = $hostId ?: $this->host_user_id;
1039
1040 if ($hostId && $user = get_user_by('ID', $hostId)) {
1041 $name = trim($user->first_name . ' ' . $user->last_name);
1042 if (!$name) {
1043 $name = $user->display_name;
1044 }
1045 $data = [
1046 'id' => $user->ID,
1047 'name' => $name,
1048 'email' => $user->user_email,
1049 'first_name' => $user->first_name,
1050 'last_name' => $user->last_name,
1051 'avatar' => Helper::fluentBookingUserAvatar($user->ID, $user)
1052 ];
1053 } else {
1054 $data = $this->calendar->getAuthorProfile(false);
1055 }
1056
1057 if ($isPublic) {
1058 unset($data['email']);
1059 }
1060
1061 return $data;
1062 }
1063
1064 public function getHostsDetails($isPublic = true, $excludeHostId = null)
1065 {
1066 $hostIds = $this->getHostIds();
1067
1068 $hosts = [];
1069 foreach ($hostIds as $hostId) {
1070 if ($hostId != $excludeHostId) {
1071 $hosts[] = $this->getHostDetails($isPublic, $hostId);
1072 }
1073 }
1074
1075 return $hosts;
1076 }
1077
1078 public function getHostTimezone()
1079 {
1080 if ($this->host_user_id) {
1081 $calendar = Calendar::where('user_id', $this->host_user_id)
1082 ->where('type', 'simple')
1083 ->first();
1084
1085 if (!$calendar) {
1086 return '';
1087 }
1088 return $calendar->author_timezone;
1089 }
1090 return '';
1091 }
1092
1093 public function getIcsBookingDescription()
1094 {
1095 $description = str_replace(PHP_EOL, '\\n', $this->getConfirmationData());
1096
1097 if ($this->message) {
1098 $description .= __('Note: ', 'fluent-booking') . '\\n' . $this->message . '\\n' . '\\n';
1099 }
1100
1101 if ($additionalData = $this->getAdditionalData(false)) {
1102 if (!empty($description )) {
1103 $description .= "\\n";
1104 } else {
1105 $description = '';
1106 }
1107
1108 $additionalData = str_replace(PHP_EOL, '\\n', $additionalData);
1109
1110 $description .= $additionalData;
1111 }
1112
1113 return $description;
1114 }
1115
1116 public function getAdditionalData($isHtml = false)
1117 {
1118 $customData = BookingFieldService::getFormattedCustomBookingData($this, $isHtml, true);
1119
1120 if (!$customData) {
1121 return '';
1122 }
1123
1124 if (!$isHtml) {
1125 $lines = array_filter(array_map(function ($data) {
1126 return !empty($data['value']) ? $data['label'] . ': ' . PHP_EOL . esc_html($data['value']) : null;
1127 }, $customData));
1128
1129 return implode(PHP_EOL . PHP_EOL, $lines);
1130 }
1131
1132 $html = '<table>';
1133 foreach ($customData as $data) {
1134 if (empty($data['value'])) {
1135 continue;
1136 }
1137 $html .= '<tr>';
1138 $html .= '<td><b>' . $data['label'] . '</b></td>';
1139 $html .= '<td>' . $data['value'] . '</td>';
1140 $html .= '</tr>';
1141 }
1142 $html .= '</table>';
1143
1144 return $html;
1145 }
1146
1147 public function getConfirmationData($html = false)
1148 {
1149 $author = $this->getHostDetails(false);
1150
1151 $guestName = trim($this->first_name . ' ' . $this->last_name);
1152
1153 $bookingTitle = $this->getBookingTitle();
1154
1155 $separator = $html ? '<br>' : PHP_EOL;
1156
1157 $sections = [
1158 'what' => [
1159 'title' => __('What', 'fluent-booking'),
1160 'content' => $bookingTitle,
1161 ],
1162 'when' => [
1163 'title' => __('When', 'fluent-booking'),
1164 'content' => $this->getFullBookingDateTimeText($this->person_time_zone, !$html) . ' (' . $this->person_time_zone . ')',
1165 ],
1166 'who' => [
1167 'title' => __('Who', 'fluent-booking'),
1168 'content' => $author['name'] . ' - ' . __('Organizer', 'fluent-booking') . $separator . $author['email'] . $separator . $separator . $guestName . $separator . $this->email
1169 ],
1170 'where' => [
1171 'title' => __('Where', 'fluent-booking'),
1172 'content' => $this->getLocationAsText()
1173 ],
1174 ];
1175
1176 if ($html) {
1177 unset($sections['who']);
1178 }
1179
1180 $lines = array_map(function ($section) use ($separator) {
1181 return $section['title'] . ': ' . $separator . esc_html($section['content']);
1182 }, $sections);
1183
1184 return implode($separator . $separator, $lines) . $separator . $separator;
1185 }
1186
1187 public function getMeetingBookmarks($assetsUrl = '')
1188 {
1189 $bookingTitle = $this->getBookingTitle();
1190
1191 $eventTitle = $this->calendar_event->title;
1192
1193 return apply_filters('fluent_booking/meeting_bookmarks', [
1194 'google' => [
1195 'title' => __('Google Calendar', 'fluent-booking'),
1196 'url' => add_query_arg([
1197 'dates' => gmdate('Ymd\THis\Z', strtotime($this->start_time)) . '/' . gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1198 'text' => $bookingTitle,
1199 'details' => $eventTitle,
1200 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1201 ], 'https://calendar.google.com/calendar/r/eventedit'),
1202 'icon' => $assetsUrl . 'images/g-icon.svg'
1203 ],
1204 'outlook' => [
1205 'title' => __('Outlook', 'fluent-booking'),
1206 'url' => add_query_arg([
1207 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1208 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1209 'subject' => $bookingTitle,
1210 'path' => '/calendar/action/compose',
1211 'body' => $eventTitle,
1212 'rru' => 'addevent',
1213 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1214 ], 'https://outlook.live.com/calendar/0/deeplink/compose'),
1215 'icon' => $assetsUrl . 'images/ol-icon.svg'
1216 ],
1217 'msoffice' => [
1218 'title' => __('Microsoft Office', 'fluent-booking'),
1219 'url' => add_query_arg([
1220 'startdt' => gmdate('Ymd\THis\Z', strtotime($this->start_time)),
1221 'enddt' => gmdate('Ymd\THis\Z', strtotime($this->end_time)),
1222 'subject' => $bookingTitle,
1223 'path' => '/calendar/action/compose',
1224 'body' => $eventTitle,
1225 'rru' => 'addevent',
1226 'location' => urlencode(LocationService::getBookingLocationUrl($this)),
1227 ], 'https://outlook.office.com/calendar/0/deeplink/compose'),
1228 'icon' => $assetsUrl . 'images/msoffice.svg'
1229 ],
1230 'other' => [
1231 'title' => __('Other Calendar', 'fluent-booking'),
1232 'url' => $this->getIcsDownloadUrl(),
1233 'icon' => $assetsUrl . 'images/ics.svg'
1234 ]
1235 ], $this);
1236 }
1237
1238 }