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

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