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 +457 -108 1.5.10 → 2.5.0 View file →
@@ -1,125 +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 - $authorCalendar = Calendar::where('user_id', get_current_user_id())
40 - ->where('type', 'simple')
41 - ->first();
50 + if ($request->get('page') == 1) {
51 + $this->addCountsForFirstPage($author, $data);
52 + }
42 53
43 - if($authorCalendar) {
44 - $author = $authorCalendar->id;
45 - }
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';
46 76 }
47 77
48 - if ($author && $author !== 'all') {
49 - $query->where('calendar_id', $author);
78 + $query->with($relations);
50 79
51 - if ($eventId && $eventId !== 'all') {
52 - $query->where('event_id', $eventId);
53 - }
80 + $total = (clone $query)->withoutEagerLoads()->count();
81 + $limited = $total > $limit;
54 82
55 - if ($eventType && $eventType !== 'all') {
56 - $query->where('event_type', $eventType);
57 - }
58 - }
83 + $bookings = $query->take($limit)->get();
59 84
60 - do_action_ref_array('fluent_booking/schedules_query', [&$query]);
85 + $rows = $bookings->map(function ($booking) {
86 + $row = ExportHelper::mapBooking($booking);
87 + return apply_filters('fluent_booking/booking_export_row', $row, $booking);
88 + })->all();
61 89
62 - $query->applyComputedStatus($period);
90 + $rows = apply_filters('fluent_booking/booking_export_columns', $rows, $bookings);
63 91
64 - if ($period == 'upcoming') {
65 - $query = $query->orderBy('start_time', 'ASC');
66 - } else if ($period == 'latest_bookings') {
67 - $query = $query->orderBy('created_at', 'DESC');
68 - } else if ($period == 'no_show') {
69 - $query = $query->orderBy('start_time', 'DESC');
70 - } else if ($period == 'latest_bookings') {
71 - $query = $query->orderBy('id', 'DESC');
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;
72 105 } else {
73 - $query = $query->orderBy('start_time', 'DESC');
106 + $author = sanitize_text_field($author);
74 107 }
75 108
76 - $query->groupBy('group_id');
109 + $hasPermission = PermissionManager::userCanSeeAllBookings();
77 110
78 - $search = Arr::get($filters, 'search');
111 + if ($hasPermission) {
112 + return $author;
113 + }
79 114
80 - if (!empty($search)) {
81 - $author = 'all';
82 - $query = $query->orderBy('start_time', 'DESC');
83 - $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 : '';
84 121 }
85 122
86 - $schedules = $query->paginate();
123 + return $author;
124 + }
87 125
88 - foreach ($schedules as $schedule) {
89 - $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';
90 140 }
91 141
92 - $data = [
93 - 'schedules' => $schedules,
94 - 'timezone' => 'UTC'
95 - ];
142 + $query = Booking::with(['calendar_event']);
96 143
97 - $data['calendar_event_lists'] = CalendarService::getCalendarOptionsByTitle();
144 + $hasPermission = PermissionManager::userCanSeeAllBookings();
98 145
99 - if ($author && $author != 'all') {
100 - $slotOptions = CalendarService::getSlotOptions($author);
101 - $data['slot_options'] = $slotOptions;
146 + if (!$hasPermission || $author == 'me') {
147 + $query->where('host_user_id', get_current_user_id());
102 148 }
103 149
104 - if ($request->get('page') == 1) {
105 - $pendingQuery = Booking::query()
106 - ->whereIn('status', ['pending', 'reserved'])
107 - ->distinct('group_id');
108 - if ($author && $author !== 'all') {
109 - $pendingCount = $pendingQuery->where('calendar_id', $author)->count('group_id');
110 - } else {
111 - $pendingCount = $pendingQuery->count('group_id');
150 + if ($author && $author !== 'all') {
151 + if ($author != 'me') {
152 + $query->where('calendar_id', $author);
112 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 + }
113 161
114 - $data['no_show_count'] = Booking::where('status', 'no_show')->count();
115 - $data['pending_count'] = $pendingCount;
116 - $data['cancelled_count'] = Booking::where('status', 'cancelled')->count();
162 + if (!empty($email) && is_email($email)) {
163 + $query->where('email', $email);
117 164 }
118 165
119 - 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;
120 177 }
121 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 +
122 195 public function patchBooking(Request $request, $bookingId)
123 196 {
124 197 $booking = Booking::findOrFail($bookingId);
125 198 $oldBooking = clone $booking;
@@ -145,9 +218,10 @@
145 218 'email',
146 219 'phone',
147 220 'first_name',
148 221 'last_name',
149 - 'status'
222 + 'status',
223 + 'payment_status'
150 224 ];
151 225
152 226 if (!in_array($column, $validColumns)) {
153 227 return $this->sendError(['message' => __('Invalid column', 'fluent-booking')]);
@@ -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();
@@ -175,9 +258,9 @@
175 258 $order->save();
176 259
177 260 $updateData['payment_status'] = 'paid';
178 261
179 - do_action('fluent_booking/log_booking_activity', $this->getPaymentLog($booking->id));
262 + do_action('fluent_booking/log_booking_activity', $this->getPaymentPaidLog($booking->id));
180 263
181 264 do_action('fluent_booking/payment/update_payment_status_paid', $booking);
182 265
183 266 } else if ($value == 'scheduled') {
@@ -184,28 +267,66 @@
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
311 + if ($column == 'payment_status') {
312 + if (!in_array($value, ['pending', 'paid'])) {
313 + return $this->sendError(['message' => __('Invalid payment status', 'fluent-booking')]);
314 + }
315 +
316 + if ($value == 'paid') {
317 + do_action('fluent_booking/log_booking_activity', $this->getPaymentPaidLog($booking->id));
318 + }
319 +
320 + if ($value == 'pending') {
321 + do_action('fluent_booking/log_booking_activity', $this->getPaymentPendingLog($booking->id));
322 + }
323 +
324 + if ($booking->payment_order) {
325 + do_action('fluent_booking/payment/status_changed', $booking->payment_order, $booking, $value);
326 + }
327 + }
328 +
208 329 $updateData[$column] = $value;
209 330 $booking->fill($updateData);
210 331 $booking->save();
211 332
@@ -224,9 +345,9 @@
224 345 do_action('fluent_booking/after_patch_booking_' . $column, $booking, $booking->calendar_event, $oldBooking->{$column});
225 346
226 347 return [
227 348 /* translators: Updated column name */
228 - 'message' => sprintf(__('%s has been updated', 'fluent-booking'), $column)
349 + 'message' => sprintf(__('%s has been updated', 'fluent-booking'), ucfirst($column))
229 350 ];
230 351 }
231 352
232 353 public function getBooking(Request $request, $bookingId)
@@ -233,11 +354,9 @@
233 354 {
234 355 $booking = Booking::with('calendar_event');
235 356
236 357 if (!PermissionManager::userCanSeeAllBookings()) {
237 - $booking->whereHas('calendar', function ($q) {
238 - $q->where('user_id', get_current_user_id());
239 - });
358 + $booking->whereHostAccess(get_current_user_id());
240 359 }
241 360
242 361 $booking = $booking->findOrFail($bookingId);
243 362 $booking = $this->formatBooking($booking);
@@ -275,9 +394,9 @@
275 394 }
276 395
277 396 public function sendConfirmationEmail(Request $request, $bookingId)
278 397 {
279 - $booking = Booking::with(['calendar', 'calendar_event'])->find($bookingId);
398 + $booking = Booking::with(['calendar', 'calendar_event'])->findOrFail($bookingId);
280 399
281 400 $emailTo = $request->get('email_to', 'guest');
282 401
283 402 $notifications = $booking->calendar_event->getNotifications();
@@ -302,11 +421,9 @@
302 421 {
303 422 $booking = Booking::with('slot');
304 423
305 424 if (!PermissionManager::userCanSeeAllBookings()) {
306 - $booking->whereHas('calendar', function ($q) {
307 - $q->where('user_id', get_current_user_id());
308 - });
425 + $booking->whereHostAccess(get_current_user_id());
309 426 }
310 427
311 428 $booking = $booking->where('group_id', $groupId)->first();
312 429
@@ -314,9 +431,9 @@
314 431 return $this->sendError(['message' => __('Invalid group id or the event is not a group event', 'fluent-booking')]);
315 432 }
316 433
317 434 $attendees = Booking::where('group_id', $booking->group_id);
318 - $search = sanitize_text_field($request->get('search'));
435 + $search = $request->getSafe('search');
319 436
320 437 if (!empty($search)) {
321 438 $attendees = $attendees->searchBy($search);
322 439 }
@@ -332,9 +449,12 @@
332 449 }
333 450
334 451 public function getBookingActivities(Request $request, $bookingId)
335 452 {
453 + $this->resolveOwnedBookingOrFail($bookingId);
454 +
336 455 $activities = BookingActivity::where('booking_id', $bookingId)
456 + ->where('type', '!=', BookingActivity::TYPE_NOTE)
337 457 ->orderBy('id', 'DESC')
338 458 ->get();
339 459
340 460 return [
@@ -343,32 +463,53 @@
343 463 }
344 464
345 465 public function getBookingMetaInfo(Request $request, $bookingId)
346 466 {
347 - $booking = Booking::findOrFail($bookingId);
467 + $booking = $this->resolveOwnedBookingOrFail($bookingId);
348 468
349 469 $activities = BookingActivity::where('booking_id', $booking->id)
470 + ->where('type', '!=', BookingActivity::TYPE_NOTE)
350 471 ->orderBy('id', 'DESC')
351 472 ->get();
352 473
474 + $activities->each(function ($activity) {
475 + $activity->description = wp_unslash($activity->description);
476 + });
477 +
353 478 $sidebarContents = [];
354 479 $mainBodyContents = [];
355 480
356 - if (defined('FLUENTCRM')) {
357 - $profileHtml = fluentcrm_get_crm_profile_html($booking->email, false);
358 - if ($profileHtml) {
359 - $sidebarContents[] = [
360 - 'id' => 'fluent_crm_profule',
361 - 'title' => __('CRM Profile', 'fluent-booking'),
362 - 'content' => $profileHtml
363 - ];
364 - }
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 + ];
365 490 }
366 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 +
367 504 $order = null;
368 - if ($booking->payment_method && $booking->payment_order) {
505 + if ($booking->payment_status && $booking->payment_order) {
369 506 $order = $booking->payment_order;
370 - $order->load(['items', 'transaction']);
507 + $relations = ['items', 'transaction'];
508 + if (method_exists($order, 'discounts')) {
509 + $relations[] = 'discounts';
510 + }
511 + $order->load($relations);
371 512 $order->currency_sign = CurrenciesHelper::getCurrencySign($order->currency);
372 513 }
373 514
374 515 $mainBodyContents = apply_filters('fluent_booking/booking_meta_info_main_meta', $mainBodyContents, $booking);
@@ -381,8 +522,198 @@
381 522 'main_body_contents' => $mainBodyContents
382 523 ];
383 524 }
384 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 +
385 716 private function formatBooking(&$booking)
386 717 {
387 718 $autoCompleteTimeOut = (int) Helper::getGlobalAdminSetting('auto_complete_timing', 60) * 60; // 10 minutes
388 719
@@ -389,9 +720,10 @@
389 720 if (in_array($booking->status, ['scheduled', 'pending']) && (time() - strtotime($booking->end_time)) > $autoCompleteTimeOut) {
390 721 $bookingStatus = $booking->status == 'pending' ? 'cancelled' : 'completed';
391 722 $booking->status = $bookingStatus;
392 723 $booking->save();
393 - 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);
394 726 }
395 727
396 728 if ($booking->isMultiHostBooking()) {
397 729 $booking->host_profiles = $booking->getHostProfiles();
@@ -405,8 +737,9 @@
405 737 }
406 738
407 739 $booking->title = $booking->getBookingTitle(true);
408 740 $booking->author = $booking->getHostDetails(false);
741 + $booking->details = $booking->getConfirmationData(true);
409 742 $booking->location = $booking->getLocationDetailsHtml();
410 743 $booking->reschedule_url = $booking->getRescheduleUrl();
411 744 $booking->happening_status = $booking->getOngoingStatus();
412 745 $booking->booking_status_text = $booking->getBookingStatus();
@@ -417,32 +750,48 @@
417 750
418 751 return $booking;
419 752 }
420 753
421 - private function getPaymentLog($bookingId)
754 + private function getConfirmedBy()
422 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 +
765 + private function getPaymentPaidLog($bookingId)
766 + {
423 767 return [
424 768 'booking_id' => $bookingId,
425 769 'status' => 'closed',
426 770 'type' => 'success',
427 771 'title' => __('Payment Successfully Completed', 'fluent-booking'),
428 - 'description' => __('Payment marked as paid by admin', 'fluent-booking')
772 + 'description' => __('Payment marked as paid by ', 'fluent-booking') . $this->getConfirmedBy()
429 773 ];
430 774 }
431 775
776 + private function getPaymentPendingLog($bookingId)
777 + {
778 + return [
779 + 'booking_id' => $bookingId,
780 + 'status' => 'closed',
781 + 'type' => 'success',
782 + 'title' => __('Payment Successfully Marked as Pending', 'fluent-booking'),
783 + 'description' => __('Payment marked as pending by ', 'fluent-booking') . $this->getConfirmedBy()
784 + ];
785 + }
786 +
432 787 private function getConfirmLog($bookingId)
433 788 {
434 - $confirmedBy = 'host';
435 - $userId = get_current_user_id();
436 - if ($userId && $user = get_user_by('ID', $userId)) {
437 - $confirmedBy = $user->display_name;
438 - }
439 -
440 789 return [
441 790 'booking_id' => $bookingId,
442 791 'status' => 'closed',
443 792 'type' => 'success',
444 793 'title' => __('Booking Confirmed', 'fluent-booking'),
445 - 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $confirmedBy
794 + 'description' => __('Booking has been confirmed by ', 'fluent-booking') . $this->getConfirmedBy()
446 795 ];
447 796 }
448 797 }