2021-09-13 09:01:05 +00:00
< ? php
declare ( strict_types = 1 );
/**
* Calendar App
*
* @ copyright 2021 Anna Larch < anna . larch @ gmx . net >
*
* @ author Anna Larch < anna . larch @ gmx . net >
2022-01-11 14:36:49 +00:00
* @ author Richard Steinmetz < richard @ steinmetz . cloud >
2021-09-13 09:01:05 +00:00
*
* 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 />.
*
*/
2021-10-27 08:28:52 +00:00
2021-09-13 09:01:05 +00:00
namespace OCA\Calendar\Service\Appointments ;
2021-11-19 08:49:22 +00:00
use DateTimeImmutable ;
use DateTimeZone ;
use InvalidArgumentException ;
2021-10-27 08:28:52 +00:00
use OCA\Calendar\Db\AppointmentConfig ;
2021-11-19 08:49:22 +00:00
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 ;
2021-11-25 11:15:40 +00:00
use OCP\IUser ;
2021-11-19 08:49:22 +00:00
use OCP\Security\ISecureRandom ;
2021-11-29 13:03:41 +00:00
use Psr\Log\LoggerInterface ;
use function count ;
2021-09-13 09:01:05 +00:00
class BookingService {
2021-11-19 08:49:22 +00:00
/** @var int the expiry of a booking confirmation */
public const EXPIRY = 86400 ;
2021-10-27 08:28:52 +00:00
/** @var AvailabilityGenerator */
private $availabilityGenerator ;
/** @var SlotExtrapolator */
private $extrapolator ;
2021-09-13 09:01:05 +00:00
2021-10-27 08:28:52 +00:00
/** @var DailyLimitFilter */
private $dailyLimitFilter ;
2021-09-13 09:01:05 +00:00
2021-10-27 08:28:52 +00:00
/** @var EventConflictFilter */
private $eventConflictFilter ;
2021-11-19 08:49:22 +00:00
/** @var BookingCalendarWriter */
private $calendarWriter ;
/** @var BookingMapper */
private $bookingMapper ;
/** @var ISecureRandom */
private $random ;
/** @var MailService */
private $mailService ;
2021-11-29 13:03:41 +00:00
/** @var LoggerInterface */
private $logger ;
2021-10-27 08:28:52 +00:00
public function __construct ( AvailabilityGenerator $availabilityGenerator ,
SlotExtrapolator $extrapolator ,
DailyLimitFilter $dailyLimitFilter ,
2021-11-19 08:49:22 +00:00
EventConflictFilter $eventConflictFilter ,
BookingMapper $bookingMapper ,
BookingCalendarWriter $calendarWriter ,
ISecureRandom $random ,
2021-11-29 13:03:41 +00:00
MailService $mailService ,
LoggerInterface $logger ) {
2021-10-27 08:28:52 +00:00
$this -> availabilityGenerator = $availabilityGenerator ;
$this -> extrapolator = $extrapolator ;
$this -> dailyLimitFilter = $dailyLimitFilter ;
$this -> eventConflictFilter = $eventConflictFilter ;
2021-11-19 08:49:22 +00:00
$this -> calendarWriter = $calendarWriter ;
$this -> bookingMapper = $bookingMapper ;
$this -> random = $random ;
$this -> mailService = $mailService ;
2021-11-29 13:03:41 +00:00
$this -> logger = $logger ;
2021-11-19 08:49:22 +00:00
}
/**
* @ 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 );
2021-11-24 13:20:09 +00:00
if ( ! $startObj ) {
2021-11-19 08:49:22 +00:00
throw new ClientException ( 'Could not make sense of booking times' );
}
2022-10-03 16:04:03 +00:00
$calendar = $this -> calendarWriter -> write ( $config , $startObj , $booking -> getDisplayName (), $booking -> getEmail (), $booking -> getDescription ());
2021-11-19 08:49:22 +00:00
$booking -> setConfirmed ( true );
$this -> bookingMapper -> update ( $booking );
2022-10-03 16:04:03 +00:00
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 ]);
}
2021-11-19 08:49:22 +00:00
return $booking ;
2021-09-13 09:01:05 +00:00
}
2021-11-19 08:49:22 +00:00
/**
2022-03-15 16:12:45 +00:00
* @ throws ServiceException | DbException | NoSlotFoundException | InvalidArgumentException
2021-11-19 08:49:22 +00:00
*/
2022-08-05 06:59:41 +00:00
public function book ( AppointmentConfig $config , int $start , int $end , string $timeZone , string $displayName , string $email , ? string $description = null ) : Booking {
2021-11-19 08:49:22 +00:00
$bookingSlot = current ( $this -> getAvailableSlots ( $config , $start , $end ));
if ( ! $bookingSlot ) {
throw new NoSlotFoundException ( 'Could not find slot for booking' );
}
try {
$tz = new DateTimeZone ( $timeZone );
2021-11-24 13:20:09 +00:00
} catch ( Exception $e ) {
2021-11-19 08:49:22 +00:00
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 );
2021-11-24 13:20:09 +00:00
} catch ( ServiceException $e ) {
2021-11-19 08:49:22 +00:00
$this -> bookingMapper -> delete ( $booking );
throw $e ;
}
return $booking ;
2021-09-13 09:01:05 +00:00
}
/**
2021-10-27 08:28:52 +00:00
* @ return Interval []
2021-09-13 09:01:05 +00:00
*/
2021-10-27 08:28:52 +00:00
public function getAvailableSlots ( AppointmentConfig $config , int $startTime , int $endTime ) : array {
2022-01-11 14:36:49 +00:00
if ( $config -> getFutureLimit () !== null ) {
/** @var int $maxEndTime */
$maxEndTime = time () + $config -> getFutureLimit ();
if ( $startTime > $maxEndTime ) {
return [];
}
if ( $endTime > $maxEndTime ) {
$endTime = $maxEndTime ;
}
}
2021-10-27 08:28:52 +00:00
// 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
2021-11-29 13:03:41 +00:00
$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 ),
2021-11-30 08:10:42 +00:00
'available' => count ( $available )
2021-11-29 13:03:41 +00:00
]);
return $available ;
2021-09-13 09:01:05 +00:00
}
2021-11-19 08:49:22 +00:00
/**
* @ 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
);
}
}
2021-11-25 11:15:40 +00:00
public function deleteByUser ( IUser $user ) : void {
$this -> bookingMapper -> deleteByUserId ( $user -> getUID ());
}
2021-11-19 08:49:22 +00:00
/**
* @ throws DbException
*/
public function deleteOutdated () : int {
return $this -> bookingMapper -> deleteOutdated ( self :: EXPIRY );
}
2021-09-13 09:01:05 +00:00
}