mirror of https://github.com/nextcloud/calendar
228 lines
7.1 KiB
PHP
228 lines
7.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
/**
|
|
* Calendar App
|
|
*
|
|
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
|
|
*
|
|
* @author Anna Larch <anna.larch@gmx.net>
|
|
* @author Richard Steinmetz <richard@steinmetz.cloud>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
|
* License as published by the Free Software Foundation; either
|
|
* version 3 of the License, or any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public
|
|
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
namespace OCA\Calendar\Service\Appointments;
|
|
|
|
use DateTimeImmutable;
|
|
use DateTimeZone;
|
|
use InvalidArgumentException;
|
|
use OCA\Calendar\Db\AppointmentConfig;
|
|
use OCA\Calendar\Db\Booking;
|
|
use OCA\Calendar\Db\BookingMapper;
|
|
use OCA\Calendar\Exception\ClientException;
|
|
use OCA\Calendar\Exception\NoSlotFoundException;
|
|
use OCA\Calendar\Exception\ServiceException;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
use OCP\AppFramework\Http;
|
|
use OCP\DB\Exception;
|
|
use OCP\DB\Exception as DbException;
|
|
use OCP\IUser;
|
|
use OCP\Security\ISecureRandom;
|
|
use Psr\Log\LoggerInterface;
|
|
use function count;
|
|
|
|
class BookingService {
|
|
/** @var int the expiry of a booking confirmation */
|
|
public const EXPIRY = 86400;
|
|
|
|
/** @var AvailabilityGenerator */
|
|
private $availabilityGenerator;
|
|
|
|
/** @var SlotExtrapolator */
|
|
private $extrapolator;
|
|
|
|
/** @var DailyLimitFilter */
|
|
private $dailyLimitFilter;
|
|
|
|
/** @var EventConflictFilter */
|
|
private $eventConflictFilter;
|
|
|
|
/** @var BookingCalendarWriter */
|
|
private $calendarWriter;
|
|
|
|
/** @var BookingMapper */
|
|
private $bookingMapper;
|
|
|
|
/** @var ISecureRandom */
|
|
private $random;
|
|
|
|
/** @var MailService */
|
|
private $mailService;
|
|
|
|
/** @var LoggerInterface */
|
|
private $logger;
|
|
|
|
public function __construct(AvailabilityGenerator $availabilityGenerator,
|
|
SlotExtrapolator $extrapolator,
|
|
DailyLimitFilter $dailyLimitFilter,
|
|
EventConflictFilter $eventConflictFilter,
|
|
BookingMapper $bookingMapper,
|
|
BookingCalendarWriter $calendarWriter,
|
|
ISecureRandom $random,
|
|
MailService $mailService,
|
|
LoggerInterface $logger) {
|
|
$this->availabilityGenerator = $availabilityGenerator;
|
|
$this->extrapolator = $extrapolator;
|
|
$this->dailyLimitFilter = $dailyLimitFilter;
|
|
$this->eventConflictFilter = $eventConflictFilter;
|
|
$this->calendarWriter = $calendarWriter;
|
|
$this->bookingMapper = $bookingMapper;
|
|
$this->random = $random;
|
|
$this->mailService = $mailService;
|
|
$this->logger = $logger;
|
|
}
|
|
|
|
/**
|
|
* @throws ClientException|DbException
|
|
*/
|
|
public function confirmBooking(Booking $booking, AppointmentConfig $config): Booking {
|
|
$bookingSlot = current($this->getAvailableSlots($config, $booking->getStart(), $booking->getEnd()));
|
|
|
|
if (!$bookingSlot) {
|
|
throw new ClientException('Slot for booking is not available any more');
|
|
}
|
|
|
|
$tz = new DateTimeZone($booking->getTimezone());
|
|
$startObj = (new DateTimeImmutable())->setTimestamp($booking->getStart())->setTimezone($tz);
|
|
if (!$startObj) {
|
|
throw new ClientException('Could not make sense of booking times');
|
|
}
|
|
|
|
$calendar = $this->calendarWriter->write($config, $startObj, $booking->getDisplayName(), $booking->getEmail(), $booking->getDescription());
|
|
$booking->setConfirmed(true);
|
|
$this->bookingMapper->update($booking);
|
|
try {
|
|
$this->mailService->sendBookingInformationEmail($booking, $config, $calendar);
|
|
} catch (ServiceException $e) {
|
|
$this->logger->info('Could not send booking information email after confirmation by user ' . $booking->getEmail(), ['exception' => $e]);
|
|
}
|
|
return $booking;
|
|
}
|
|
|
|
/**
|
|
* @throws ServiceException|DbException|NoSlotFoundException|InvalidArgumentException
|
|
*/
|
|
public function book(AppointmentConfig $config, int $start, int $end, string $timeZone, string $displayName, string $email, ?string $description = null): Booking {
|
|
$bookingSlot = current($this->getAvailableSlots($config, $start, $end));
|
|
|
|
if (!$bookingSlot) {
|
|
throw new NoSlotFoundException('Could not find slot for booking');
|
|
}
|
|
|
|
try {
|
|
$tz = new DateTimeZone($timeZone);
|
|
} catch (Exception $e) {
|
|
throw new InvalidArgumentException('Could not make sense of the timezone', $e->getCode(), $e);
|
|
}
|
|
|
|
$booking = new Booking();
|
|
$booking->setApptConfigId($config->getId());
|
|
$booking->setCreatedAt(time());
|
|
$booking->setToken($this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC));
|
|
$booking->setDisplayName($displayName);
|
|
$booking->setDescription($description);
|
|
$booking->setEmail($email);
|
|
$booking->setStart($start);
|
|
$booking->setEnd($end);
|
|
$booking->setTimezone($tz->getName());
|
|
try {
|
|
$this->bookingMapper->insert($booking);
|
|
} catch (Exception $e) {
|
|
throw new ServiceException('Could not create booking', 0, $e);
|
|
}
|
|
|
|
try {
|
|
$this->mailService->sendConfirmationEmail($booking, $config);
|
|
} catch (ServiceException $e) {
|
|
$this->bookingMapper->delete($booking);
|
|
throw $e;
|
|
}
|
|
|
|
return $booking;
|
|
}
|
|
|
|
/**
|
|
* @return Interval[]
|
|
*/
|
|
public function getAvailableSlots(AppointmentConfig $config, int $startTime, int $endTime): array {
|
|
if ($config->getFutureLimit() !== null) {
|
|
/** @var int $maxEndTime */
|
|
$maxEndTime = time() + $config->getFutureLimit();
|
|
if ($startTime > $maxEndTime) {
|
|
return [];
|
|
}
|
|
if ($endTime > $maxEndTime) {
|
|
$endTime = $maxEndTime;
|
|
}
|
|
}
|
|
|
|
// 1. Build intervals at which slots may be booked
|
|
$availabilityIntervals = $this->availabilityGenerator->generate($config, $startTime, $endTime);
|
|
// 2. Generate all possible slots
|
|
$allPossibleSlots = $this->extrapolator->extrapolate($config, $availabilityIntervals);
|
|
// 3. Filter out the daily limits
|
|
$filteredByDailyLimit = $this->dailyLimitFilter->filter($config, $allPossibleSlots);
|
|
// 4. Filter out booking conflicts
|
|
$available = $this->eventConflictFilter->filter($config, $filteredByDailyLimit);
|
|
|
|
$this->logger->debug('Appointment config ' . $config->getToken() . ' has {availabilityIntervals} intervals that result in {allPossibleSlots} possible slots. {filteredByDailyLimit} slots remain after the daily limit. {available} available slots remain after conflict checking.', [
|
|
'availabilityIntervals' => count($availabilityIntervals),
|
|
'allPossibleSlots' => count($allPossibleSlots),
|
|
'filteredByDailyLimit' => count($filteredByDailyLimit),
|
|
'available' => count($available)
|
|
]);
|
|
|
|
return $available;
|
|
}
|
|
|
|
/**
|
|
* @throws ClientException
|
|
*/
|
|
public function findByToken(string $token): Booking {
|
|
try {
|
|
return $this->bookingMapper->findByToken($token);
|
|
} catch (DoesNotExistException $e) {
|
|
throw new ClientException(
|
|
"Booking $token does not exist",
|
|
0,
|
|
$e,
|
|
Http::STATUS_NOT_FOUND
|
|
);
|
|
}
|
|
}
|
|
|
|
public function deleteByUser(IUser $user): void {
|
|
$this->bookingMapper->deleteByUserId($user->getUID());
|
|
}
|
|
|
|
/**
|
|
* @throws DbException
|
|
*/
|
|
public function deleteOutdated(): int {
|
|
return $this->bookingMapper->deleteOutdated(self::EXPIRY);
|
|
}
|
|
}
|