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
fluent-booking / app / Services / Integrations / FluentForms / FluentFormInit.php

FluentFormInit.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.5.0, at app/Services/Integrations/FluentForms/FluentFormInit.php

827 lines 30.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBooking\App\Services\Integrations\FluentForms;
4
5 use FluentBooking\App\Services\BookingFieldService;
6 use FluentBooking\App\Services\LocationService;
7 use FluentBooking\Framework\Support\Arr;
8 use FluentBooking\App\Models\Booking;
9 use FluentBooking\App\Models\CalendarSlot;
10 use FluentBooking\App\Services\Helper;
11 use FluentBooking\App\Services\DateTimeHelper;
12 use FluentBooking\App\Services\BookingService;
13 use FluentBooking\App\Hooks\Handlers\TimeSlotServiceHandler;
14 use FluentBooking\App\Hooks\Handlers\FrontEndHandler;
15 use FluentForm\App\Models\Submission;
16 use FluentForm\App\Helpers\Helper as FluentFormHelper;
17 use FluentForm\App\Modules\Form\FormFieldsParser;
18 use FluentForm\App\Services\FormBuilder\ShortCodeParser;
19 use FluentBooking\App\Vite;
20
21
22 class FluentFormInit
23 {
24 // Fluent Forms keys its Offline Payment method 'test'.
25 const FF_OFFLINE_METHOD = 'test';
26
27 protected $hostId;
28
29 public function init()
30 {
31 $this->registerHooks();
32 $this->registerIntegrations();
33 }
34
35 public function registerHooks()
36 {
37 add_action('fluentform/validate_input_item_fcal_booking', [$this, 'handleValidations'], 10, 3);
38 add_action('fluentform/notify_on_form_submit', [$this, 'handleFormSubmitted'], 10, 3);
39 add_action('fluentform/after_payment_status_change', [$this, 'handlePaymentStatusChanged'], 10, 2);
40 add_action('fluentform/conversational_question', [$this, 'loadConversationalAsset'], 10, 3);
41
42 add_filter('fluentform/conversational_field_types', function ($fieldTypes) {
43 $fieldTypes['fcal_booking'] = 'FlowFormCustomType';
44 return $fieldTypes;
45 });
46
47 add_filter('fluentform/conversational_accepted_field_elements', function ($elements) {
48 $elements[] = 'fcal_booking';
49 return $elements;
50 });
51
52 add_action('fluent_booking/booking_meta_info_main_meta_fluentform', [$this, 'pushFormDataToBooking'], 10, 2);
53 }
54
55 public function registerIntegrations()
56 {
57 add_action('init', function () {
58 new BookingElement();
59 });
60 }
61
62 public function handleValidations($error, $field, $formData)
63 {
64 if ($error) {
65 return $error;
66 }
67
68 $name = Arr::get($field, 'name');
69
70 if (!isset($formData[$name])) {
71 return $error;
72 }
73
74 $isRequired = Arr::get($field, 'rules.required.value');
75
76 $bookingData = Arr::get($formData, $name);
77
78 if ($bookingData) {
79 $bookingData = json_decode($bookingData, true);
80 } else {
81 $bookingData = [];
82 }
83
84 if ($isRequired) {
85 if (empty($bookingData['start_time']) || empty($bookingData['timezone'])) {
86 $error = Arr::get($field, 'rules.required.message');
87 if (!$error) {
88 // translators: %s is the label of the required field
89 $error = sprintf(__('%s field is required', 'fluent-booking'), Arr::get($field, 'raw.settings.label'));
90 }
91 return $error;
92 }
93 } else if (empty($bookingData['start_time'])) {
94 return $error;
95 }
96
97 $eventId = Arr::get($field, 'raw.settings.event_id');
98 $calendarEvent = CalendarSlot::find($eventId);
99
100 if (!$calendarEvent || $calendarEvent->status != 'active') {
101 return __('Sorry, the host is not accepting any new bookings at the moment.', 'fluent-booking');
102 }
103
104 $duration = $calendarEvent->getDuration(Arr::get($bookingData, 'duration', null));
105
106 $startTime = Arr::get($bookingData, 'start_time');
107 $timeZone = $bookingData['timezone'];
108
109 $startDateTime = DateTimeHelper::convertToUtc($startTime, $timeZone);
110 $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startDateTime) + ($duration * 60));
111
112 $timeSlotService = TimeSlotServiceHandler::initService($calendarEvent->calendar, $calendarEvent);
113
114 if (is_wp_error($timeSlotService)) {
115 return TimeSlotServiceHandler::sendError($timeSlotService, $calendarEvent, $timeZone);
116 }
117
118 $isSlotLocked = Helper::lockRoundRobinSlot($calendarEvent, $startDateTime, $endDateTime);
119
120 $availableSpot = $isSlotLocked ? $timeSlotService->isSpotAvailable($startDateTime, $endDateTime, $duration) : false;
121
122 if (!$availableSpot) {
123 $message = __('This selected time slot is not available. Maybe someone booked the spot just a few seconds ago.', 'fluent-booking');
124 wp_send_json(['errors' => [$message]], 422);
125 }
126
127 if ($calendarEvent->isRoundRobin()) {
128 $this->hostId = $timeSlotService->hostUserId;
129 }
130
131 if (!is_user_logged_in()) {
132 $fieldError = '';
133 // Now check if the email field is given or not
134 $emailFieldKey = Arr::get($field, 'raw.settings.cal_guest_fields.email_field');
135 if (!$emailFieldKey) {
136 $fieldError = __('Email is required for this appointment. Looks like this field does not have email field selected.', 'fluent-booking');
137 } else {
138 $email = Arr::get($formData, $emailFieldKey);
139 if (!$email || !is_email($email)) {
140 $fieldError = __('Email is required for this appointment. Please provide a valid email', 'fluent-booking');
141 }
142 }
143
144 if ($fieldError) {
145 return $fieldError;
146 }
147 }
148
149 $locationFieldKey = $this->getLocationFieldKey($calendarEvent);
150
151 if ($locationFieldKey) {
152 $requiredKeys = [];
153 if ($locationFieldKey == 'location') {
154 $locationFieldKey = 'location_config';
155 }
156
157 $userInputData = Arr::get($bookingData, 'form.' . $locationFieldKey);
158
159 if (in_array($locationFieldKey, ['phone_number', 'address'])) {
160 $requiredKeys[] = $locationFieldKey;
161 } else if ($locationFieldKey == 'location_config') {
162 $requiredKeys[] = 'location_config.driver';
163 $selectedLocation = LocationService::getLocationDetails($calendarEvent, $userInputData, $bookingData['form']);
164 $selectedLocationDriver = Arr::get($selectedLocation, 'type');
165 if (in_array($selectedLocationDriver, ['in_person_guest', 'phone_guest'])) {
166 $requiredKeys[] = 'location_config.user_location_input';
167 }
168 }
169
170 foreach ($requiredKeys as $requiredKey) {
171 if (!Arr::get($bookingData['form'], $requiredKey)) {
172 return __('Please provide a valid location for this meeting', 'fluent-booking');
173 }
174 }
175 }
176
177 /*
178 * We are decoding the data with valid array
179 */
180 add_filter('fluentform/insert_response_data', function ($data) use ($name, $calendarEvent) {
181
182 if (isset($data[$name]) && is_string($data[$name])) {
183 $bookingArr = json_decode($data[$name], true);
184 $validData = array_filter(Arr::only($bookingArr, ['start_time', 'timezone', 'duration', 'form.location_config', 'form.phone_number', 'form.address']));
185 $extendedData = array_filter(Arr::only(Arr::get($bookingArr, 'form', []), ['location_config', 'phone_number', 'address']));
186
187 if ($extendedData) {
188 $validData = array_merge($validData, $extendedData);
189 }
190
191 if ($validData) {
192 $validData['duration'] = $calendarEvent->getDuration(Arr::get($validData, 'duration', null));
193 $validData['end_time'] = gmdate('Y-m-d H:i:s', strtotime($bookingArr['start_time']) + ($validData['duration'] * 60));
194 }
195
196 $data[$name] = (array)$validData;
197 }
198
199 return $data;
200 });
201
202
203 return '';
204 }
205
206 /**
207 * Read the Fluent Forms values mapped onto the event's custom booking fields.
208 * The form's own field rules decide what is required; values are only sanitized here.
209 *
210 * @param array $field parsed fcal_booking field
211 * @param array $formData submitted form values keyed by field name
212 * @param CalendarSlot $event
213 *
214 * @return array
215 */
216 private function getMappedFieldsData($field, $formData, CalendarSlot $event)
217 {
218 $fieldMap = array_filter((array) Arr::get($field, 'raw.settings.cal_guest_fields.field_map', []));
219
220 if (!$fieldMap) {
221 return [];
222 }
223
224 // Form values arrive unslashed; getCustomFieldsData() unslashes, so slash to keep backslashes.
225 $postedData = [];
226 foreach ($fieldMap as $bookingFieldKey => $formFieldName) {
227 $postedData[$bookingFieldKey] = wp_slash(Arr::get($formData, $formFieldName));
228 }
229
230 return BookingFieldService::getCustomFieldsData($postedData, $event, array_keys($fieldMap));
231 }
232
233 public function handleFormSubmitted($entryId, $formDataX, $form)
234 {
235 $fields = FormFieldsParser::getInputs($form, ['rules', 'raw', 'name']);
236
237 $bookingFields = array_filter($fields, function ($field) {
238 return $field['element'] == 'fcal_booking';
239 });
240
241 if (!$bookingFields) {
242 return;
243 }
244
245 if (FluentFormHelper::getSubmissionMeta($entryId, 'fluent_booking_id')) {
246 return; // Already processed
247 }
248
249 $entry = wpFluent()->table('fluentform_submissions')
250 ->where('id', $entryId)
251 ->first();
252
253 if (!$entry) {
254 return;
255 }
256
257 $formData = json_decode($entry->response, true);
258
259 foreach ($bookingFields as $bookingField) {
260 $fieldName = Arr::get($bookingField, 'raw.attributes.name');
261 $ffFieldData = Arr::get($formData, $fieldName);
262
263 if (!$ffFieldData) {
264 continue;
265 }
266
267 if (is_string($ffFieldData)) {
268 $ffFieldData = json_decode($ffFieldData, true);
269 }
270
271 if (empty($ffFieldData['timezone']) || empty($ffFieldData['start_time']) || empty($ffFieldData['duration'])) {
272 continue;
273 }
274
275 $eventId = Arr::get($bookingField, 'raw.settings.event_id');
276 $event = CalendarSlot::find($eventId);
277
278 if (!$event || $event->status != 'active') {
279 continue;
280 }
281
282 $submittedData = json_decode($entry->response, true);
283
284 $emailFieldKey = Arr::get($bookingField, 'raw.settings.cal_guest_fields.email_field');
285 $guestEmail = '';
286 $guestName = '';
287 if ($emailFieldKey) {
288 $guestEmail = Arr::get($submittedData, $emailFieldKey);
289 $nameFieldKey = Arr::get($bookingField, 'raw.settings.cal_guest_fields.name_field');
290
291 $guestName = Arr::get($submittedData, $nameFieldKey);
292 if (is_array($guestName)) {
293 $guestName = implode(' ', $guestName);
294 }
295 }
296
297 if (!$guestEmail) {
298 $guestEmail = Arr::get($submittedData, 'email');
299 $guestName = Arr::get($submittedData, 'names');
300 if (is_array($guestName)) {
301 $guestName = implode(' ', $guestName);
302 }
303 }
304
305 if (!$guestEmail && $entry->user_id) {
306 $user = get_user_by('id', $entry->user_id);
307 if ($user) {
308 $guestEmail = $user->user_email;
309 $guestName = trim($user->first_name . ' ' . $user->last_name);
310 if (!$guestName) {
311 $guestName = $user->display_name;
312 }
313 }
314 }
315
316 if (!$guestEmail || !is_email($guestEmail)) {
317 do_action('fluentform/log_data', [
318 'parent_source_id' => $form->id,
319 'source_type' => 'submission_item',
320 'source_id' => $entry->id,
321 'component' => 'FluentBooking',
322 'status' => 'error',
323 'title' => __('Appointment could not be created', 'fluent-booking'),
324 'description' => __('Appointment could not be created because email is not given or invalid', 'fluent-booking'),
325 ]);
326 continue;
327 }
328
329 $startTime = $ffFieldData['start_time'];
330 $timeZone = $ffFieldData['timezone'];
331 $duration = $ffFieldData['duration'];
332
333 $startDateTime = DateTimeHelper::convertToUtc($startTime, $timeZone);
334
335 $bookingData = [
336 'start_time' => $startDateTime,
337 'name' => $guestName,
338 'email' => $guestEmail,
339 'person_time_zone' => sanitize_text_field($ffFieldData['timezone']),
340 'source' => 'fluentform',
341 'source_id' => $entry->id,
342 'status' => 'scheduled',
343 'source_url' => $entry->source_url,
344 'ip_address' => $entry->ip,
345 'event_type' => $event->event_type,
346 'slot_minutes' => $duration
347 ];
348
349 if ($event->isConfirmationRequired($startDateTime)) {
350 $bookingData['status'] = 'pending';
351 }
352
353 $bookingData = $this->maybeAddPaymentData($bookingData, $entry);
354
355 if ($entry->user_id) {
356 $bookingData['person_user_id'] = $entry->user_id;
357 }
358
359 if ($this->hostId) {
360 $bookingData['host_user_id'] = $this->hostId;
361 }
362
363 $selectedLocation = LocationService::getLocationDetails($event, Arr::get($ffFieldData, 'location_config', []), $ffFieldData);
364 if ($selectedLocation['type'] == 'phone_guest') {
365 $bookingData['phone'] = sanitize_textarea_field($selectedLocation['description']);
366 } else if (!empty($ffFieldData['address'])) {
367 $bookingData['address'] = sanitize_textarea_field($ffFieldData['address']);
368 }
369 $bookingData['location_details'] = $selectedLocation;
370
371 try {
372 $customFieldsData = $this->getMappedFieldsData($bookingField, $submittedData, $event);
373
374 $booking = BookingService::createBooking($bookingData, $event, $customFieldsData);
375
376 // Must persist, or the guard above misses a payment retry on the same submission.
377 FluentFormHelper::setSubmissionMeta($entry->id, 'fluent_booking_id', $booking->id, $form->id);
378
379 $fieldData = $submittedData[$fieldName];
380 $fieldData['booking_id'] = $booking->id;
381
382 $submittedData[$fieldName] = (array)$fieldData;
383
384 wpFluent()->table('fluentform_submissions')
385 ->where('id', $entryId)
386 ->update([
387 'response' => wp_json_encode($submittedData, JSON_UNESCAPED_UNICODE)
388 ]);
389
390 do_action('fluentform/log_data', [
391 'parent_source_id' => $form->id,
392 'source_type' => 'submission_item',
393 'source_id' => $entry->id,
394 'component' => 'FluentBooking',
395 'status' => 'info',
396 'title' => __('Booking has been created on FluentBooking', 'fluent-booking'),
397 /* translators: %1$s is the opening anchor tag, %2$s is the closing anchor tag. */
398 'description' => sprintf(__('A new appointment has been created on FluentBooking. %1$sView Booking Details%2$s', 'fluent-booking'), '<a rel="noopener" href="' . $booking->getAdminViewUrl() . '" target="_blank">', '</a>'),
399 ]);
400
401 } catch (\Exception $exception) {
402 do_action('fluentform/log_data', [
403 'parent_source_id' => $form->id,
404 'source_type' => 'submission_item',
405 'source_id' => $entry->id,
406 'component' => 'FluentBooking',
407 'status' => 'error',
408 'title' => __('Failed to create booking', 'fluent-booking'),
409 'description' => $exception->getMessage(),
410 ]);
411 }
412 }
413 }
414
415 /**
416 * Fluent Forms fires notify_on_form_submit before the gateway runs, so the row is
417 * written to hold the slot but waits for the payment, as the cart integration does.
418 * Offline is honoured up front, like the native offline method.
419 *
420 * @param array $bookingData
421 * @param object $entry fluentform_submissions row
422 *
423 * @return array
424 */
425 private function maybeAddPaymentData($bookingData, $entry)
426 {
427 $submissionStatus = isset($entry->payment_status) ? $entry->payment_status : '';
428
429 if (!$submissionStatus) {
430 return $bookingData; // Not a payment submission
431 }
432
433 $paymentStatus = $this->mapPaymentStatus($submissionStatus);
434 $isOffline = isset($entry->payment_method) && $entry->payment_method === self::FF_OFFLINE_METHOD;
435
436 // 'offline' is the only method FiveMinuteScheduler::maybeAutoCancelBooking exempts.
437 $bookingData['payment_method'] = $isOffline ? 'offline' : 'fluentform';
438 $bookingData['payment_status'] = $paymentStatus;
439
440 if ($paymentStatus != 'paid' && !$isOffline) {
441 $bookingData['status'] = 'pending';
442 }
443
444 return $bookingData;
445 }
446
447 /**
448 * @param string $submissionStatus Fluent Forms payment status
449 *
450 * @return string FluentBooking payment status
451 */
452 private function mapPaymentStatus($submissionStatus)
453 {
454 $statusMap = [
455 'paid' => 'paid',
456 'refunded' => 'refunded',
457 'partially-refunded' => 'partially-refunded',
458 'failed' => 'failed',
459 'cancelled' => 'failed'
460 ];
461
462 // pending / processing / requires_review all mean "not settled yet"
463 return Arr::get($statusMap, $submissionStatus, 'pending');
464 }
465
466 /**
467 * @param string $newStatus Fluent Forms payment status
468 * @param object $submission Submission model or fluentform_submissions row
469 */
470 public function handlePaymentStatusChanged($newStatus, $submission)
471 {
472 $submissionId = $this->readSubmissionId($submission);
473
474 if (!$submissionId) {
475 return;
476 }
477
478 // Fires for every payment on the site; Fluent Forms' indexed meta is the cheap check.
479 if (!FluentFormHelper::getSubmissionMeta($submissionId, 'fluent_booking_id')) {
480 return;
481 }
482
483 // A form can carry several booking fields, so a submission can own several bookings.
484 $bookings = Booking::where('source', 'fluentform')
485 ->where('source_id', $submissionId)
486 ->get();
487
488 if ($bookings->isEmpty()) {
489 return;
490 }
491
492 $paymentStatus = $this->mapPaymentStatus($newStatus);
493
494 foreach ($bookings as $booking) {
495 if ($paymentStatus == 'paid') {
496 $this->confirmPaidBooking($booking);
497 continue;
498 }
499
500 if ($booking->payment_status == $paymentStatus) {
501 continue;
502 }
503
504 if ($this->undoesSettledOutcome($paymentStatus, $booking->payment_status)) {
505 continue;
506 }
507
508 // Failure and refund leave the booking status alone, as the native gateways do.
509 $this->settlePaymentStatus($booking, $paymentStatus);
510 }
511 }
512
513 /**
514 * One payment module passes a Submission model, whose columns live behind __get,
515 * the other a plain row. Property access reads both; an array cast reads only the row.
516 *
517 * @param object $submission
518 *
519 * @return int
520 */
521 private function readSubmissionId($submission)
522 {
523 if (!is_object($submission)) {
524 return 0;
525 }
526
527 return isset($submission->id) ? (int)$submission->id : 0;
528 }
529
530 /**
531 * Statuses that already record an outcome. An unsettled event landing on one of
532 * these is stale delivery, not a state change.
533 */
534 const SETTLED_PAYMENT_STATUSES = ['paid', 'refunded', 'partially-refunded'];
535
536 /**
537 * @param string $paymentStatus
538 *
539 * @return bool
540 */
541 private function isSettled($paymentStatus)
542 {
543 return in_array($paymentStatus, self::SETTLED_PAYMENT_STATUSES, true);
544 }
545
546 /**
547 * Redelivered events arrive out of order, and one carrying no outcome must not
548 * overwrite a recorded one - flattening a refund would hide it from the paid path.
549 *
550 * @param string $paymentStatus incoming
551 * @param string $currentPaymentStatus stored on the booking
552 *
553 * @return bool
554 */
555 private function undoesSettledOutcome($paymentStatus, $currentPaymentStatus)
556 {
557 return !$this->isSettled($paymentStatus) && $this->isSettled($currentPaymentStatus);
558 }
559
560 /**
561 * @param string $paymentStatus
562 *
563 * @return bool
564 */
565 private function isRefunded($paymentStatus)
566 {
567 return in_array($paymentStatus, ['refunded', 'partially-refunded'], true);
568 }
569
570 /**
571 * The precedence between two events landing together is settled in SQL, not against
572 * the row as it was read: a refund is the final outcome and lands whatever arrived
573 * first, anything else only fills in a booking with no outcome recorded yet.
574 *
575 * @param \FluentBooking\App\Models\Booking $booking
576 * @param string $paymentStatus
577 */
578 private function settlePaymentStatus($booking, $paymentStatus)
579 {
580 $query = Booking::where('id', $booking->id);
581
582 if (!$this->isRefunded($paymentStatus)) {
583 $query->whereNotIn('payment_status', self::SETTLED_PAYMENT_STATUSES);
584 }
585
586 $query->update([
587 'payment_status' => $paymentStatus,
588 'updated_at' => gmdate('Y-m-d H:i:s') // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
589 ]);
590 }
591
592 /**
593 * @param \FluentBooking\App\Models\Booking $booking
594 */
595 private function confirmPaidBooking($booking)
596 {
597 if ($booking->payment_status == 'paid') {
598 return;
599 }
600
601 $calendarEvent = $booking->calendar_event;
602
603 if (!$calendarEvent) {
604 return;
605 }
606
607 // Webhooks arrive late and out of order, and a refund is the last word on an
608 // order - a stale 'paid' must not settle it again, let alone put it back on
609 // the calendar. Logged rather than dropped, so the mismatch is visible.
610 if ($this->isRefunded($booking->payment_status)) {
611 do_action('fluent_booking/log_booking_activity', [
612 'booking_id' => $booking->id,
613 'status' => 'closed',
614 'type' => 'error',
615 'title' => __('Fluent Forms: Payment status could not be changed', 'fluent-booking'),
616 /* translators: %s is the current payment status of the booking */
617 'description' => sprintf(__('A paid notification arrived after the order was %s, so the booking was left unchanged.', 'fluent-booking'), $booking->getPaymentStatus())
618 ]);
619 return;
620 }
621
622 if ($booking->status != 'pending') {
623 // Honoured up front, or already moved on - only settle the payment.
624 $this->settlePaymentStatus($booking, 'paid');
625 return;
626 }
627
628 $isRequireConfirmation = $calendarEvent->isConfirmationRequired($booking->start_time, $booking->created_at);
629
630 // Awaiting approval stays pending; paying only releases the held-back notifications.
631 $newStatus = $isRequireConfirmation ? 'pending' : 'scheduled';
632
633 // Gateways redeliver, so only the request that settles the row dispatches the hooks.
634 $flagged = Booking::where('id', $booking->id)
635 ->where('status', 'pending')
636 ->whereNotIn('payment_status', self::SETTLED_PAYMENT_STATUSES)
637 ->update([
638 'status' => $newStatus,
639 'payment_status' => 'paid',
640 'updated_at' => gmdate('Y-m-d H:i:s') // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
641 ]);
642
643 if (!$flagged) {
644 return;
645 }
646
647 $booking = Booking::with(['calendar_event', 'calendar'])->find($booking->id);
648
649 do_action('fluent_booking/log_booking_activity', [
650 'booking_id' => $booking->id,
651 'status' => 'closed',
652 'type' => 'success',
653 'title' => __('Payment completed on Fluent Forms', 'fluent-booking'),
654 /* translators: %s is the booking status after the payment has been completed */
655 'description' => sprintf(__('The form payment has been paid and the appointment is now in %s status.', 'fluent-booking'), $booking->getBookingStatus())
656 ]);
657
658 $bookingData = [
659 'name' => $booking->first_name . ' ' . $booking->last_name,
660 'email' => $booking->email,
661 'phone' => $booking->phone
662 ];
663
664 // this pre hook is for early actions that require for remote calendars and locations
665 do_action('fluent_booking/pre_after_booking_' . $newStatus, $booking, $calendarEvent, $bookingData);
666
667 $booking = Booking::with(['calendar_event', 'calendar'])->find($booking->id);
668
669 do_action('fluent_booking/after_booking_' . $newStatus, $booking, $calendarEvent, $bookingData);
670 }
671
672 public function loadConversationalAsset($question, $field, $form)
673 {
674 if ('fcal_booking' === $field['element']) {
675
676 $calendarEventId = Arr::get($field, 'settings.event_id');
677 $calendarEvent = CalendarSlot::find($calendarEventId);
678
679 if (!$calendarEvent || !$calendarEvent->calendar) {
680 return;
681 }
682
683 [$localizeData, $elementId] = $this->getLocalizedData($calendarEvent, $field, $form);
684
685 Vite::enqueueScript('fluent_booking', 'ff_conversational', [], FLUENT_BOOKING_ASSETS_VERSION);
686
687 if (BookingFieldService::hasPhoneNumberField($localizeData['form_fields'])) {
688 Vite::enqueueScript('fluent-booking-phone-field', 'phone_field', [], FLUENT_BOOKING_ASSETS_VERSION);
689 $inlineStyle = '.fcal_phone_wrapper .flag { background: url(' . esc_url(FLUENT_BOOKING_URL . 'assets/images/flags_responsive.png') . ') no-repeat;background-size: 100%;}';
690 wp_add_inline_style('fluent-booking-phone-field', $inlineStyle);
691 }
692
693 wp_localize_script('fluent_booking', 'fcal_public_vars_' . $question['id'], $localizeData);
694 wp_localize_script('fluent_booking', 'fluentCalendarPublicVars', (new FrontEndHandler())->getGlobalVars());
695 }
696 }
697
698 public function pushFormDataToBooking($meta, $booking)
699 {
700 if (!$booking->source_id) {
701 return $meta;
702 }
703
704 try {
705 $submission = Submission::find($booking->source_id);
706
707 if (!$submission) {
708 return $meta;
709 }
710
711 $response = json_decode($submission->response);
712
713 $smartCode = '{all_data}';
714
715 if ($submission->payment_total) {
716 $smartCode .= '<h3>' . __('Related Payments', 'fluent-booking') . '</h3>{payment.receipt}';
717 }
718
719 $entryHtmlData = ShortCodeParser::parse(
720 $smartCode,
721 $submission->id,
722 $response,
723 $submission->form,
724 false,
725 true
726 );
727
728 $entryHtmlData .= '<p><a target="_blank" rel="noopener" href="' . admin_url('admin.php?page=fluent_forms&route=entries&form_id=' . $submission->form_id . '#/entries/' . $submission->id) . '">' . __('View Form Submission', 'fluent-booking') . '</a></p>';
729
730 $meta[] = [
731 'id' => 'fluentform',
732 'title' => __('Related Form Data', 'fluent-booking'),
733 'content' => $entryHtmlData
734 ];
735 } catch (\Exception $e) {
736
737 }
738
739 return $meta;
740 }
741
742 public function getLocalizedData($calendarEvent, $data, $form)
743 {
744 $element_id = $this->makeElementId($data, $form);
745
746 $calendar = $calendarEvent->calendar;
747
748 $settings = Arr::get($data, 'settings');
749
750 $name = Arr::get($data, 'attributes.name');
751
752 $localizeData = (new FrontEndHandler())->getCalendarEventVars($calendar, $calendarEvent);
753
754 $localizeData['name'] = $name;
755 $localizeData['settings'] = $settings;
756
757 $isHostEnabled = Arr::get($localizeData['settings']['cal_guest_fields'], 'host_info', 'hide') == 'show';
758
759 $showHostInfo = $isHostEnabled || Arr::isTrue($calendarEvent->settings, 'multi_duration.enabled');
760
761 if ($showHostInfo) {
762 $localizeData['disable_author'] = false;
763 } else {
764 $localizeData['disable_author'] = true;
765 }
766
767 if(!empty($form->instance_css_class)) {
768 $localizeData['form_instance'] = $form->instance_css_class;
769 }
770
771 $locationFieldKey = $this->getLocationFieldKey($calendarEvent);
772 if ($locationFieldKey) {
773 $formFields = $localizeData['form_fields'];
774 $formFields = array_filter($formFields, function ($field) use ($locationFieldKey) {
775 return $field['name'] == $locationFieldKey;
776 });
777
778 $localizeData['form_fields'] = array_values($formFields);
779 } else {
780 $localizeData['form_fields'] = [];
781 }
782
783 return [$localizeData, $element_id];
784 }
785
786 private function getLocationFieldKey($calendarEvent)
787 {
788 $locationFieldKey = '';
789 if ($calendarEvent->isPhoneRequired()) {
790 $locationFieldKey = 'phone_number';
791 } else if ($calendarEvent->isAddressRequired()) {
792 $locationFieldKey = 'address';
793 } else if ($calendarEvent->isLocationFieldRequired()) {
794 $locationFieldKey = 'location';
795 }
796 return $locationFieldKey;
797 }
798
799 /**
800 * Build unique ID concatenating form id and name attribute
801 *
802 * @param array $data $form
803 *
804 * @return string for id value
805 */
806 protected function makeElementId($data, $form)
807 {
808 if (isset($data['attributes']['name'])) {
809 $formInstance = FluentFormHelper::$formInstance;
810 if (!empty($data['attributes']['id'])) {
811 return $data['attributes']['id'];
812 }
813 $elementName = $data['attributes']['name'];
814 $elementName = str_replace(['[', ']', ' '], '_', $elementName);
815
816 $suffix = esc_attr($form->id);
817 if ($formInstance > 1) {
818 $suffix = $suffix . '_' . $formInstance;
819 }
820
821 $suffix .= '_' . $elementName;
822
823 return 'ff_' . esc_attr($suffix);
824 }
825 }
826 }
827