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

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