PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
← All changes | app/Http/Controllers/SchedulesController.php +409 -98 1.7.0 → 2.5.0 View file →
@@ -3,120 +3,194 @@
3 3 namespace FluentBooking\App\Http\Controllers;
4 4
5 5 use FluentBooking\App\Models\Booking;
6 6 use FluentBooking\App\Models\Calendar;
7 +use FluentBooking\App\Models\CalendarSlot;
7 8 use FluentBooking\App\Models\BookingActivity;
9 +use FluentBooking\Framework\Database\Orm\ModelNotFoundException;
8 10 use FluentBooking\App\Services\EmailNotificationService;
9 11 use FluentBooking\App\Services\Helper;
12 +use FluentBooking\Framework\Support\Arr;
10 13 use FluentBooking\App\Services\CurrenciesHelper;
11 -use FluentBooking\Framework\Support\Arr;
12 14 use FluentBooking\Framework\Http\Request\Request;
13 15 use FluentBooking\App\Services\PermissionManager;
14 16 use FluentBooking\App\Services\CalendarService;
17 +use FluentBooking\App\Services\ExportHelper;
18 +use FluentBooking\App\Services\Integrations\FluentCRM\CrmContactService;
19 +use FluentBooking\App\Services\Integrations\FluentCart\CustomerProfileService;
20 +use FluentCrm\App\Services\PermissionManager as CrmPermissionManager;
15 21
16 22 class SchedulesController extends Controller
17 23 {
18 24 public function index(Request $request)
19 25 {
20 - $filters = $request->get('filters', []);
26 + $author = $this->resolveAuthor($request);
21 27
22 - $period = Arr::get($filters, 'period', 'upcoming');
28 + $query = $this->buildSchedulesQuery($request, $author);
23 29
24 - $eventId = Arr::get($filters, 'event');
30 + $query->groupBy('group_id');
25 31
26 - $eventType = Arr::get($filters, 'event_type');
32 + $schedules = $query->paginate();
27 33
28 - $author = Arr::get($filters, 'author');
34 + foreach ($schedules as $schedule) {
35 + $this->formatBooking($schedule);
36 + }
29 37
30 - $query = Booking::with(['calendar_event']);
38 + $data = [
39 + 'schedules' => $schedules,
40 + 'timezone' => 'UTC'
41 + ];
31 42
43 + $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle();
44 +
45 + if ($author == 'me') {
46 + $slotOptions = CalendarService::getSlotOptions(null, get_current_user_id());
47 + $data['slot_options'] = $slotOptions;
48 + }
49 +
50 + if ($request->get('page') == 1) {
51 + $this->addCountsForFirstPage($author, $data);
52 + }
53 +
54 + return $data;
55 + }
56 +
57 + public function export(Request $request)
58 + {
59 + $limit = (int) apply_filters('fluent_booking/data_export_limit', 2000);
60 +
61 + $author = $this->resolveAuthor($request);
62 +
63 + $query = $this->buildSchedulesQuery($request, $author);
64 +
65 + $relations = [
66 + 'calendar_event',
67 + 'user',
68 + 'booking_activities' => function ($q) {
69 + $q->where('type', 'cancel_reason');
70 + },
71 + 'booking_meta',
72 + ];
73 +
74 + if (defined('FLUENT_BOOKING_PRO_DIR_FILE')) {
75 + $relations[] = 'payment_order.transaction';
76 + }
77 +
78 + $query->with($relations);
79 +
80 + $total = (clone $query)->withoutEagerLoads()->count();
81 + $limited = $total > $limit;
82 +
83 + $bookings = $query->take($limit)->get();
84 +
85 + $rows = $bookings->map(function ($booking) {
86 + $row = ExportHelper::mapBooking($booking);
87 + return apply_filters('fluent_booking/booking_export_row', $row, $booking);
88 + })->all();
89 +
90 + $rows = apply_filters('fluent_booking/booking_export_columns', $rows, $bookings);
91 +
92 + return [
93 + 'bookings' => $rows,
94 + 'limited' => $limited,
95 + 'total' => $total,
96 + ];
97 + }
98 +
99 + protected function resolveAuthor(Request $request)
100 + {
101 + $author = Arr::get($request->get('filters', []), 'author');
102 +
32 103 if (is_numeric($author)) {
33 - $author = (int)$author;
104 + $author = (int) $author;
105 + } else {
106 + $author = sanitize_text_field($author);
34 107 }
35 108
36 109 $hasPermission = PermissionManager::userCanSeeAllBookings();
37 110
38 - if (!$hasPermission) {
39 - if (!$author || $author == 'all') {
40 - $authorCalendar = Calendar::where('user_id', get_current_user_id())
41 - ->where('type', 'simple')
42 - ->first();
43 - if($authorCalendar) {
44 - $author = $authorCalendar->id;
45 - }
46 - }
111 + if ($hasPermission) {
112 + return $author;
47 113 }
48 114
115 + if (!$author || $author == 'all') {
116 + $authorCalendar = Calendar::where('user_id', get_current_user_id())
117 + ->where('type', 'simple')
118 + ->first();
119 +
120 + $author = $authorCalendar ? $authorCalendar->id : '';
121 + }
122 +
123 + return $author;
124 + }
125 +
126 + protected function buildSchedulesQuery(Request $request, $author = null)
127 + {
128 + $filters = $request->get('filters', []);
129 + $search = $request->getSafe('search');
130 + $eventVal = Arr::get($filters, 'event');
131 + $eventId = is_numeric($eventVal) ? (int) $eventVal : 0;
132 + $eventType = sanitize_text_field(Arr::get($filters, 'event_type'));
133 + $period = sanitize_text_field(Arr::get($filters, 'period', 'upcoming'));
134 + $range = array_map('sanitize_text_field', Arr::get($filters, 'range', []));
135 + $email = sanitize_email(Arr::get($filters, 'email', ''));
136 +
137 + $allowedPeriods = ['upcoming', 'completed', 'cancelled', 'pending', 'no_show', 'latest_bookings', 'all'];
138 + if (!in_array($period, $allowedPeriods, true)) {
139 + $period = 'upcoming';
140 + }
141 +
142 + $query = Booking::with(['calendar_event']);
143 +
144 + $hasPermission = PermissionManager::userCanSeeAllBookings();
145 +
146 + if (!$hasPermission || $author == 'me') {
147 + $query->where('host_user_id', get_current_user_id());
148 + }
149 +
49 150 if ($author && $author !== 'all') {
50 - if ($author == 'me') {
51 - $query->where('host_user_id', get_current_user_id());
52 - } else {
151 + if ($author != 'me') {
53 152 $query->where('calendar_id', $author);
54 - if (!$hasPermission) {
55 - $query->where('host_user_id', get_current_user_id());
56 - }
57 153 }
58 -
59 - if ($eventId && $eventId !== 'all') {
154 + if ($eventId > 0) {
60 155 $query->where('event_id', $eventId);
61 156 }
62 -
63 157 if ($eventType && $eventType !== 'all') {
64 158 $query->where('event_type', $eventType);
65 159 }
66 160 }
67 161
162 + if (!empty($email) && is_email($email)) {
163 + $query->where('email', $email);
164 + }
165 +
68 166 do_action_ref_array('fluent_booking/schedules_query', [&$query]);
69 167
168 + $query->applyDateRangeFilter($range);
70 169 $query->applyComputedStatus($period);
71 -
72 170 $query->applyBookingOrderByStatus($period);
73 171
74 - $query->groupBy('group_id');
75 -
76 - $search = Arr::get($filters, 'search');
77 -
78 172 if (!empty($search)) {
79 - $query = $query->orderBy('start_time', 'DESC');
80 - $query = $query->searchBy($search);
173 + $query->searchBy($search);
81 174 }
82 175
83 - $schedules = $query->paginate();
176 + return $query;
177 + }
84 178
85 - foreach ($schedules as $schedule) {
86 - $this->formatBooking($schedule);
87 - }
179 + private function addCountsForFirstPage($author, &$data)
180 + {
181 + $bookingQuery = Booking::query()
182 + ->when(!PermissionManager::userCanSeeAllBookings() || $author == 'me', function($query) {
183 + return $query->where('host_user_id', get_current_user_id());
184 + })
185 + ->when($author && is_numeric($author), function($query) use ($author) {
186 + return $query->where('calendar_id', $author);
187 + })
188 + ->distinct('group_id');
88 189
89 - $data = [
90 - 'schedules' => $schedules,
91 - 'timezone' => 'UTC'
92 - ];
93 -
94 - $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle();
95 -
96 - if ($author == 'me') {
97 - $slotOptions = CalendarService::getSlotOptions(null, get_current_user_id());
98 - $data['slot_options'] = $slotOptions;
99 - }
100 -
101 - if ($request->get('page') == 1) {
102 - $pendingCount = Booking::query()
103 - ->whereIn('status', ['pending', 'reserved'])
104 - ->distinct('group_id')
105 - ->when($author == 'me', function($query) {
106 - return $query->where('host_user_id', get_current_user_id());
107 - })
108 - ->when($author && is_numeric($author), function($query) use ($author) {
109 - return $query->where('calendar_id', $author);
110 - })
111 - ->count('group_id');
112 -
113 - $data['pending_count'] = $pendingCount;
114 - $data['no_show_count'] = Booking::where('status', 'no_show')->count();
115 - $data['cancelled_count'] = Booking::where('status', 'cancelled')->count();
116 - }
117 -
118 - return $data;
190 + $data['pending_count'] = (clone $bookingQuery)->whereIn('status', ['pending', 'reserved'])->count('group_id');
191 + $data['no_show_count'] = (clone $bookingQuery)->where('status', 'no_show')->count('group_id');
192 + $data['cancelled_count'] = (clone $bookingQuery)->where('status', 'cancelled')->count('group_id');
119 193 }
120 194
121 195 public function patchBooking(Request $request, $bookingId)
122 196 {
@@ -166,10 +240,19 @@
166 240 if (!in_array($value, ['scheduled', 'completed', 'cancelled', 'rejected', 'no_show'])) {
167 241 return $this->sendError(['message' => __('Invalid status', 'fluent-booking')]);
168 242 }
169 243
244 + if (in_array($booking->status, ['cancelled', 'rejected'])) {
245 + return $this->sendError(['message' => __('A cancelled or rejected booking can not be changed', 'fluent-booking')]);
246 + }
247 +
170 248 if ($value == 'scheduled' && $booking->payment_method && $booking->payment_order) {
171 249 $order = $booking->payment_order;
250 +
251 + if (in_array($order->status, ['refunded', 'partially-refunded'])) {
252 + return $this->sendError(['message' => __('A refunded payment can not be marked as paid', 'fluent-booking')]);
253 + }
254 +
172 255 $order->total_paid = $order->total_amount;
173 256 $order->completed_at = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
174 257 $order->status = 'paid';
175 258 $order->save();
@@ -184,25 +267,45 @@
184 267 do_action('fluent_booking/log_booking_activity', $this->getConfirmLog($booking->id));
185 268 }
186 269
187 270 if ($value == 'cancelled') {
188 - $cancelReason = sanitize_text_field($data['cancel_reason']);
271 + $cancelReason = sanitize_text_field(Arr::get($data, 'cancel_reason', ''));
189 272 $booking->cancelMeeting($cancelReason, 'host', get_current_user_id());
190 - return [
191 - 'message' => __('The booking has been cancelled', 'fluent-booking')
192 - ];
193 273 }
194 274
195 275 if ($value == 'rejected') {
196 - $rejectReason = sanitize_text_field($data['reject_reason']);
276 + $rejectReason = sanitize_text_field(Arr::get($data, 'reject_reason', ''));
197 277 $booking->rejectMeeting($rejectReason, get_current_user_id());
278 + }
279 +
280 + if (in_array($value, ['cancelled', 'rejected'])) {
281 + // cancelMeeting() and rejectMeeting() refuse some statuses without changing the booking.
282 + if ($booking->status != $value) {
283 + /* translators: %s: Booking status */
284 + return $this->sendError(['message' => sprintf(__('This booking can not be %s', 'fluent-booking'), $value)]);
285 + }
286 +
287 + if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes') {
288 + do_action('fluent_booking/refund_payment_' . $booking->payment_method, $booking, $booking->calendar_event);
289 + }
198 290 return [
199 - 'message' => __('The booking has been rejected', 'fluent-booking')
291 + /* translators: %s: Booking status */
292 + 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value)
200 293 ];
201 294 }
202 295
203 - if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes' && in_array($value, ['cancelled', 'rejected'])) {
204 - do_action('fluent_booking/refund_payment_' . $booking->payment_method, $booking, $booking->calendar_event);
296 + $updateAll = Arr::isTrue($data, 'update_all') && $booking->isMultiGuestBooking();
297 +
298 + if ($updateAll && in_array($value, ['no_show', 'completed'])) {
299 + Booking::where('event_id', $booking->event_id)
300 + ->where('group_id', $booking->group_id)
301 + ->update([
302 + 'status' => $value
303 + ]);
304 + return [
305 + /* translators: %s: Booking status */
306 + 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value)
307 + ];
205 308 }
206 309 }
207 310
208 311 if ($column == 'payment_status') {
@@ -218,9 +321,9 @@
218 321 do_action('fluent_booking/log_booking_activity', $this->getPaymentPendingLog($booking->id));
219 322 }
220 323
221 324 if ($booking->payment_order) {
222 - do_action('fluent_booking/payment/status_changed', $booking->payment_order, $booking);
325 + do_action('fluent_booking/payment/status_changed', $booking->payment_order, $booking, $value);
223 326 }
224 327 }
225 328
226 329 $updateData[$column] = $value;
@@ -251,11 +354,9 @@
251 354 {
252 355 $booking = Booking::with('calendar_event');
253 356
254 357 if (!PermissionManager::userCanSeeAllBookings()) {
255 - $booking->whereHas('calendar', function ($q) {
256 - $q->where('user_id', get_current_user_id());
257 - });
358 + $booking->whereHostAccess(get_current_user_id());
258 359 }
259 360
260 361 $booking = $booking->findOrFail($bookingId);
261 362 $booking = $this->formatBooking($booking);
@@ -293,9 +394,9 @@
293 394 }
294 395
295 396 public function sendConfirmationEmail(Request $request, $bookingId)
296 397 {
297 - $booking = Booking::with(['calendar', 'calendar_event'])->find($bookingId);
398 + $booking = Booking::with(['calendar', 'calendar_event'])->findOrFail($bookingId);
298 399
299 400 $emailTo = $request->get('email_to', 'guest');
300 401
301 402 $notifications = $booking->calendar_event->getNotifications();
@@ -320,11 +421,9 @@
320 421 {
321 422 $booking = Booking::with('slot');
322 423
323 424 if (!PermissionManager::userCanSeeAllBookings()) {
324 - $booking->whereHas('calendar', function ($q) {
325 - $q->where('user_id', get_current_user_id());
326 - });
425 + $booking->whereHostAccess(get_current_user_id());
327 426 }
328 427
329 428 $booking = $booking->where('group_id', $groupId)->first();
330 429
@@ -332,9 +431,9 @@
332 431 return $this->sendError(['message' => __('Invalid group id or the event is not a group event', 'fluent-booking')]);
333 432 }
334 433
335 434 $attendees = Booking::where('group_id', $booking->group_id);
336 - $search = sanitize_text_field($request->get('search'));
435 + $search = $request->getSafe('search');
337 436
338 437 if (!empty($search)) {
339 438 $attendees = $attendees->searchBy($search);
340 439 }
@@ -350,9 +449,12 @@
350 449 }
351 450
352 451 public function getBookingActivities(Request $request, $bookingId)
353 452 {
453 + $this->resolveOwnedBookingOrFail($bookingId);
454 +
354 455 $activities = BookingActivity::where('booking_id', $bookingId)
456 + ->where('type', '!=', BookingActivity::TYPE_NOTE)
355 457 ->orderBy('id', 'DESC')
356 458 ->get();
357 459
358 460 return [
@@ -361,14 +463,15 @@
361 463 }
362 464
363 465 public function getBookingMetaInfo(Request $request, $bookingId)
364 466 {
365 - $booking = Booking::findOrFail($bookingId);
467 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
366 468
367 469 $activities = BookingActivity::where('booking_id', $booking->id)
470 + ->where('type', '!=', BookingActivity::TYPE_NOTE)
368 471 ->orderBy('id', 'DESC')
369 472 ->get();
370 -
473 +
371 474 $activities->each(function ($activity) {
372 475 $activity->description = wp_unslash($activity->description);
373 476 });
374 477
@@ -374,23 +477,39 @@
374 477
375 478 $sidebarContents = [];
376 479 $mainBodyContents = [];
377 480
378 - if (defined('FLUENTCRM')) {
379 - $profileHtml = fluentcrm_get_crm_profile_html($booking->email, false);
380 - if ($profileHtml) {
381 - $sidebarContents[] = [
382 - 'id' => 'fluent_crm_profule',
383 - 'title' => __('CRM Profile', 'fluent-booking'),
384 - 'content' => $profileHtml
385 - ];
386 - }
481 + $canReadCrm = CrmContactService::isActive() && CrmPermissionManager::currentUserCan('fcrm_read_contacts');
482 + $crmProfile = $canReadCrm ? CrmContactService::getProfileData($booking->email) : null;
483 + if ($crmProfile) {
484 + $sidebarContents[] = [
485 + 'id' => 'fluent_crm_profule',
486 + 'title' => __('CRM Profile', 'fluent-booking'),
487 + 'type' => 'crm_profile',
488 + 'profile' => $crmProfile,
489 + ];
387 490 }
388 491
492 + $cartProfile = CustomerProfileService::canView()
493 + ? CustomerProfileService::getProfileData($booking->email)
494 + : null;
495 + if ($cartProfile) {
496 + $sidebarContents[] = [
497 + 'id' => 'fluent_cart_profile',
498 + 'title' => __('Cart Profile', 'fluent-booking'),
499 + 'type' => 'cart_profile',
500 + 'profile' => $cartProfile,
501 + ];
502 + }
503 +
389 504 $order = null;
390 - if ($booking->payment_method && $booking->payment_order) {
505 + if ($booking->payment_status && $booking->payment_order) {
391 506 $order = $booking->payment_order;
392 - $order->load(['items', 'transaction']);
507 + $relations = ['items', 'transaction'];
508 + if (method_exists($order, 'discounts')) {
509 + $relations[] = 'discounts';
510 + }
511 + $order->load($relations);
393 512 $order->currency_sign = CurrenciesHelper::getCurrencySign($order->currency);
394 513 }
395 514
396 515 $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta', $mainBodyContents, $booking);
@@ -403,8 +522,198 @@
403 522 'main_body_contents' => $mainBodyContents
404 523 ];
405 524 }
406 525
526 + /**
527 + * Return the CRM contact state for a booking plus the full tag/list option
528 + * sets, so the admin can manage tags/lists inline from the CRM Profile card.
529 + */
530 + public function getCrmContact(Request $request, $bookingId)
531 + {
532 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
533 +
534 + if (!CrmContactService::isActive()) {
535 + return $this->sendError([
536 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
537 + ]);
538 + }
539 +
540 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
541 + return $this->sendError([
542 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
543 + ], 403);
544 + }
545 +
546 + $state = CrmContactService::getContactState($booking->email);
547 +
548 + if (!$state) {
549 + return $this->sendError([
550 + 'message' => __('No CRM contact found for this booking.', 'fluent-booking')
551 + ], 404);
552 + }
553 +
554 + return $state;
555 + }
556 +
557 + /**
558 + * Guest-field prefill for the CRM "Book Appointment" action.
559 + * Keyed by contact id, so it carries its own CRM-read gate.
560 + */
561 + public function getCrmContactPrefill(Request $request)
562 + {
563 + if (!CrmContactService::isActive()) {
564 + return $this->sendError([
565 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
566 + ]);
567 + }
568 +
569 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
570 + return $this->sendError([
571 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
572 + ], 403);
573 + }
574 +
575 + $contactId = absint($request->get('contact_id'));
576 +
577 + if (!$contactId) {
578 + return $this->sendError([
579 + 'message' => __('Invalid contact.', 'fluent-booking')
580 + ], 422);
581 + }
582 +
583 + $prefill = CrmContactService::getBookingPrefillData($contactId);
584 +
585 + if (!$prefill) {
586 + return $this->sendError([
587 + 'message' => __('No CRM contact found.', 'fluent-booking')
588 + ], 404);
589 + }
590 +
591 + return [
592 + 'prefill' => $prefill,
593 + ];
594 + }
595 +
596 + /**
597 + * Typeahead search for the admin booking modal's CRM contact picker.
598 + * Same intersection gate as getCrmContactPrefill: booking access (policy) AND fcrm_read_contacts.
599 + */
600 + public function searchCrmContacts(Request $request)
601 + {
602 + if (!CrmContactService::isActive()) {
603 + return $this->sendError([
604 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
605 + ]);
606 + }
607 +
608 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
609 + return $this->sendError([
610 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
611 + ], 403);
612 + }
613 +
614 + $search = sanitize_text_field($request->get('search', ''));
615 +
616 + return [
617 + 'contacts' => CrmContactService::searchContacts($search),
618 + ];
619 + }
620 +
621 + /**
622 + * Bounded, searchable tag/list options for the CRM Profile picker.
623 + */
624 + public function getCrmOptions(Request $request, $bookingId)
625 + {
626 + $this->resolveOwnedBookingOrFail($bookingId);
627 +
628 + if (!CrmContactService::isActive()) {
629 + return $this->sendError([
630 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
631 + ]);
632 + }
633 +
634 + if (!CrmPermissionManager::currentUserCan('fcrm_read_contacts')) {
635 + return $this->sendError([
636 + 'message' => __('You do not have permission to read CRM contacts.', 'fluent-booking')
637 + ], 403);
638 + }
639 +
640 + $type = $request->get('type') === 'lists' ? 'lists' : 'tags';
641 + $search = sanitize_text_field($request->get('search', ''));
642 +
643 + return [
644 + 'options' => CrmContactService::getOptions($type, $search),
645 + ];
646 + }
647 +
648 + public function updateCrmTags(Request $request, $bookingId)
649 + {
650 + return $this->updateCrmTaxonomy($request, $bookingId, 'tags');
651 + }
652 +
653 + public function updateCrmLists(Request $request, $bookingId)
654 + {
655 + return $this->updateCrmTaxonomy($request, $bookingId, 'lists');
656 + }
657 +
658 + private function updateCrmTaxonomy(Request $request, $bookingId, $type)
659 + {
660 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
661 +
662 + if (!CrmContactService::isActive()) {
663 + return $this->sendError([
664 + 'message' => __('FluentCRM is not active.', 'fluent-booking')
665 + ]);
666 + }
667 +
668 + if (!CrmPermissionManager::currentUserCan('fcrm_manage_contacts')) {
669 + return $this->sendError([
670 + 'message' => __('You do not have permission to manage CRM contacts.', 'fluent-booking')
671 + ], 403);
672 + }
673 +
674 + $requestKey = $type === 'tags' ? 'tag_ids' : 'list_ids';
675 + $desired = (array) $request->get($requestKey, []);
676 +
677 + $result = CrmContactService::syncTaxonomy($booking->email, $type, $desired);
678 +
679 + if ($result === null) {
680 + return $this->sendError([
681 + 'message' => __('No CRM contact found for this booking.', 'fluent-booking')
682 + ], 404);
683 + }
684 +
685 + return $this->sendSuccess(array_merge([
686 + 'message' => __('CRM contact updated successfully.', 'fluent-booking'),
687 + ], $result));
688 + }
689 +
690 + private function resolveOwnedBookingOrFail($bookingId)
691 + {
692 + $booking = Booking::findOrFail($bookingId);
693 +
694 + if (PermissionManager::userCanSeeAllBookings()) {
695 + return $booking;
696 + }
697 +
698 + $allowedIds = $booking->getHostIds();
699 +
700 + if (PermissionManager::userCan('manage_own_calendar')) {
701 + if ($event = CalendarSlot::find($booking->event_id)) {
702 + $allowedIds = array_merge($allowedIds, $event->getHostIds());
703 + }
704 + }
705 +
706 + if (in_array(get_current_user_id(), $allowedIds)) {
707 + return $booking;
708 + }
709 +
710 + $exception = new ModelNotFoundException();
711 + $exception->setModel(Booking::class, [$bookingId]);
712 + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
713 + throw $exception;
714 + }
715 +
407 716 private function formatBooking(&$booking)
408 717 {
409 718 $autoCompleteTimeOut = (int) Helper::getGlobalAdminSetting('auto_complete_timing', 60) * 60; // 10 minutes
410 719
@@ -411,9 +720,10 @@
411 720 if (in_array($booking->status, ['scheduled', 'pending']) && (time() - strtotime($booking->end_time)) > $autoCompleteTimeOut) {
412 721 $bookingStatus = $booking->status == 'pending' ? 'cancelled' : 'completed';
413 722 $booking->status = $bookingStatus;
414 723 $booking->save();
415 - do_action('fluent_booking/booking_schedule_' . $bookingStatus, $booking, $booking->calendar_event);
724 + $hookName = $bookingStatus === 'cancelled' ? 'auto_cancelled' : $bookingStatus;
725 + do_action('fluent_booking/booking_schedule_' . $hookName, $booking, $booking->calendar_event);
416 726 }
417 727
418 728 if ($booking->isMultiHostBooking()) {
419 729 $booking->host_profiles = $booking->getHostProfiles();
@@ -427,8 +737,9 @@
427 737 }
428 738
429 739 $booking->title = $booking->getBookingTitle(true);
430 740 $booking->author = $booking->getHostDetails(false);
741 + $booking->details = $booking->getConfirmationData(true);
431 742 $booking->location = $booking->getLocationDetailsHtml();
432 743 $booking->reschedule_url = $booking->getRescheduleUrl();
433 744 $booking->happening_status = $booking->getOngoingStatus();
434 745 $booking->booking_status_text = $booking->getBookingStatus();