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 / FluentCart / Bootstrap.php

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

522 lines 19.6 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\FluentCart;
4
5 use FluentBooking\Framework\Foundation\Application;
6 use FluentCart\Api\StoreSettings;
7 use FluentCart\App\Helpers\Status;
8 use FluentCart\App\Services\Renderer\Receipt\ThankYouRender;
9 use FluentBooking\App\Models\Booking;
10 use FluentBooking\App\Models\CalendarSlot;
11 use FluentBooking\App\Services\DateTimeHelper;
12 use FluentBooking\App\Services\Helper;
13 use FluentBooking\Framework\Support\Arr;
14
15 class Bootstrap
16 {
17 public function __construct(Application $app)
18 {
19 $app->router->group(function ($router) {
20 require_once __DIR__ . '/Http/cart_api.php';
21 });
22
23 $this->registerHooks();
24 }
25
26 public function registerHooks()
27 {
28 add_filter('fluent_booking/public_event_vars', [$this, 'maybePushPaymentVars'], 11, 2);
29 add_filter('fluent_booking/booking_data', [$this, 'maybePushPaymentData'], 10, 2);
30
31 add_action('fluent_cart/receipt/thank_you/after_order_items', [$this, 'maybeShowBookingDetailsInReceipt'], 10, 1);
32 add_action('fluent_booking/booking_meta_info_main_meta_cart', [$this, 'pushOrderDataToBookingView'], 10, 2);
33
34 // new API
35 add_action('fluent_cart/cart/line_item/line_meta', [$this, 'maybeRenderBookingInfoOnCartItem'], 10, 1);
36 // We are adding booking id to the order config after draft is created
37 add_action('fluent_booking/cart/booking_order_created', function ($eventData) {
38 $cart = Arr::get($eventData, 'cart');
39 if (empty($cart->checkout_data['fluent_booking_data'])) {
40 return;
41 }
42
43 $order = Arr::get($eventData, 'order');
44 $config = $order->config;
45 $bookingId = Arr::get($cart->checkout_data, 'fluent_booking_data.booking_id');
46 if ($bookingId) {
47 $config['fcal_booking_id'] = $bookingId;
48 $config['fcal_target_variant_id'] = Arr::get($cart->checkout_data, 'fluent_booking_data.target_variant_id');
49 $order->config = $config;
50 $order->save();
51 }
52 });
53
54 // The booking cart is locked to pin its booking item, which also hides order bumps
55 add_filter('fluent_cart/cart/accepts_additional_items', [$this, 'maybeAcceptOrderBumps'], 10, 2);
56
57 // after order confirmation
58 add_action('fluent_booking/cart/booking_order_completed', [$this, 'maybeScheduleBooking'], 10, 1);
59
60 // Offline and async payments settle after their cart is closed, so the
61 // action above never reaches us. This one fires on the payment itself.
62 add_action('fluent_cart/order_paid_done', [$this, 'reconcileBookingFromOrder'], 10, 1);
63
64 add_filter('fluent_cart/checkout_page_name_fields_schema', [$this, 'maybeFillSplitNameFields'], 10, 2);
65 }
66
67 public function maybeAcceptOrderBumps($accepts, $context)
68 {
69 $cart = Arr::get($context, 'cart');
70
71 if ($accepts || !$cart || empty($cart->checkout_data['fluent_booking_data'])) {
72 return $accepts;
73 }
74
75 // A draft order binds the cart, and upgrade carts keep their own restriction
76 if ($cart->order_id || !empty($cart->checkout_data['upgrade_data'])) {
77 return $accepts;
78 }
79
80 return true;
81 }
82
83 public function maybeFillSplitNameFields($nameFields, $context)
84 {
85 $cart = Arr::get($context, 'cart');
86 if (!$cart || empty($cart->checkout_data['fluent_booking_data'])) {
87 return $nameFields;
88 }
89
90 if (isset($nameFields['billing_first_name']) && empty($nameFields['billing_first_name']['value'])) {
91 $nameFields['billing_first_name']['value'] = $cart->first_name;
92 }
93
94 if (isset($nameFields['billing_last_name']) && empty($nameFields['billing_last_name']['value'])) {
95 $nameFields['billing_last_name']['value'] = $cart->last_name;
96 }
97
98 return $nameFields;
99 }
100
101
102 public function maybePushPaymentVars($eventVars, CalendarSlot $calendarEvent)
103 {
104 if (!CartHelper::isEnabled($calendarEvent)) {
105 return $eventVars;
106 }
107
108 $paymentSettings = $calendarEvent->getPaymentSettings();
109 $defaultDuration = $calendarEvent->getDefaultDuration();
110
111 $eventVars['slot']['total_payment'] = CartHelper::getCartProductPrice($paymentSettings, $defaultDuration);
112
113 $isMultiEnabled = Arr::get($paymentSettings, 'multi_payment_enabled') === 'yes';
114 if ($calendarEvent->isMultiDurationEnabled() && $isMultiEnabled) {
115 $productIds = Arr::get($paymentSettings, 'multi_payment_cart_ids', []);
116 $eventVars['multi_payment_cart_ids'] = CartHelper::getCartProductPriceByDuration($productIds);
117 }
118
119 return $eventVars;
120 }
121
122 public function maybePushPaymentData($bookingData, $calendarEvent)
123 {
124 if (Arr::get($bookingData, 'source') != 'web') {
125 return $bookingData;
126 }
127
128 if (!CartHelper::isEnabled($calendarEvent)) {
129 return $bookingData;
130 }
131
132 $duration = Arr::get($bookingData, 'slot_minutes');
133
134 $variationId = CartHelper::getEventProductId($calendarEvent, $duration);
135 if (!$variationId) {
136 return $bookingData;
137 }
138
139 $product = CartHelper::getProduct($variationId);
140 if (!$product) {
141 return $bookingData;
142 }
143
144 $bookingData['source'] = 'cart';
145 $bookingData['payment_method'] = 'fluent_cart';
146 $bookingData['payment_status'] = 'pending';
147 $bookingData['status'] = 'pending';
148
149 add_filter('fluent_booking/booking_confirmation_response', function ($response, $booking) use ($product) {
150 if ($booking->status != 'pending' || $booking->source != 'cart') {
151 return $response;
152 }
153
154 $quantity = $booking->getMeta('quantity', 1);
155 $instantCart = \FluentCart\App\Helpers\CartHelper::generateCartFromVariation($product, $quantity);
156
157 $cartData = $instantCart->cart_data;
158 $cartData[0]['fcal_booking_id'] = $booking->id;
159 $instantCart->cart_data = $cartData;
160
161 $instantCart->cart_group = 'instant';
162 $instantCart->first_name = $booking->first_name;
163 $instantCart->last_name = $booking->last_name;
164 $instantCart->email = $booking->email;
165 $instantCart->user_id = $booking->person_user_id;
166 $instantCart->cart_hash = md5('booking_cart_' . wp_generate_uuid4() . time());
167 $instantCart->checkout_data = [
168 'is_locked' => 'yes',
169 'fluent_booking_data' => [
170 'booking_id' => $booking->id,
171 'target_variant_id' => $product->id
172 ],
173 '__on_success_actions__' => [
174 'fluent_booking/cart/booking_order_completed'
175 ],
176 '__after_draft_created_actions__' => [
177 'fluent_booking/cart/booking_order_created'
178 ],
179 '__cart_notices' => [
180
181 ]
182 ];
183
184 $instantCart->save();
185
186 $cartHash = $instantCart->cart_hash;
187 $checkoutUrl = add_query_arg(
188 [
189 'fct_cart_hash' => $cartHash
190 ],
191 (new StoreSettings())->getCheckoutPage()
192 );
193
194 $response['data']['redirect_to'] = $checkoutUrl;
195 $response['data']['redirect_message'] = __('You are redirecting to checkout page to complete the appointment.', 'fluent-booking');
196
197 do_action('fluent_booking/log_booking_activity', [
198 'booking_id' => $booking->id,
199 'status' => 'closed',
200 'type' => 'info',
201 'title' => __('Redirect to FluentCart checkout page', 'fluent-booking'),
202 'description' => __('User redirected to FluentCart checkout page to complete the order.', 'fluent-booking')
203 ]);
204
205 return $response;
206 }, 10, 2);
207
208 return $bookingData;
209 }
210
211 public function maybeRenderBookingInfoOnCartItem($eventInfo)
212 {
213 $item = Arr::get($eventInfo, 'item', []);
214
215 if (empty($item['fcal_booking_id'])) {
216 return;
217 }
218
219 $bookingId = $item['fcal_booking_id'];
220 $booking = Booking::find($bookingId);
221 if (!$booking || !$booking->calendar_event || $booking->status !== 'pending') {
222 return;
223 }
224
225 $bookingTime = $booking->getFullBookingDateTimeText($booking->person_time_zone, true) . ' (' . $booking->person_time_zone . ')';
226
227 if ($booking->calendar_event->allowMultiBooking()) {
228 $bookingTime = array_merge((array)$bookingTime, $booking->getOtherBookingTimes());
229 }
230 ?>
231 <div class="fct_item_title booking_meta">
232 <div class="fcal_meta_label"
233 style="font-weight: 600;"><?php echo esc_html__('Appointment:', 'fluent-booking'); ?></div>
234 <div class="fcal_meta_value"
235 style="font-weight: 400;"><?php echo esc_html(implode(', ', (array)$bookingTime)); ?></div>
236 </div>
237 <?php
238 }
239
240 public function maybeScheduleBooking($eventData)
241 {
242 $order = Arr::get($eventData, 'order');
243
244 if (empty($order)) {
245 return;
246 }
247
248 $cart = Arr::get($eventData, 'cart');
249
250 if (!$cart) {
251 return;
252 }
253
254 $targetProductId = Arr::get($cart->checkout_data, 'fluent_booking_data.target_variant_id', 0);
255
256 if (!$targetProductId) {
257 return;
258 }
259
260 $checkoutItem = array_filter($cart->cart_data, function ($item) use ($targetProductId) {
261 return Arr::get($item, 'object_id', 0) == $targetProductId;
262 });
263
264 if (!$checkoutItem) {
265 return;
266 }
267
268 $this->scheduleBookingForOrder($order);
269 }
270
271 /**
272 * The same work, reached without a cart.
273 *
274 * __on_success_actions__ only run while the cart is open, and offline and
275 * async orders close theirs before the payment settles - leaving the
276 * booking pending for good. The order keeps the booking id either way.
277 *
278 * @param array $eventData
279 */
280 public function reconcileBookingFromOrder($eventData)
281 {
282 $order = Arr::get($eventData, 'order');
283
284 if (empty($order)) {
285 return;
286 }
287
288 $this->scheduleBookingForOrder($order);
289 }
290
291 /**
292 * @param \FluentCart\App\Models\Order $order
293 */
294 protected function scheduleBookingForOrder($order)
295 {
296 $bookingId = Arr::get($order->config, 'fcal_booking_id', '');
297
298 if (empty($bookingId)) {
299 return;
300 }
301
302 $booking = Booking::find($bookingId);
303 if (!$booking || $booking->source_id) {
304 return;
305 }
306
307 $calendarEvent = $booking->calendar_event;
308 if (!CartHelper::isEnabled($calendarEvent)) {
309 return;
310 }
311
312 // The cart takes order bumps, so a paid order is not proof the booking item was bought.
313 // Orders drafted before the variant was recorded carry no id and skip the check.
314 $targetVariantId = (int) Arr::get($order->config, 'fcal_target_variant_id', 0);
315 if ($targetVariantId && !$order->order_items()->where('object_id', $targetVariantId)->exists()) {
316 do_action('fluent_booking/log_booking_activity', [
317 'booking_id' => $booking->id,
318 'status' => 'closed',
319 'type' => 'error',
320 'title' => __('Cart: Booking status could not be changed', 'fluent-booking'),
321 'description' => sprintf(
322 /* translators: %1$s and %2$s are the HTML link tags for "View Order" */
323 __('The paid order does not contain the booking item. %1$sView Order%2$s', 'fluent-booking'),
324 '<a target="_blank" href="' . $order->getViewUrl('admin') . '">',
325 '</a>')
326 ]);
327 return;
328 }
329
330 if ($booking->status != 'pending') {
331 do_action('fluent_booking/log_booking_activity', [
332 'booking_id' => $booking->id,
333 'status' => 'closed',
334 'type' => 'success',
335 'title' => __('Cart: Booking status could not be changed', 'fluent-booking'),
336 'description' => sprintf(
337 /* translators: Notification message when the booking status could not be changed due to its current status. %1$s is the status, %2$s and %3$s are the HTML link tags for "View Order" */
338 __('Booking status could not be changed as it is in %1$s status. %2$sView Order%3$s', 'fluent-booking'),
339 $booking->status,
340 '<a target="_blank" href="' . $order->getViewUrl('admin') . '">',
341 '</a>')
342 ]);
343 return;
344 }
345
346 $isRequireConfirmation = $calendarEvent->isConfirmationRequired($booking->start_time, $booking->created_at);
347
348 if (!$isRequireConfirmation) {
349 $booking->status = 'scheduled';
350 }
351
352 $booking->payment_status = 'paid';
353 $booking->source_id = $order->id;
354 $booking->save();
355
356 $this->maybeUpdateChildBookings($booking, $calendarEvent, $order);
357
358 do_action('fluent_booking/log_booking_activity', CartHelper::getSuccessLog($booking->id, $order));
359
360 $bookingData = [
361 'name' => $booking->first_name . ' ' . $booking->last_name,
362 'email' => $booking->email,
363 'phone' => $booking->phone
364 ];
365
366 $order->addLog(
367 __('Booking Confirmation', 'fluent-booking'),
368 sprintf(
369 /* translators: Order log message for the change of booking status to scheduled. %1$s is the booking ID, %2$s is the booking status, %3$s is the date/time+timezone, %4$s is a link open tag, %5$s is the link close tag */
370 __('Booking #%1$s status changed to %2$s at %3$s. %4$sView Booking%5$s', 'fluent-booking'),
371 $booking->id,
372 $booking->status,
373 $booking->getFullBookingDateTimeText($booking->calendar->author_timezone, true) . ' (' . $booking->calendar->author_timezone . ')',
374 '<a target="_blank" href="' . esc_url(Helper::getAppBaseUrl('scheduled-events?period=upcoming&booking_id=' . $booking->id)) . '">',
375 '</a>'
376 ),
377 'info',
378 'FluentBooking'
379 );
380
381 // this pre hook is for early actions that require for remote calendars and locations
382 do_action('fluent_booking/pre_after_booking_' . $booking->status, $booking, $booking->calendar_event, $bookingData);
383
384 do_action('fluent_booking/after_booking_' . $booking->status, $booking, $booking->calendar_event, $bookingData);
385 }
386
387 public function maybeShowBookingDetailsInReceipt($event)
388 {
389 $order = Arr::get($event, 'order');
390 $bookingId = Arr::get($order->config, 'fcal_booking_id', '');
391
392 if (empty($order) || empty($bookingId)) {
393 return;
394 }
395
396 $booking = Booking::find($bookingId);
397 if (!$booking) {
398 return;
399 }
400
401 $redirectUrl = $booking->getRedirectUrlWithQuery();
402
403 if ($redirectUrl && in_array($order->payment_status, Status::getOrderPaymentSuccessStatuses()) && Arr::get($event, 'is_first_time', false)) {
404 add_action('wp_footer', function () use ($redirectUrl) {
405 ?>
406 <script type="text/javascript">
407 document.addEventListener('DOMContentLoaded', function () {
408 window.location.href = "<?php echo esc_url($redirectUrl); ?>";
409 });
410 </script>
411 <?php
412 });
413 }
414 ?>
415 <style>
416 .fcal_receipt_booking_details h5 {
417 margin: 0 0 10px;
418 border-bottom: 1px solid #dee2e6;
419 padding-bottom: 5px;
420 font-size: 15px;
421 font-weight: 700;
422 color: #495057;
423 }
424
425 .fcal_receipt_booking_info p {
426 margin: 0 0 4px;
427 color: #111111;
428 font-size: 14px;
429 }
430 </style>
431 <div class="fcal_receipt_booking_details">
432 <h5><?php esc_html_e('Booking Details', 'fluent-booking'); ?></h5>
433 <div class="fcal_receipt_booking_info">
434 <p>
435 <b><?php esc_html_e('Meeting Info:', 'fluent-booking'); ?></b> <?php echo esc_html($booking->getBookingTitle()); ?>
436 </p>
437 <p>
438 <b><?php esc_html_e('Date & Time:', 'fluent-booking'); ?></b> <?php echo esc_html(implode(', ', $booking->getAllBookingShortTimes($booking->person_time_zone))); ?>
439 (<?php echo esc_html($booking->person_time_zone); ?>)
440 </p>
441 <p>
442 <b><?php esc_html_e('Status:', 'fluent-booking'); ?></b> <?php echo esc_html(ucfirst($booking->status)); ?>
443 </p>
444 <p>
445 <a href="<?php echo esc_url($booking->getConfirmationUrl()); ?>"><?php esc_html_e('View Full Meeting Details', 'fluent-booking'); ?></a>
446 </p>
447 </div>
448 </div>
449 <?php
450 }
451
452 public function pushOrderDataToBookingView($meta, $booking)
453 {
454 $orderId = $booking->source_id;
455 if (!$orderId) {
456 return $meta;
457 }
458
459 $order = CartHelper::getOrder($orderId);
460 if (!$order) {
461 return $meta;
462 }
463
464 // Get FluentCart Order Summary as html
465 ob_start();
466 printf(
467 /* translators: 1: order number 2: order date 3: order status */
468 esc_html__('Order %1$s was placed on %2$s and is currently %3$s.', 'fluent-booking'),
469 '<span class="order-number"><a href="' . esc_url($order->getViewUrl('admin')) . '">' . '#' . esc_html($order->id) . '</a></span>',
470 '<span class="order-date">' . esc_html(DateTimeHelper::formatToLocale($order->created_at, 'date_time')) . '</span>',
471 '<span class="order-status">' . esc_html($order->status) . '</span>'
472 );
473
474 (new ThankYouRender(['order' => $order]))->renderOrderItems();
475
476 $orderSummary = ob_get_clean();
477
478 $meta[] = [
479 'id' => 'cart-order-summary',
480 'title' => __('Order Summary', 'fluent-booking'),
481 'content' => $orderSummary
482 ];
483
484 return $meta;
485 }
486
487 private function maybeUpdateChildBookings($booking, $calendarEvent, $order)
488 {
489 $childBookingIds = Booking::where('parent_id', $booking->id)
490 ->where('status', 'pending')
491 ->pluck('id')
492 ->toArray();
493
494 if (!$childBookingIds) {
495 return;
496 }
497
498 foreach ($childBookingIds as $childBookingId) {
499 $childBooking = Booking::find($childBookingId);
500
501 if (!$childBooking) {
502 continue;
503 }
504
505 $childBooking->update([
506 'status' => $booking->status,
507 'payment_status' => $booking->payment_status,
508 ]);
509
510 if ($booking->status == 'scheduled') {
511 do_action('fluent_booking/pre_after_booking_scheduled', $childBooking, $calendarEvent, $childBooking);
512
513 $childBooking = Booking::with(['calendar_event', 'calendar'])->find($childBooking->id);
514
515 do_action('fluent_booking/after_booking_scheduled', $childBooking, $calendarEvent, $childBooking);
516 }
517
518 do_action('fluent_booking/log_booking_activity', CartHelper::getSuccessLog($childBookingId, $order));
519 }
520 }
521 }
522