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 +428 -106 1.5.25 → 2.5.0 View file →
@@ -1,126 +1,198 @@
1 1 <?php
2 2
3 3 namespace FluentBooking\App\Http\Controllers;
4 4
5 -use FluentBooking\App\App;
6 5 use FluentBooking\App\Models\Booking;
7 6 use FluentBooking\App\Models\Calendar;
7 +use FluentBooking\App\Models\CalendarSlot;
8 8 use FluentBooking\App\Models\BookingActivity;
9 -use FluentBooking\App\Models\Meta;
9 +use FluentBooking\Framework\Database\Orm\ModelNotFoundException;
10 10 use FluentBooking\App\Services\EmailNotificationService;
11 11 use FluentBooking\App\Services\Helper;
12 +use FluentBooking\Framework\Support\Arr;
12 13 use FluentBooking\App\Services\CurrenciesHelper;
13 -use FluentBooking\Framework\Support\Arr;
14 14 use FluentBooking\Framework\Http\Request\Request;
15 15 use FluentBooking\App\Services\PermissionManager;
16 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;
17 21
18 22 class SchedulesController extends Controller
19 23 {
20 24 public function index(Request $request)
21 25 {
22 - $filters = $request->get('filters', []);
26 + $author = $this->resolveAuthor($request);
23 27
24 - $period = Arr::get($filters, 'period', 'upcoming');
28 + $query = $this->buildSchedulesQuery($request, $author);
25 29
26 - $eventId = Arr::get($filters, 'event');
30 + $query->groupBy('group_id');
27 31
28 - $eventType = Arr::get($filters, 'event_type');
32 + $schedules = $query->paginate();
29 33
30 - $query = Booking::with(['calendar_event']);
34 + foreach ($schedules as $schedule) {
35 + $this->formatBooking($schedule);
36 + }
31 37
32 - $author = Arr::get($filters, 'author');
38 + $data = [
39 + 'schedules' => $schedules,
40 + 'timezone' => 'UTC'
41 + ];
33 42
34 - if ($author !== 'all') {
35 - $author = (int)$author;
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;
36 48 }
37 49
38 - if (!PermissionManager::userCanSeeAllBookings()) {
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 - }
50 + if ($request->get('page') == 1) {
51 + $this->addCountsForFirstPage($author, $data);
47 52 }
48 53
49 - if ($author && $author !== 'all') {
50 - $query->where('calendar_id', $author);
54 + return $data;
55 + }
51 56
52 - if ($eventId && $eventId !== 'all') {
53 - $query->where('event_id', $eventId);
54 - }
57 + public function export(Request $request)
58 + {
59 + $limit = (int) apply_filters('fluent_booking/data_export_limit', 2000);
55 60
56 - if ($eventType && $eventType !== 'all') {
57 - $query->where('event_type', $eventType);
58 - }
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';
59 76 }
60 77
61 - do_action_ref_array('fluent_booking/schedules_query', [&$query]);
78 + $query->with($relations);
62 79
63 - $query->applyComputedStatus($period);
80 + $total = (clone $query)->withoutEagerLoads()->count();
81 + $limited = $total > $limit;
64 82
65 - if ($period == 'upcoming') {
66 - $query = $query->orderBy('start_time', 'ASC');
67 - } else if ($period == 'latest_bookings') {
68 - $query = $query->orderBy('created_at', 'DESC');
69 - } else if ($period == 'no_show') {
70 - $query = $query->orderBy('start_time', 'DESC');
71 - } else if ($period == 'latest_bookings') {
72 - $query = $query->orderBy('id', 'DESC');
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 +
103 + if (is_numeric($author)) {
104 + $author = (int) $author;
73 105 } else {
74 - $query = $query->orderBy('start_time', 'DESC');
106 + $author = sanitize_text_field($author);
75 107 }
76 108
77 - $query->groupBy('group_id');
109 + $hasPermission = PermissionManager::userCanSeeAllBookings();
78 110
79 - $search = Arr::get($filters, 'search');
111 + if ($hasPermission) {
112 + return $author;
113 + }
80 114
81 - if (!empty($search)) {
82 - $author = 'all';
83 - $query = $query->orderBy('start_time', 'DESC');
84 - $query = $query->searchBy($search);
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 : '';
85 121 }
86 122
87 - $schedules = $query->paginate();
123 + return $author;
124 + }
88 125
89 - foreach ($schedules as $schedule) {
90 - $this->formatBooking($schedule);
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';
91 140 }
92 141
93 - $data = [
94 - 'schedules' => $schedules,
95 - 'timezone' => 'UTC'
96 - ];
142 + $query = Booking::with(['calendar_event']);
97 143
98 - $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle();
144 + $hasPermission = PermissionManager::userCanSeeAllBookings();
99 145
100 - if ($author && $author != 'all') {
101 - $slotOptions = CalendarService::getSlotOptions($author);
102 - $data['slot_options'] = $slotOptions;
146 + if (!$hasPermission || $author == 'me') {
147 + $query->where('host_user_id', get_current_user_id());
103 148 }
104 149
105 - if ($request->get('page') == 1) {
106 - $pendingQuery = Booking::query()
107 - ->whereIn('status', ['pending', 'reserved'])
108 - ->distinct('group_id');
109 - if ($author && $author !== 'all') {
110 - $pendingCount = $pendingQuery->where('calendar_id', $author)->count('group_id');
111 - } else {
112 - $pendingCount = $pendingQuery->count('group_id');
150 + if ($author && $author !== 'all') {
151 + if ($author != 'me') {
152 + $query->where('calendar_id', $author);
113 153 }
154 + if ($eventId > 0) {
155 + $query->where('event_id', $eventId);
156 + }
157 + if ($eventType && $eventType !== 'all') {
158 + $query->where('event_type', $eventType);
159 + }
160 + }
114 161
115 - $data['no_show_count'] = Booking::where('status', 'no_show')->count();
116 - $data['pending_count'] = $pendingCount;
117 - $data['cancelled_count'] = Booking::where('status', 'cancelled')->count();
162 + if (!empty($email) && is_email($email)) {
163 + $query->where('email', $email);
118 164 }
119 165
120 - return $data;
166 + do_action_ref_array('fluent_booking/schedules_query', [&$query]);
167 +
168 + $query->applyDateRangeFilter($range);
169 + $query->applyComputedStatus($period);
170 + $query->applyBookingOrderByStatus($period);
171 +
172 + if (!empty($search)) {
173 + $query->searchBy($search);
174 + }
175 +
176 + return $query;
121 177 }
122 178
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');
189 +
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');
193 + }
194 +
123 195 public function patchBooking(Request $request, $bookingId)
124 196 {
125 197 $booking = Booking::findOrFail($bookingId);
126 198 $oldBooking = clone $booking;
@@ -168,10 +240,19 @@
168 240 if (!in_array($value, ['scheduled', 'completed', 'cancelled', 'rejected', 'no_show'])) {
169 241 return $this->sendError(['message' => __('Invalid status', 'fluent-booking')]);
170 242 }
171 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 +
172 248 if ($value == 'scheduled' && $booking->payment_method && $booking->payment_order) {
173 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 +
174 255 $order->total_paid = $order->total_amount;
175 256 $order->completed_at = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
176 257 $order->status = 'paid';
177 258 $order->save();
@@ -186,25 +267,45 @@
186 267 do_action('fluent_booking/log_booking_activity', $this->getConfirmLog($booking->id));
187 268 }
188 269
189 270 if ($value == 'cancelled') {
190 - $cancelReason = sanitize_text_field($data['cancel_reason']);
271 + $cancelReason = sanitize_text_field(Arr::get($data, 'cancel_reason', ''));
191 272 $booking->cancelMeeting($cancelReason, 'host', get_current_user_id());
192 - return [
193 - 'message' => __('The booking has been cancelled', 'fluent-booking')
194 - ];
195 273 }
196 274
197 275 if ($value == 'rejected') {
198 - $rejectReason = sanitize_text_field($data['reject_reason']);
276 + $rejectReason = sanitize_text_field(Arr::get($data, 'reject_reason', ''));
199 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 + }
200 290 return [
201 - 'message' => __('The booking has been rejected', 'fluent-booking')
291 + /* translators: %s: Booking status */
292 + 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value)
202 293 ];
203 294 }
204 295
205 - if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes' && in_array($value, ['cancelled', 'rejected'])) {
206 - 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 + ];
207 308 }
208 309 }
209 310
210 311 if ($column == 'payment_status') {
@@ -218,8 +319,12 @@
218 319
219 320 if ($value == 'pending') {
220 321 do_action('fluent_booking/log_booking_activity', $this->getPaymentPendingLog($booking->id));
221 322 }
323 +
324 + if ($booking->payment_order) {
325 + do_action('fluent_booking/payment/status_changed', $booking->payment_order, $booking, $value);
326 + }
222 327 }
223 328
224 329 $updateData[$column] = $value;
225 330 $booking->fill($updateData);
@@ -249,11 +354,9 @@
249 354 {
250 355 $booking = Booking::with('calendar_event');
251 356
252 357 if (!PermissionManager::userCanSeeAllBookings()) {
253 - $booking->whereHas('calendar', function ($q) {
254 - $q->where('user_id', get_current_user_id());
255 - });
358 + $booking->whereHostAccess(get_current_user_id());
256 359 }
257 360
258 361 $booking = $booking->findOrFail($bookingId);
259 362 $booking = $this->formatBooking($booking);
@@ -291,9 +394,9 @@
291 394 }
292 395
293 396 public function sendConfirmationEmail(Request $request, $bookingId)
294 397 {
295 - $booking = Booking::with(['calendar', 'calendar_event'])->find($bookingId);
398 + $booking = Booking::with(['calendar', 'calendar_event'])->findOrFail($bookingId);
296 399
297 400 $emailTo = $request->get('email_to', 'guest');
298 401
299 402 $notifications = $booking->calendar_event->getNotifications();
@@ -318,11 +421,9 @@
318 421 {
319 422 $booking = Booking::with('slot');
320 423
321 424 if (!PermissionManager::userCanSeeAllBookings()) {
322 - $booking->whereHas('calendar', function ($q) {
323 - $q->where('user_id', get_current_user_id());
324 - });
425 + $booking->whereHostAccess(get_current_user_id());
325 426 }
326 427
327 428 $booking = $booking->where('group_id', $groupId)->first();
328 429
@@ -330,9 +431,9 @@
330 431 return $this->sendError(['message' => __('Invalid group id or the event is not a group event', 'fluent-booking')]);
331 432 }
332 433
333 434 $attendees = Booking::where('group_id', $booking->group_id);
334 - $search = sanitize_text_field($request->get('search'));
435 + $search = $request->getSafe('search');
335 436
336 437 if (!empty($search)) {
337 438 $attendees = $attendees->searchBy($search);
338 439 }
@@ -348,9 +449,12 @@
348 449 }
349 450
350 451 public function getBookingActivities(Request $request, $bookingId)
351 452 {
453 + $this->resolveOwnedBookingOrFail($bookingId);
454 +
352 455 $activities = BookingActivity::where('booking_id', $bookingId)
456 + ->where('type', '!=', BookingActivity::TYPE_NOTE)
353 457 ->orderBy('id', 'DESC')
354 458 ->get();
355 459
356 460 return [
@@ -359,32 +463,53 @@
359 463 }
360 464
361 465 public function getBookingMetaInfo(Request $request, $bookingId)
362 466 {
363 - $booking = Booking::findOrFail($bookingId);
467 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
364 468
365 469 $activities = BookingActivity::where('booking_id', $booking->id)
470 + ->where('type', '!=', BookingActivity::TYPE_NOTE)
366 471 ->orderBy('id', 'DESC')
367 472 ->get();
368 473
474 + $activities->each(function ($activity) {
475 + $activity->description = wp_unslash($activity->description);
476 + });
477 +
369 478 $sidebarContents = [];
370 479 $mainBodyContents = [];
371 480
372 - if (defined('FLUENTCRM')) {
373 - $profileHtml = fluentcrm_get_crm_profile_html($booking->email, false);
374 - if ($profileHtml) {
375 - $sidebarContents[] = [
376 - 'id' => 'fluent_crm_profule',
377 - 'title' => __('CRM Profile', 'fluent-booking'),
378 - 'content' => $profileHtml
379 - ];
380 - }
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 + ];
381 490 }
382 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 +
383 504 $order = null;
384 - if ($booking->payment_method && $booking->payment_order) {
505 + if ($booking->payment_status && $booking->payment_order) {
385 506 $order = $booking->payment_order;
386 - $order->load(['items', 'transaction']);
507 + $relations = ['items', 'transaction'];
508 + if (method_exists($order, 'discounts')) {
509 + $relations[] = 'discounts';
510 + }
511 + $order->load($relations);
387 512 $order->currency_sign = CurrenciesHelper::getCurrencySign($order->currency);
388 513 }
389 514
390 515 $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta', $mainBodyContents, $booking);
@@ -397,8 +522,198 @@
397 522 'main_body_contents' => $mainBodyContents
398 523 ];
399 524 }
400 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 +
401 716 private function formatBooking(&$booking)
402 717 {
403 718 $autoCompleteTimeOut = (int) Helper::getGlobalAdminSetting('auto_complete_timing', 60) * 60; // 10 minutes
404 719
@@ -405,9 +720,10 @@
405 720 if (in_array($booking->status, ['scheduled', 'pending']) && (time() - strtotime($booking->end_time)) > $autoCompleteTimeOut) {
406 721 $bookingStatus = $booking->status == 'pending' ? 'cancelled' : 'completed';
407 722 $booking->status = $bookingStatus;
408 723 $booking->save();
409 - 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);
410 726 }
411 727
412 728 if ($booking->isMultiHostBooking()) {
413 729 $booking->host_profiles = $booking->getHostProfiles();
@@ -421,8 +737,9 @@
421 737 }
422 738
423 739 $booking->title = $booking->getBookingTitle(true);
424 740 $booking->author = $booking->getHostDetails(false);
741 + $booking->details = $booking->getConfirmationData(true);
425 742 $booking->location = $booking->getLocationDetailsHtml();
426 743 $booking->reschedule_url = $booking->getRescheduleUrl();
427 744 $booking->happening_status = $booking->getOngoingStatus();
428 745 $booking->booking_status_text = $booking->getBookingStatus();
@@ -433,8 +750,19 @@
433 750
434 751 return $booking;
435 752 }
436 753
754 + private function getConfirmedBy()
755 + {
756 + $confirmedBy = 'host';
757 + $userId = get_current_user_id();
758 + if ($userId && $user = get_user_by('ID', $userId)) {
759 + $confirmedBy = $user->display_name;
760 + }
761 +
762 + return $confirmedBy;
763 + }
764 +
437 765 private function getPaymentPaidLog($bookingId)
438 766 {
439 767 return [
440 768 'booking_id' => $bookingId,
@@ -440,9 +768,9 @@
440 768 'booking_id' => $bookingId,
441 769 'status' => 'closed',
442 770 'type' => 'success',
443 771 'title' => __('Payment Successfully Completed', 'fluent-booking'),
444 - 'description' => __('Payment marked as paid by admin', 'fluent-booking')
772 + 'description' => __('Payment marked as paid by ', 'fluent-booking') . $this->getConfirmedBy()
445 773 ];
446 774 }
447 775
448 776 private function getPaymentPendingLog($bookingId)
@@ -451,25 +779,19 @@
451 779 'booking_id' => $bookingId,
452 780 'status' => 'closed',
453 781 'type' => 'success',
454 782 'title' => __('Payment Successfully Marked as Pending', 'fluent-booking'),
455 - 'description' => __('Payment marked as pending by admin', 'fluent-booking')
783 + 'description' => __('Payment marked as pending by ', 'fluent-booking') . $this->getConfirmedBy()
456 784 ];
457 785 }
458 786
459 787 private function getConfirmLog($bookingId)
460 788 {
461 - $confirmedBy = 'host';
462 - $userId = get_current_user_id();
463 - if ($userId && $user = get_user_by('ID', $userId)) {
464 - $confirmedBy = $user->display_name;
465 - }
466 -
467 789 return [
468 790 'booking_id' => $bookingId,
469 791 'status' => 'closed',
470 792 'type' => 'success',
471 793 'title' => __('Booking Confirmed', 'fluent-booking'),
472 - 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $confirmedBy
794 + 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $this->getConfirmedBy()
473 795 ];
474 796 }
475 797 }