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/Services/BookingService.php +71 -8 2.2.5 → 2.5.0 View file →
@@ -55,9 +55,11 @@
55 55 }
56 56
57 57 do_action('fluent_booking/before_booking', $bookingData, $calendarSlot);
58 58
59 - $booking = Booking::create($bookingData);
59 + $booking = Helper::dbTransaction(function () use ($bookingData) {
60 + return Booking::create($bookingData);
61 + });
60 62
61 63 self::attachHosts($booking, $calendarSlot);
62 64 self::updateParentInfo($booking, $bookingIds);
63 65 self::updateMetas($booking, $bookingData, $guests, $customFieldsData, $calendarSlot);
@@ -64,8 +66,9 @@
64 66
65 67 $booking->load('calendar');
66 68
67 69 $bookingStatus = $booking->status;
70 + $paymentStatus = $booking->payment_status;
68 71
69 72 $bookingData = apply_filters('fluent_booking/after_booking_data', $bookingData, $booking, $calendarSlot, $customFieldsData);
70 73
71 74 // this pre hook is for early actions that require for remote calendars and locations
@@ -73,9 +76,9 @@
73 76
74 77 // We are just renewing this as this may have been changed by the pre hook
75 78 $booking = Booking::find($booking->id);
76 79
77 - if ($bookingStatus != $booking->status) {
80 + if (self::preHookHasDispatched($bookingStatus, $paymentStatus, $booking)) {
78 81 return $booking;
79 82 }
80 83
81 84 do_action('fluent_booking/after_booking_' . $booking->status, $booking, $calendarSlot, $bookingData);
@@ -82,8 +85,25 @@
82 85
83 86 return $booking;
84 87 }
85 88
89 + /**
90 + * Whether the pre hook already dispatched the lifecycle action, so
91 + * dispatching again would notify twice. Status is not the only sign: a
92 + * full-price coupon settles payment on a booking that stays pending for
93 + * manual confirmation.
94 + *
95 + * Loose on purpose - payment_status is nullable with no default, and
96 + * multi-time child rows are written as ''.
97 + *
98 + * @return bool
99 + */
100 + private static function preHookHasDispatched($bookingStatus, $paymentStatus, $booking)
101 + {
102 + return $bookingStatus != $booking->status
103 + || $paymentStatus != $booking->payment_status;
104 + }
105 +
86 106 public static function createMultiTimeBooking($data, $calendarSlot, $customFieldsData, $guests)
87 107 {
88 108 $booking = [];
89 109 $bookingIds = [];
@@ -434,10 +454,8 @@
434 454 }
435 455
436 456 public static function generateBookingICS(Booking $booking)
437 457 {
438 - $author = $booking->getHostDetails(false);
439 -
440 458 // Initialize the ICS content
441 459 $icsContent = "BEGIN:VCALENDAR\r\n";
442 460 $icsContent .= "VERSION:2.0\r\n";
443 461 $icsContent .= "PRODID:-//FluentBooking//Fluent Booking//EN\r\n";
@@ -445,9 +463,57 @@
445 463 // PUBLISH = plain "add to calendar" event. METHOD:REQUEST makes it an iTIP
446 464 // invitation bound to the ATTENDEE, which Google Calendar then rejects/mishandles.
447 465 $icsContent .= "METHOD:PUBLISH\r\n";
448 466
449 - $icsContent .= "BEGIN:VEVENT\r\n";
467 + foreach (self::getIcsBookings($booking) as $icsBooking) {
468 + $icsContent .= self::generateIcsEvent($icsBooking);
469 + }
470 +
471 + // Close the VCALENDAR component
472 + $icsContent .= "END:VCALENDAR\r\n";
473 +
474 + return $icsContent;
475 + }
476 +
477 + /**
478 + * A recurring or multiple-time booking is stored as one row per time, with
479 + * the last row as the parent the guest lands on. Its export carries every
480 + * confirmed time in the set, one VEVENT each, so an occurrence that was
481 + * cancelled or is still awaiting confirmation stays out of the calendar.
482 + */
483 + private static function getIcsBookings(Booking $booking)
484 + {
485 + if ($booking->parent_id) {
486 + return [$booking];
487 + }
488 +
489 + // Additional guests on a group booking are linked the same way, each
490 + // with their own email; their bookings are not this guest's to export.
491 + $childBookings = Booking::with(['calendar', 'calendar_event', 'booking_meta'])
492 + ->where('parent_id', $booking->id)
493 + ->where('email', $booking->email)
494 + ->whereIn('status', ['scheduled', 'completed'])
495 + ->get()
496 + ->all();
497 +
498 + if (!$childBookings) {
499 + return [$booking];
500 + }
501 +
502 + $bookings = array_merge($childBookings, [$booking]);
503 +
504 + usort($bookings, function ($first, $second) {
505 + return strtotime($first->start_time) - strtotime($second->start_time);
506 + });
507 +
508 + return $bookings;
509 + }
510 +
511 + private static function generateIcsEvent(Booking $booking)
512 + {
513 + $author = $booking->getHostDetails(false);
514 +
515 + $icsContent = "BEGIN:VEVENT\r\n";
450 516 $icsContent .= "STATUS:CONFIRMED\r\n";
451 517 $icsContent .= "UID:" . md5($booking->hash) . "\r\n"; // Unique ID for the event
452 518 $icsContent .= "DTSTAMP:" . gmdate('Ymd\THis\Z') . "\r\n"; // Required by RFC5545; Google rejects ICS without it
453 519
@@ -466,11 +532,8 @@
466 532 $organizerEmail = sanitize_email($author['email']) ?: $author['email'];
467 533 $icsContent .= "ORGANIZER;CN=\"" . self::escapeIcsText($author['name']) . "\":mailto:" . $organizerEmail . "\r\n";
468 534
469 535 $icsContent .= "END:VEVENT\r\n";
470 -
471 - // Close the VCALENDAR component
472 - $icsContent .= "END:VCALENDAR\r\n";
473 536
474 537 return $icsContent;
475 538 }
476 539