PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.4.6
Booking for Appointments and Events Calendar – Amelia v2.4.6
2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / src / Application / Services / WaitingList / WaitingListService.php
ameliabooking / src / Application / Services / WaitingList Last commit date
WaitingListService.php 3 weeks ago
WaitingListService.php
246 lines
1 <?php
2
3 namespace AmeliaBooking\Application\Services\WaitingList;
4
5 use AmeliaBooking\Application\Services\Booking\EventApplicationService;
6 use AmeliaBooking\Application\Services\Notification\ApplicationNotificationService;
7 use AmeliaBooking\Domain\Collection\Collection;
8 use AmeliaBooking\Domain\Common\Exceptions\InvalidArgumentException;
9 use AmeliaBooking\Domain\Entity\Booking\Appointment\Appointment;
10 use AmeliaBooking\Domain\Entity\Bookable\Service\Service;
11 use AmeliaBooking\Domain\Entity\Booking\Appointment\CustomerBooking;
12 use AmeliaBooking\Domain\Entity\Booking\Event\Event;
13 use AmeliaBooking\Domain\Entity\Booking\Event\EventTicket;
14 use AmeliaBooking\Domain\Services\Settings\SettingsService;
15 use AmeliaBooking\Domain\ValueObjects\String\BookingStatus;
16 use AmeliaBooking\Infrastructure\Common\Container;
17 use AmeliaBooking\Infrastructure\Repository\Booking\Appointment\AppointmentRepository;
18 use AmeliaBooking\Infrastructure\Common\Exceptions\QueryExecutionException;
19 use Interop\Container\Exception\ContainerException;
20 use Slim\Exception\ContainerValueNotFoundException;
21
22 /**
23 * Class WaitingListService
24 *
25 * Waiting-list helpers for appointments and events (joinability, capacity,
26 * and whether a booking should skip the regular slot availability check).
27 */
28 class WaitingListService
29 {
30 /** @var Container */
31 private $container;
32
33 /**
34 * @param Container $container
35 */
36 public function __construct(Container $container)
37 {
38 $this->container = $container;
39 }
40
41 /**
42 * Notify waiting-list customers that a spot is available for the given appointment.
43 *
44 * Builds the waiting bookings collection from the appointment entity and
45 * dispatches notifications through all enabled channels.
46 *
47 * @param Appointment $appointment
48 *
49 * @throws ContainerException
50 * @throws ContainerValueNotFoundException
51 * @throws QueryExecutionException
52 */
53 public function sendAvailableSpotNotifications($appointment)
54 {
55 $waitingBookings = new Collection();
56
57 foreach ($appointment->getBookings()->getItems() as $booking) {
58 if ($booking->getStatus()->getValue() === BookingStatus::WAITING) {
59 $waitingBookings->addItem($booking);
60 }
61 }
62
63 if ($waitingBookings->length()) {
64 /** @var ApplicationNotificationService $applicationNotificationService */
65 $applicationNotificationService = $this->container->get('application.notification.service');
66
67 $applicationNotificationService->sendWaitingListAvailableSpotNotifications(
68 $appointment,
69 $waitingBookings
70 );
71 }
72 }
73
74 /**
75 * Determine if current booking qualifies as a waiting list booking.
76 *
77 * @param Service $service
78 * @param array $appointmentData (expects bookingStart, serviceId, providerId, bookings array)
79 * @param CustomerBooking $booking
80 *
81 * @return bool True if booking is legitimate waiting list booking (skip slot check), false otherwise.
82 * @throws ContainerValueNotFoundException
83 * @throws ContainerException
84 * @throws QueryExecutionException
85 */
86 public function isWaitingListBooking($service, $appointmentData, $booking)
87 {
88 if (empty($appointmentData['bookings'][0]['status']) || $appointmentData['bookings'][0]['status'] !== BookingStatus::WAITING) {
89 return false;
90 }
91
92 // Extract waiting list settings from service settings JSON
93 $rawSettings = null;
94 if ($service->getSettings()) {
95 $rawSettings = $service->getSettings()->getValue() ?? $service->getSettings();
96 }
97
98 $decoded = [];
99 if (is_string($rawSettings)) {
100 $decoded = json_decode($rawSettings, true) ?: [];
101 } elseif (is_array($rawSettings)) {
102 $decoded = $rawSettings;
103 }
104
105 $waitingSettings = $decoded['waitingList'] ?? [];
106 $waitingEnabled = !empty($waitingSettings['enabled']);
107 $waitingCapacity = isset($waitingSettings['maxCapacity']) ? (int)$waitingSettings['maxCapacity'] : 0;
108
109 if (!$waitingEnabled || $waitingCapacity <= 0) {
110 return false; // feature not enabled / no capacity defined
111 }
112
113 // Retrieve existing appointment(s) at same slot
114 /** @var AppointmentRepository $appointmentRepo */
115 $appointmentRepo = $this->container->get('domain.booking.appointment.repository');
116
117 $existingAppointments = $appointmentRepo->getFiltered([
118 'dates' => [$appointmentData['bookingStart'], $appointmentData['bookingStart']],
119 'services' => [$appointmentData['serviceId']],
120 'providers' => [$appointmentData['providerId']],
121 'skipServices' => true,
122 'skipProviders' => true,
123 'skipCustomers' => true,
124 ]);
125
126 if (!$existingAppointments->length()) {
127 return false;
128 }
129
130 $currentWaitingPersons = 0;
131 foreach ($existingAppointments->getItems() as $existingAppointment) {
132 foreach ($existingAppointment->getBookings()->getItems() as $existingBooking) {
133 if ($existingBooking->getStatus()->getValue() === BookingStatus::WAITING) {
134 $currentWaitingPersons += $existingBooking->getPersons()->getValue();
135 }
136 }
137 }
138
139 $newPersons = $booking->getPersons() ? $booking->getPersons()->getValue() : 1;
140
141 if ($currentWaitingPersons + $newPersons <= $waitingCapacity) {
142 return true;
143 }
144
145 return false;
146 }
147
148 /**
149 * Waiting-list joinability for an event (parity with frontend useWaitingListAvailability).
150 *
151 * Uses EventApplicationService::getEventInfo() occupancy fields (`full`, `closed`, `waiting`)
152 * plus event waiting-list settings.
153 *
154 * @param Event $event
155 * @param array|null $info Result of getEventInfo(); computed when omitted.
156 *
157 * @return array{
158 * available: bool,
159 * maxCapacity: int,
160 * peopleWaiting: int,
161 * spotsLeft: int,
162 * maxExtraPeople: int|null
163 * }|null Null when waiting list feature/settings are unavailable.
164 *
165 * @throws ContainerException
166 * @throws ContainerValueNotFoundException
167 * @throws InvalidArgumentException
168 */
169 public function getEventWaitingListAvailability($event, $info = null)
170 {
171 /** @var SettingsService $settingsDS */
172 $settingsDS = $this->container->get('domain.settings.service');
173
174 if (!$settingsDS->isFeatureEnabled('waitingList')) {
175 return null;
176 }
177
178 $eventSettings = $event->getSettings() && $event->getSettings()->getValue()
179 ? json_decode($event->getSettings()->getValue(), true)
180 : null;
181
182 if (!is_array($eventSettings) || empty($eventSettings['waitingList']['enabled'])) {
183 return null;
184 }
185
186 if ($info === null) {
187 /** @var EventApplicationService $eventApplicationService */
188 $eventApplicationService = $this->container->get('application.booking.event.service');
189 $info = $eventApplicationService->getEventInfo($event, true);
190 }
191
192 $waitingList = $eventSettings['waitingList'];
193 $peopleWaiting = isset($info['waiting']) ? (int) $info['waiting'] : 0;
194 $maxCapacity = isset($waitingList['maxCapacity']) ? (int) $waitingList['maxCapacity'] : 0;
195 $maxExtraPeople = null;
196
197 if (!empty($waitingList['maxExtraPeopleEnabled']) && isset($waitingList['maxExtraPeople'])) {
198 $maxExtraPeople = (int) $waitingList['maxExtraPeople'];
199 }
200
201 $capacityRule = false;
202 $waitingAlreadyStarted = 0;
203
204 if ($event->getCustomPricing() && $event->getCustomPricing()->getValue()) {
205 $capacityRulePerTicket = [];
206 $maxCustomCapacity = $event->getMaxCustomCapacity() && $event->getMaxCustomCapacity()->getValue();
207 $ticketWaitingCapacity = 0;
208
209 /** @var EventTicket $ticket */
210 foreach ($event->getCustomTickets()->getItems() as $ticket) {
211 $waiting = $ticket->getWaiting() ? $ticket->getWaiting()->getValue() : 0;
212 $waitingAlreadyStarted += $waiting;
213 $waitingListSpots = $ticket->getWaitingListSpots() ? $ticket->getWaitingListSpots()->getValue() : 0;
214 $ticketWaitingCapacity += $waitingListSpots;
215
216 if (!$maxCustomCapacity) {
217 $capacityRulePerTicket[] = $waitingListSpots > $waiting;
218 }
219 }
220
221 if (!$maxCustomCapacity) {
222 $capacityRule = in_array(true, $capacityRulePerTicket, true);
223 $maxCapacity = $ticketWaitingCapacity;
224 } else {
225 $capacityRule = $maxCapacity > $peopleWaiting;
226 }
227 } else {
228 $capacityRule = $maxCapacity > $peopleWaiting;
229 $waitingAlreadyStarted = $peopleWaiting;
230 }
231
232 $spotsLeft = max(0, $maxCapacity - $peopleWaiting);
233 $available = empty($info['closed'])
234 && $capacityRule
235 && (!empty($info['full']) || $waitingAlreadyStarted !== 0);
236
237 return [
238 'available' => $available,
239 'maxCapacity' => $maxCapacity,
240 'peopleWaiting' => $peopleWaiting,
241 'spotsLeft' => $spotsLeft,
242 'maxExtraPeople' => $maxExtraPeople,
243 ];
244 }
245 }
246