PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.2.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.2.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 2.2.0, at app/Models/Booking.php

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