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 / Models / Booking.php

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

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