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 +427 -99 1.6.0 → 2.5.0 View file →
@@ -1,116 +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 - $query->applyBookingOrderByStatus($period);
83 + $bookings = $query->take($limit)->get();
66 84
67 - $query->groupBy('group_id');
85 + $rows = $bookings->map(function ($booking) {
86 + $row = ExportHelper::mapBooking($booking);
87 + return apply_filters('fluent_booking/booking_export_row', $row, $booking);
88 + })->all();
68 89
69 - $search = Arr::get($filters, 'search');
90 + $rows = apply_filters('fluent_booking/booking_export_columns', $rows, $bookings);
70 91
71 - if (!empty($search)) {
72 - $author = 'all';
73 - $query = $query->orderBy('start_time', 'DESC');
74 - $query = $query->searchBy($search);
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;
105 + } else {
106 + $author = sanitize_text_field($author);
75 107 }
76 108
77 - $schedules = $query->paginate();
109 + $hasPermission = PermissionManager::userCanSeeAllBookings();
78 110
79 - foreach ($schedules as $schedule) {
80 - $this->formatBooking($schedule);
111 + if ($hasPermission) {
112 + return $author;
81 113 }
82 114
83 - $data = [
84 - 'schedules' => $schedules,
85 - 'timezone' => 'UTC'
86 - ];
115 + if (!$author || $author == 'all') {
116 + $authorCalendar = Calendar::where('user_id', get_current_user_id())
117 + ->where('type', 'simple')
118 + ->first();
87 119
88 - $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle();
120 + $author = $authorCalendar ? $authorCalendar->id : '';
121 + }
89 122
90 - if ($author && $author != 'all') {
91 - $slotOptions = CalendarService::getSlotOptions($author);
92 - $data['slot_options'] = $slotOptions;
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';
93 140 }
94 141
95 - if ($request->get('page') == 1) {
96 - $pendingQuery = Booking::query()
97 - ->whereIn('status', ['pending', 'reserved'])
98 - ->distinct('group_id');
99 - if ($author && $author !== 'all') {
100 - $pendingCount = $pendingQuery->where('calendar_id', $author)->count('group_id');
101 - } else {
102 - $pendingCount = $pendingQuery->count('group_id');
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 +
150 + if ($author && $author !== 'all') {
151 + if ($author != 'me') {
152 + $query->where('calendar_id', $author);
103 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 + }
104 161
105 - $data['no_show_count'] = Booking::where('status', 'no_show')->count();
106 - $data['pending_count'] = $pendingCount;
107 - $data['cancelled_count'] = Booking::where('status', 'cancelled')->count();
162 + if (!empty($email) && is_email($email)) {
163 + $query->where('email', $email);
108 164 }
109 165
110 - 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;
111 177 }
112 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 +
113 195 public function patchBooking(Request $request, $bookingId)
114 196 {
115 197 $booking = Booking::findOrFail($bookingId);
116 198 $oldBooking = clone $booking;
@@ -158,10 +240,19 @@
158 240 if (!in_array($value, ['scheduled', 'completed', 'cancelled', 'rejected', 'no_show'])) {
159 241 return $this->sendError(['message' => __('Invalid status', 'fluent-booking')]);
160 242 }
161 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 +
162 248 if ($value == 'scheduled' && $booking->payment_method && $booking->payment_order) {
163 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 +
164 255 $order->total_paid = $order->total_amount;
165 256 $order->completed_at = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
166 257 $order->status = 'paid';
167 258 $order->save();
@@ -176,25 +267,45 @@
176 267 do_action('fluent_booking/log_booking_activity', $this->getConfirmLog($booking->id));
177 268 }
178 269
179 270 if ($value == 'cancelled') {
180 - $cancelReason = sanitize_text_field($data['cancel_reason']);
271 + $cancelReason = sanitize_text_field(Arr::get($data, 'cancel_reason', ''));
181 272 $booking->cancelMeeting($cancelReason, 'host', get_current_user_id());
182 - return [
183 - 'message' => __('The booking has been cancelled', 'fluent-booking')
184 - ];
185 273 }
186 274
187 275 if ($value == 'rejected') {
188 - $rejectReason = sanitize_text_field($data['reject_reason']);
276 + $rejectReason = sanitize_text_field(Arr::get($data, 'reject_reason', ''));
189 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 + }
190 290 return [
191 - 'message' => __('The booking has been rejected', 'fluent-booking')
291 + /* translators: %s: Booking status */
292 + 'message' => sprintf(__('The booking has been %s', 'fluent-booking'), $value)
192 293 ];
193 294 }
194 295
195 - if ($booking->payment_method && Arr::get($data, 'refund_payment') == 'yes' && in_array($value, ['cancelled', 'rejected'])) {
196 - 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 + ];
197 308 }
198 309 }
199 310
200 311 if ($column == 'payment_status') {
@@ -208,8 +319,12 @@
208 319
209 320 if ($value == 'pending') {
210 321 do_action('fluent_booking/log_booking_activity', $this->getPaymentPendingLog($booking->id));
211 322 }
323 +
324 + if ($booking->payment_order) {
325 + do_action('fluent_booking/payment/status_changed', $booking->payment_order, $booking, $value);
326 + }
212 327 }
213 328
214 329 $updateData[$column] = $value;
215 330 $booking->fill($updateData);
@@ -239,11 +354,9 @@
239 354 {
240 355 $booking = Booking::with('calendar_event');
241 356
242 357 if (!PermissionManager::userCanSeeAllBookings()) {
243 - $booking->whereHas('calendar', function ($q) {
244 - $q->where('user_id', get_current_user_id());
245 - });
358 + $booking->whereHostAccess(get_current_user_id());
246 359 }
247 360
248 361 $booking = $booking->findOrFail($bookingId);
249 362 $booking = $this->formatBooking($booking);
@@ -281,9 +394,9 @@
281 394 }
282 395
283 396 public function sendConfirmationEmail(Request $request, $bookingId)
284 397 {
285 - $booking = Booking::with(['calendar', 'calendar_event'])->find($bookingId);
398 + $booking = Booking::with(['calendar', 'calendar_event'])->findOrFail($bookingId);
286 399
287 400 $emailTo = $request->get('email_to', 'guest');
288 401
289 402 $notifications = $booking->calendar_event->getNotifications();
@@ -308,11 +421,9 @@
308 421 {
309 422 $booking = Booking::with('slot');
310 423
311 424 if (!PermissionManager::userCanSeeAllBookings()) {
312 - $booking->whereHas('calendar', function ($q) {
313 - $q->where('user_id', get_current_user_id());
314 - });
425 + $booking->whereHostAccess(get_current_user_id());
315 426 }
316 427
317 428 $booking = $booking->where('group_id', $groupId)->first();
318 429
@@ -320,9 +431,9 @@
320 431 return $this->sendError(['message' => __('Invalid group id or the event is not a group event', 'fluent-booking')]);
321 432 }
322 433
323 434 $attendees = Booking::where('group_id', $booking->group_id);
324 - $search = sanitize_text_field($request->get('search'));
435 + $search = $request->getSafe('search');
325 436
326 437 if (!empty($search)) {
327 438 $attendees = $attendees->searchBy($search);
328 439 }
@@ -338,9 +449,12 @@
338 449 }
339 450
340 451 public function getBookingActivities(Request $request, $bookingId)
341 452 {
453 + $this->resolveOwnedBookingOrFail($bookingId);
454 +
342 455 $activities = BookingActivity::where('booking_id', $bookingId)
456 + ->where('type', '!=', BookingActivity::TYPE_NOTE)
343 457 ->orderBy('id', 'DESC')
344 458 ->get();
345 459
346 460 return [
@@ -349,14 +463,15 @@
349 463 }
350 464
351 465 public function getBookingMetaInfo(Request $request, $bookingId)
352 466 {
353 - $booking = Booking::findOrFail($bookingId);
467 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
354 468
355 469 $activities = BookingActivity::where('booking_id', $booking->id)
470 + ->where('type', '!=', BookingActivity::TYPE_NOTE)
356 471 ->orderBy('id', 'DESC')
357 472 ->get();
358 -
473 +
359 474 $activities->each(function ($activity) {
360 475 $activity->description = wp_unslash($activity->description);
361 476 });
362 477
@@ -362,23 +477,39 @@
362 477
363 478 $sidebarContents = [];
364 479 $mainBodyContents = [];
365 480
366 - if (defined('FLUENTCRM')) {
367 - $profileHtml = fluentcrm_get_crm_profile_html($booking->email, false);
368 - if ($profileHtml) {
369 - $sidebarContents[] = [
370 - 'id' => 'fluent_crm_profule',
371 - 'title' => __('CRM Profile', 'fluent-booking'),
372 - 'content' => $profileHtml
373 - ];
374 - }
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 + ];
375 490 }
376 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 +
377 504 $order = null;
378 - if ($booking->payment_method && $booking->payment_order) {
505 + if ($booking->payment_status && $booking->payment_order) {
379 506 $order = $booking->payment_order;
380 - $order->load(['items', 'transaction']);
507 + $relations = ['items', 'transaction'];
508 + if (method_exists($order, 'discounts')) {
509 + $relations[] = 'discounts';
510 + }
511 + $order->load($relations);
381 512 $order->currency_sign = CurrenciesHelper::getCurrencySign($order->currency);
382 513 }
383 514
384 515 $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta', $mainBodyContents, $booking);
@@ -391,8 +522,198 @@
391 522 'main_body_contents' => $mainBodyContents
392 523 ];
393 524 }
394 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 +
395 716 private function formatBooking(&$booking)
396 717 {
397 718 $autoCompleteTimeOut = (int) Helper::getGlobalAdminSetting('auto_complete_timing', 60) * 60; // 10 minutes
398 719
@@ -399,9 +720,10 @@
399 720 if (in_array($booking->status, ['scheduled', 'pending']) && (time() - strtotime($booking->end_time)) > $autoCompleteTimeOut) {
400 721 $bookingStatus = $booking->status == 'pending' ? 'cancelled' : 'completed';
401 722 $booking->status = $bookingStatus;
402 723 $booking->save();
403 - 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);
404 726 }
405 727
406 728 if ($booking->isMultiHostBooking()) {
407 729 $booking->host_profiles = $booking->getHostProfiles();
@@ -415,8 +737,9 @@
415 737 }
416 738
417 739 $booking->title = $booking->getBookingTitle(true);
418 740 $booking->author = $booking->getHostDetails(false);
741 + $booking->details = $booking->getConfirmationData(true);
419 742 $booking->location = $booking->getLocationDetailsHtml();
420 743 $booking->reschedule_url = $booking->getRescheduleUrl();
421 744 $booking->happening_status = $booking->getOngoingStatus();
422 745 $booking->booking_status_text = $booking->getBookingStatus();
@@ -427,8 +750,19 @@
427 750
428 751 return $booking;
429 752 }
430 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 +
431 765 private function getPaymentPaidLog($bookingId)
432 766 {
433 767 return [
434 768 'booking_id' => $bookingId,
@@ -434,9 +768,9 @@
434 768 'booking_id' => $bookingId,
435 769 'status' => 'closed',
436 770 'type' => 'success',
437 771 'title' => __('Payment Successfully Completed', 'fluent-booking'),
438 - 'description' => __('Payment marked as paid by admin', 'fluent-booking')
772 + 'description' => __('Payment marked as paid by ', 'fluent-booking') . $this->getConfirmedBy()
439 773 ];
440 774 }
441 775
442 776 private function getPaymentPendingLog($bookingId)
@@ -445,25 +779,19 @@
445 779 'booking_id' => $bookingId,
446 780 'status' => 'closed',
447 781 'type' => 'success',
448 782 'title' => __('Payment Successfully Marked as Pending', 'fluent-booking'),
449 - 'description' => __('Payment marked as pending by admin', 'fluent-booking')
783 + 'description' => __('Payment marked as pending by ', 'fluent-booking') . $this->getConfirmedBy()
450 784 ];
451 785 }
452 786
453 787 private function getConfirmLog($bookingId)
454 788 {
455 - $confirmedBy = 'host';
456 - $userId = get_current_user_id();
457 - if ($userId && $user = get_user_by('ID', $userId)) {
458 - $confirmedBy = $user->display_name;
459 - }
460 -
461 789 return [
462 790 'booking_id' => $bookingId,
463 791 'status' => 'closed',
464 792 'type' => 'success',
465 793 'title' => __('Booking Confirmed', 'fluent-booking'),
466 - 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $confirmedBy
794 + 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $this->getConfirmedBy()
467 795 ];
468 796 }
469 797 }