PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / trunk
Booking for Appointments and Events Calendar – Amelia vtrunk
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 / TimeSlot / TimeSlotService.php
ameliabooking / src / Application / Services / TimeSlot Last commit date
TimeSlotService.php 2 months ago
TimeSlotService.php
533 lines
1 <?php
2
3 namespace AmeliaBooking\Application\Services\TimeSlot;
4
5 use AmeliaBooking\Application\Services\Booking\EventApplicationService;
6 use AmeliaBooking\Application\Services\Location\AbstractLocationApplicationService;
7 use AmeliaBooking\Application\Services\Resource\AbstractResourceApplicationService;
8 use AmeliaBooking\Application\Services\User\ProviderApplicationService;
9 use AmeliaBooking\Application\Services\User\UserApplicationService;
10 use AmeliaBooking\Domain\Entity\Booking\Appointment\Appointment;
11 use AmeliaBooking\Domain\Entity\Booking\SlotsEntities;
12 use AmeliaBooking\Domain\Entity\User\Provider;
13 use AmeliaBooking\Domain\Factory\Booking\SlotsEntitiesFactory;
14 use AmeliaBooking\Domain\Services\Entity\EntityService;
15 use AmeliaBooking\Domain\Services\Resource\AbstractResourceService;
16 use AmeliaBooking\Domain\Collection\Collection;
17 use AmeliaBooking\Domain\Common\Exceptions\InvalidArgumentException;
18 use AmeliaBooking\Domain\Entity\Bookable\Service\Service;
19 use AmeliaBooking\Domain\Services\TimeSlot\TimeSlotService as DomainTimeSlotService;
20 use AmeliaBooking\Domain\Services\DateTime\DateTimeService;
21 use AmeliaBooking\Domain\Services\Settings\SettingsService;
22 use AmeliaBooking\Domain\Services\User\ProviderService;
23 use AmeliaBooking\Domain\ValueObjects\String\Status;
24 use AmeliaBooking\Infrastructure\Common\Container;
25 use AmeliaBooking\Infrastructure\Common\Exceptions\QueryExecutionException;
26 use AmeliaBooking\Infrastructure\Repository\Bookable\Service\ServiceRepository;
27 use AmeliaBooking\Infrastructure\Repository\Booking\Appointment\AppointmentRepository;
28 use AmeliaBooking\Infrastructure\Repository\User\ProviderRepository;
29 use AmeliaBooking\Infrastructure\Services\Apple\AbstractAppleCalendarService;
30 use AmeliaBooking\Infrastructure\Services\Google\AbstractGoogleCalendarService;
31 use AmeliaBooking\Infrastructure\Services\Outlook\AbstractOutlookCalendarService;
32 use DateTime;
33 use Exception;
34 use Interop\Container\Exception\ContainerException;
35 use Slim\Exception\ContainerValueNotFoundException;
36
37 /**
38 * Class TimeSlotService
39 *
40 * @package AmeliaBooking\Application\Services\TimeSlot
41 */
42 class TimeSlotService
43 {
44 /** @var Container $container */
45 private $container;
46
47 /**
48 * TimeSlotService constructor.
49 *
50 * @param Container $container
51 */
52 public function __construct(Container $container)
53 {
54 $this->container = $container;
55 }
56
57 /** @noinspection MoreThanThreeArgumentsInspection */
58 /**
59 * @param Service $service
60 * @param DateTime $requiredDateTime
61 * @param DateTime $minimumAppointmentDateTime
62 * @param DateTime $maximumAppointmentDateTime
63 * @param int $providerId
64 * @param int|null $locationId
65 * @param array $selectedExtras
66 * @param int|null $excludeAppointmentId
67 * @param int $personsCount
68 * @param boolean $isFrontEndBooking
69 *
70 * @return boolean
71 * @throws QueryExecutionException
72 * @throws ContainerValueNotFoundException
73 * @throws InvalidArgumentException
74 * @throws ContainerException
75 * @throws Exception
76 */
77 public function isSlotFree(
78 $service,
79 $requiredDateTime,
80 $minimumAppointmentDateTime,
81 $maximumAppointmentDateTime,
82 $providerId,
83 $locationId,
84 $selectedExtras,
85 $excludeAppointmentId,
86 $personsCount,
87 $isFrontEndBooking
88 ) {
89 $dateKey = $requiredDateTime->format('Y-m-d');
90 $timeKey = $requiredDateTime->format('H:i');
91
92 /** @var SettingsService $settingsDS */
93 $settingsDS = $this->container->get('domain.settings.service');
94
95 /** @var EntityService $entityService */
96 $entityService = $this->container->get('domain.entity.service');
97
98 $minimumBookingDateTime = $this->getMinimumDateTimeForBooking(
99 '',
100 $isFrontEndBooking,
101 $settingsDS
102 ->getEntitySettings($service->getSettings())
103 ->getGeneralSettings()
104 ->getMinimumTimeRequirementPriorToBooking()
105 );
106
107 if ($requiredDateTime < $minimumBookingDateTime) {
108 return false;
109 }
110
111 $searchStartDateTime = clone $requiredDateTime;
112
113 $searchStartDateTime->modify('first day of this month')->modify('-1 days');
114
115 $searchEndDateTime = clone $requiredDateTime;
116
117 $searchEndDateTime->modify('+1 days');
118
119 /** @var SlotsEntities $slotsEntities */
120 $slotsEntities = $this->getSlotsEntities(
121 [
122 'isFrontEndBooking' => $isFrontEndBooking,
123 'providerIds' => [$providerId],
124 ]
125 );
126
127 /** @var Service $slotEntitiesService */
128 foreach ($slotsEntities->getServices()->getItems() as $slotEntitiesService) {
129 if ($slotEntitiesService->getId()->getValue() === $service->getId()->getValue()) {
130 $slotEntitiesService->setDuration($service->getDuration());
131 break;
132 }
133 }
134
135 $settings = $this->getSlotsSettings($isFrontEndBooking, $slotsEntities);
136
137 $props = [
138 'startDateTime' => $searchStartDateTime,
139 'endDateTime' => $searchEndDateTime,
140 'minimumDateTime' => $minimumAppointmentDateTime,
141 'maximumDateTime' => $maximumAppointmentDateTime,
142 'serviceId' => $service->getId()->getValue(),
143 'providerIds' => [$providerId],
144 'locationId' => $locationId,
145 'extras' => $selectedExtras,
146 'excludeAppointmentId' => $excludeAppointmentId,
147 'personsCount' => $personsCount,
148 'isFrontEndBooking' => $isFrontEndBooking,
149 ];
150
151 /** @var SlotsEntities $filteredSlotEntities */
152 $filteredSlotEntities = $entityService->getFilteredSlotsEntities(
153 $settings,
154 $props,
155 $slotsEntities
156 );
157
158 $freeSlots = $this->getSlotsByProps(
159 $settings,
160 $props,
161 $filteredSlotEntities
162 );
163
164 return
165 array_key_exists($dateKey, $freeSlots['available']) &&
166 array_key_exists($timeKey, $freeSlots['available'][$dateKey]);
167 }
168
169 /**
170 * @param string $requiredBookingDateTimeString
171 * @param boolean $isFrontEndBooking
172 * @param string $minimumTime
173 *
174 * @return DateTime
175 * @throws Exception
176 */
177 public function getMinimumDateTimeForBooking($requiredBookingDateTimeString, $isFrontEndBooking, $minimumTime)
178 {
179 $requiredTimeOffset = $isFrontEndBooking ? $minimumTime : 0;
180
181 $minimumBookingDateTime = DateTimeService::getNowDateTimeObject()->modify("+{$requiredTimeOffset} seconds");
182
183 $requiredBookingDateTime = DateTimeService::getCustomDateTimeObject($requiredBookingDateTimeString);
184
185 $minimumDateTime = ($minimumBookingDateTime > $requiredBookingDateTime ||
186 $minimumBookingDateTime->format('Y-m-d') === $requiredBookingDateTime->format('Y-m-d')
187 ) ? $minimumBookingDateTime : $requiredBookingDateTime->setTime(0, 0, 0);
188
189 /** @var SettingsService $settingsDS */
190 $settingsDS = $this->container->get('domain.settings.service');
191
192 $pastAvailableDays = $settingsDS->getSetting('general', 'backendSlotsDaysInPast');
193
194 if (!$isFrontEndBooking && $pastAvailableDays) {
195 $minimumDateTime->modify("-{$pastAvailableDays} days");
196 }
197
198 return $minimumDateTime;
199 }
200
201 /**
202 * @param string $requiredBookingDateTimeString
203 * @param boolean $isFrontEndBooking
204 * @param int $maximumTime
205 *
206 * @return DateTime
207 * @throws Exception
208 */
209 public function getMaximumDateTimeForBooking($requiredBookingDateTimeString, $isFrontEndBooking, $maximumTime)
210 {
211 /** @var SettingsService $settingsDS */
212 $settingsDS = $this->container->get('domain.settings.service');
213
214 $futureAvailableDays = $settingsDS->getSetting('general', 'backendSlotsDaysInFuture');
215
216 $days = $maximumTime > $futureAvailableDays ?
217 $maximumTime :
218 $futureAvailableDays;
219
220 $daysAvailableForBooking = $isFrontEndBooking ? $maximumTime : $days;
221
222 $maximumBookingDateTime = DateTimeService::getNowDateTimeObject()->modify("+{$daysAvailableForBooking} day");
223
224 $requiredBookingDateTime = $requiredBookingDateTimeString ?
225 DateTimeService::getCustomDateTimeObject($requiredBookingDateTimeString) : $maximumBookingDateTime;
226
227 return ($maximumBookingDateTime < $requiredBookingDateTime ||
228 $maximumBookingDateTime->format('Y-m-d') === $requiredBookingDateTime->format('Y-m-d')
229 ) ? $maximumBookingDateTime : $requiredBookingDateTime;
230 }
231
232 /**
233 * get provider id values for appointments fetch needed for slot calculation
234 *
235 * @param Collection $providers
236 * @param Collection $resources
237 *
238 * @return array
239 */
240 private function getAppointmentsProvidersIds($providers, $resources)
241 {
242 /** @var AbstractResourceService $resourceService */
243 $resourceService = $this->container->get('domain.resource.service');
244
245 if ($resources->length()) {
246 $resourcesProvidersIds = $resourceService->getResourcesProvidersIds($resources);
247
248 return $resourcesProvidersIds ? array_unique(array_merge($providers->keys(), $resourcesProvidersIds)) : [];
249 }
250
251 return $providers->keys();
252 }
253
254 /**
255 * add busy appointments to providers from google calendar events, outlook calendar events and amelia events
256 *
257 * @param Collection $providers
258 * @param array $props
259 *
260 * @throws ContainerException
261 * @throws Exception
262 */
263 public function setBlockerAppointments($providers, $props)
264 {
265 /** @var AbstractGoogleCalendarService $googleCalendarService */
266 $googleCalendarService = $this->container->get('infrastructure.google.calendar.service');
267
268 /** @var AbstractOutlookCalendarService $outlookCalendarService */
269 $outlookCalendarService = $this->container->get('infrastructure.outlook.calendar.service');
270
271 /** @var AbstractAppleCalendarService $appleCalendarService */
272 $appleCalendarService = $this->container->get('infrastructure.apple.calendar.service');
273
274 /** @var EventApplicationService $eventApplicationService */
275 $eventApplicationService = $this->container->get('application.booking.event.service');
276
277 /** @var ProviderApplicationService $providerApplicationService */
278 $providerApplicationService = $this->container->get('application.user.provider.service');
279
280 try {
281 $googleCalendarService->removeSlotsFromGoogleCalendar(
282 $providers,
283 $props['excludeAppointmentId'],
284 !empty($props['minimumDateTime']) ? $props['minimumDateTime'] : $props['startDateTime'],
285 !empty($props['maximumDateTime']) ? $props['maximumDateTime'] : $props['endDateTime']
286 );
287 } catch (Exception $e) {
288 }
289
290 try {
291 $outlookCalendarService->removeSlotsFromOutlookCalendar(
292 $providers,
293 $props['excludeAppointmentId'],
294 !empty($props['minimumDateTime']) ? $props['minimumDateTime'] : $props['startDateTime'],
295 !empty($props['maximumDateTime']) ? $props['maximumDateTime'] : $props['endDateTime']
296 );
297 } catch (Exception $e) {
298 }
299
300 try {
301 $appleCalendarService->removeSlotsFromAppleCalendar(
302 $providers,
303 $props['excludeAppointmentId'],
304 !empty($props['minimumDateTime']) ? $props['minimumDateTime'] : $props['startDateTime'],
305 !empty($props['maximumDateTime']) ? $props['maximumDateTime'] : $props['endDateTime']
306 );
307 } catch (Exception $e) {
308 }
309
310 $eventApplicationService->removeSlotsFromEvents(
311 $providers,
312 [
313 DateTimeService::getCustomDateTimeObject($props['startDateTime']->format('Y-m-d H:i:s'))
314 ->modify('-10 day')
315 ->format('Y-m-d H:i:s'),
316 DateTimeService::getCustomDateTimeObject($props['startDateTime']->format('Y-m-d H:i:s'))
317 ->modify('+2 years')
318 ->format('Y-m-d H:i:s')
319 ]
320 );
321
322 $providerApplicationService->removeSlotsFromBlockTime(
323 $providers,
324 [
325 $props['startDateTime']->format('Y-m-d H:i:s'),
326 $props['endDateTime']->format('Y-m-d H:i:s')
327 ]
328 );
329 }
330
331 /**
332 * add busy appointments to providers from google calendar events, outlook calendar events and amelia events
333 *
334 * @param array $props
335 * @param SlotsEntities $slotsEntities
336 *
337 * @return Collection
338 * @throws Exception
339 */
340 public function getBookedAppointments($slotsEntities, $props)
341 {
342 /** @var AppointmentRepository $appointmentRepository */
343 $appointmentRepository = $this->container->get('domain.booking.appointment.repository');
344
345 /** @var Collection $appointments */
346 $appointments = new Collection();
347
348 $startDateTime = DateTimeService::getCustomDateTimeObjectInUtc($props['startDateTime']->format('Y-m-d H:i:s'))
349 ->format('Y-m-d H:i:s');
350
351 if ($props['startDateTime']->format('Y-m-d') == DateTimeService::getNowDateTimeObjectInUtc()->format('Y-m-d')) {
352 $startDateTime = DateTimeService::getCustomDateTimeObjectInUtc($props['startDateTime']->format('Y-m-d H:i:s'))
353 ->setTime(0, 0, 0)
354 ->format('Y-m-d H:i:s');
355 }
356
357 $appointmentRepository->getFutureAppointments(
358 $appointments,
359 $this->getAppointmentsProvidersIds(
360 $slotsEntities->getProviders(),
361 $slotsEntities->getResources()
362 ),
363 $startDateTime,
364 DateTimeService::getCustomDateTimeObjectInUtc($props['endDateTime']->format('Y-m-d H:i:s'))
365 ->modify('+1 day')
366 ->format('Y-m-d H:i:s')
367 );
368
369 return $appointments;
370 }
371
372 /**
373 * get slot settings
374 *
375 * @param bool $isFrontEndBooking
376 * @param SlotsEntities $slotsEntities
377 * @param array $customSettings
378 *
379 * @return array
380 * @throws ContainerException
381 */
382 public function getSlotsSettings($isFrontEndBooking, $slotsEntities, $customSettings = [])
383 {
384 /** @var SettingsService $settingsDomainService */
385 $settingsDomainService = $this->container->get('domain.settings.service');
386
387 /** @var UserApplicationService $userApplicationService */
388 $userApplicationService = $this->container->get('application.user.service');
389
390 /** @var \AmeliaBooking\Application\Services\Settings\SettingsService $settingsApplicationService */
391 $settingsApplicationService = $this->container->get('application.settings.service');
392
393 return [
394 'allowAdminBookAtAnyTime' =>
395 isset($customSettings['allowAdminBookAtAnyTime'])
396 ? filter_var($customSettings['allowAdminBookAtAnyTime'], FILTER_VALIDATE_BOOLEAN) :
397 (!$isFrontEndBooking && $userApplicationService->isAdminAndAllowedToBookAtAnyTime()),
398 'allowAdminBookOverApp' => isset($customSettings['allowAdminBookOverApp']) ?
399 filter_var($customSettings['allowAdminBookOverApp'], FILTER_VALIDATE_BOOLEAN) : (
400 !$isFrontEndBooking &&
401 $userApplicationService->isAdminAndAllowedToBookOver()
402 ),
403 'isGloballyBusySlot' =>
404 $settingsDomainService->getSetting('appointments', 'isGloballyBusySlot') &&
405 !$slotsEntities->getResources()->length(),
406 'allowBookingIfPending' =>
407 isset($customSettings['allowBookingIfPending']) ? filter_var($customSettings['allowBookingIfPending'], FILTER_VALIDATE_BOOLEAN) :
408 $settingsDomainService->getSetting('appointments', 'allowBookingIfPending'),
409 'allowBookingIfNotMin' =>
410 isset($customSettings['allowBookingIfNotMin']) ? filter_var($customSettings['allowBookingIfNotMin'], FILTER_VALIDATE_BOOLEAN) :
411 $settingsDomainService->getSetting('appointments', 'allowBookingIfNotMin'),
412 'openedBookingAfterMin' =>
413 $settingsDomainService->getSetting('appointments', 'openedBookingAfterMin'),
414 'timeSlotLength' => isset($customSettings['timeSlotLength']) ? (int)$customSettings['timeSlotLength'] :
415 $settingsDomainService->getSetting('general', 'timeSlotLength'),
416 'serviceDurationAsSlot' =>
417 isset($customSettings['serviceDurationAsSlot']) ? filter_var($customSettings['serviceDurationAsSlot'], FILTER_VALIDATE_BOOLEAN) :
418 $settingsDomainService->getSetting('general', 'serviceDurationAsSlot'),
419 'bufferTimeInSlot' =>
420 isset($customSettings['bufferTimeInSlot']) ? filter_var($customSettings['bufferTimeInSlot'], FILTER_VALIDATE_BOOLEAN) :
421 $settingsDomainService->getSetting('general', 'bufferTimeInSlot'),
422 'globalDaysOff' => $settingsApplicationService->getGlobalDaysOff(),
423 'adminServiceDurationAsSlot' => !$isFrontEndBooking &&
424 $userApplicationService->isAdminAndAllowedToBookAtAnyTime() &&
425 $settingsDomainService->getSetting('roles', 'adminServiceDurationAsSlot'),
426 'limitPerEmployee' => $settingsDomainService->getSetting('roles', 'limitPerEmployee'),
427 'timezonesFeatureEnabled' => $settingsDomainService->getSetting('featuresIntegrations', 'timezones')['enabled'],
428 ];
429 }
430
431 /**
432 * @param array $props
433 *
434 * @return SlotsEntities
435 * @throws InvalidArgumentException
436 * @throws QueryExecutionException
437 */
438 public function getSlotsEntities($props)
439 {
440 /** @var SettingsService $settingsDS */
441 $settingsDS = $this->container->get('domain.settings.service');
442
443 /** @var ServiceRepository $serviceRepository */
444 $serviceRepository = $this->container->get('domain.bookable.service.repository');
445
446 /** @var ProviderRepository $providerRepository */
447 $providerRepository = $this->container->get('domain.users.providers.repository');
448
449 /** @var AbstractResourceApplicationService $resourceApplicationService */
450 $resourceApplicationService = $this->container->get('application.resource.service');
451
452 /** @var AbstractLocationApplicationService $locationAS */
453 $locationAS = $this->container->get('application.location.service');
454
455 /** @var Collection $services */
456 $services = $serviceRepository->getWithExtras(
457 !empty($props['serviceCriteria']) ? $props['serviceCriteria'] : []
458 );
459
460 /** @var Collection $providers */
461 $providers = $providerRepository->getWithSchedule(
462 array_merge(
463 [
464 'dates' => [
465 DateTimeService::getNowDateTimeObject()->modify('-1 days')->format('Y-m-d H:i:s')
466 ],
467 'providers' => $props['providerIds'],
468 'fetchCalendars' => true,
469 ],
470 $props['isFrontEndBooking'] ? ['providerStatus' => Status::VISIBLE, 'show' => 1] : []
471 )
472 );
473
474 /** @var Collection $locations */
475 $locations = $locationAS->getAllIndexedById();
476
477 /** @var Collection $resources */
478 $resources = $settingsDS->isFeatureEnabled('resources')
479 ? $resourceApplicationService->getAll(['status' => Status::VISIBLE])
480 : new Collection();
481
482 /** @var SlotsEntities $slotEntities */
483 $slotEntities = SlotsEntitiesFactory::create();
484
485 $slotEntities->setServices($services);
486
487 $slotEntities->setProviders($providers);
488
489 $slotEntities->setLocations($locations);
490
491 $slotEntities->setResources($resources);
492
493 return $slotEntities;
494 }
495
496 /**
497 * @param array $settings
498 * @param array $props
499 * @param SlotsEntities $slotsEntities
500 *
501 * @return array
502 * @throws ContainerException
503 * @throws Exception
504 */
505 public function getSlotsByProps($settings, $props, $slotsEntities)
506 {
507 /** @var DomainTimeSlotService $timeSlotService */
508 $timeSlotService = $this->container->get('domain.timeSlot.service');
509
510 /** @var SettingsService $settingsService */
511 $settingsService = $this->container->get('domain.settings.service');
512
513 $settings['defaultAppointmentStatus'] = $settingsService
514 ->getEntitySettings($slotsEntities->getServices()->getItem($props['serviceId'])->getSettings())
515 ->getGeneralSettings()
516 ->getDefaultAppointmentStatus();
517
518 /** @var Provider $provider */
519 foreach ($slotsEntities->getProviders()->getItems() as $provider) {
520 $provider->setAppointmentList(new Collection());
521 }
522
523 $this->setBlockerAppointments($slotsEntities->getProviders(), $props);
524
525 return $timeSlotService->getSlots(
526 $settings,
527 $props,
528 $slotsEntities,
529 $this->getBookedAppointments($slotsEntities, $props)
530 );
531 }
532 }
533