190 lines
6.6 KiB
PHP
190 lines
6.6 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
class RsvTimetableReservationService {
|
||
|
|
private RsvTimetableReservationRepository $repo;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Events produced by create() that must not be dispatched until the
|
||
|
|
* surrounding transaction has committed — otherwise a later rollback would
|
||
|
|
* leave listeners (e.g. the maintainer notification email) acting on rows
|
||
|
|
* that no longer exist.
|
||
|
|
*
|
||
|
|
* @var list<object>
|
||
|
|
*/
|
||
|
|
private array $deferred_events = [];
|
||
|
|
|
||
|
|
public function __construct() {
|
||
|
|
$this->repo = new RsvTimetableReservationRepository();
|
||
|
|
}
|
||
|
|
|
||
|
|
public function get_all(): array {
|
||
|
|
return $this->repo->get_all();
|
||
|
|
}
|
||
|
|
|
||
|
|
public function get(int $id) : RsvTimetableReservation {
|
||
|
|
return $this->repo->get($id);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function time_of_day_minutes(DateTime $utc_dt): int {
|
||
|
|
$dt = (clone $utc_dt)->setTimezone(wp_timezone());
|
||
|
|
return (int) $dt->format('G') * 60 + (int) $dt->format('i');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function check_availability(int $timetable_id, DateTime $start_utc, DateTime $end_utc): bool {
|
||
|
|
$overlapping_capacity = (new RsvTimetableCapacityRepository())
|
||
|
|
->get_overlapping_capacity($timetable_id, $start_utc, $end_utc);
|
||
|
|
|
||
|
|
if (count($overlapping_capacity) === 0) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$start_min = $this->time_of_day_minutes($start_utc);
|
||
|
|
$end_min = $this->time_of_day_minutes($end_utc);
|
||
|
|
|
||
|
|
if ((int) $overlapping_capacity[0]->start_time > $start_min) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$max = (int) $overlapping_capacity[0]->end_time;
|
||
|
|
foreach ($overlapping_capacity as $cap) {
|
||
|
|
if ((int) $cap->start_time > $max) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
$max = max($max, (int) $cap->end_time);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($max < $end_min) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$capacity = (int) $overlapping_capacity[0]->capacity;
|
||
|
|
$reservations = $this->repo->get_overlapping($timetable_id, $start_utc, $end_utc);
|
||
|
|
|
||
|
|
return count($reservations) < $capacity;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function is_reservation_valid(RsvTimetableReservation $reservation): bool {
|
||
|
|
return $this->check_availability(
|
||
|
|
$reservation->timetable_id,
|
||
|
|
$reservation->start_utc,
|
||
|
|
$reservation->end_utc,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function create_confirmation(int $reservation_id, int $timetable_reservation_id): string {
|
||
|
|
$code = wp_generate_password(32, false);
|
||
|
|
$this->repo->insert_confirmation($reservation_id, $timetable_reservation_id, $code);
|
||
|
|
return $code;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function set_confirmed_state(string $code, bool $state): int {
|
||
|
|
$confirmation = $this->repo->get_confirmation($code);
|
||
|
|
if ($confirmation === null) {
|
||
|
|
throw new InvalidArgumentException('Confirmation code not found.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->repo->set_confirmed((int) $confirmation['timetable_reservation_id'], $state);
|
||
|
|
$this->repo->delete_confirmation($code);
|
||
|
|
|
||
|
|
$reservation_service = new RsvReservationService();
|
||
|
|
$reservation_service->confirmation_state_changed($confirmation['reservation_id']);
|
||
|
|
|
||
|
|
return $confirmation['timetable_reservation_id'];
|
||
|
|
}
|
||
|
|
|
||
|
|
public function accept(string $code): int {
|
||
|
|
return $this->set_confirmed_state($code, true);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function refuse(string $code): int {
|
||
|
|
return $this->set_confirmed_state($code, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function has_pending_confirmation(int $reservation_id): bool {
|
||
|
|
return $this->repo->has_pending_confirmation($reservation_id);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function get_confirmation_code(int $reservation_id): string {
|
||
|
|
$code = $this->repo->get_confirmation_code($reservation_id);
|
||
|
|
|
||
|
|
if ($code === null) {
|
||
|
|
throw new InvalidArgumentException('No pending confirmation for this reservation.');
|
||
|
|
}
|
||
|
|
|
||
|
|
return $code;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function accept_by_reservation_id(int $reservation_id): void {
|
||
|
|
$this->set_confirmed_state($this->get_confirmation_code($reservation_id), true);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function refuse_by_reservation_id(int $reservation_id): void {
|
||
|
|
$this->set_confirmed_state($this->get_confirmation_code($reservation_id), false);
|
||
|
|
}
|
||
|
|
|
||
|
|
// TODO: Add requires_confirmation parameter
|
||
|
|
public function create(int $reservation_id, RsvTimetableReservation $reservation): int {
|
||
|
|
if (!$this->is_reservation_valid($reservation)) {
|
||
|
|
throw new InvalidArgumentException();
|
||
|
|
}
|
||
|
|
|
||
|
|
$capacity = (new RsvTimetableCapacityRepository())->get_overlapping_capacity(
|
||
|
|
$reservation->timetable_id,
|
||
|
|
$reservation->start_utc,
|
||
|
|
$reservation->end_utc,
|
||
|
|
);
|
||
|
|
$needs_confirmation = array_filter($capacity, fn($c) => (bool) $c->requires_confirmation);
|
||
|
|
|
||
|
|
$insert_array = $reservation->to_array();
|
||
|
|
$insert_array['reservation_id'] = $reservation_id;
|
||
|
|
$insert_array['is_confirmed'] = empty($needs_confirmation);
|
||
|
|
|
||
|
|
$id = $this->repo->insert($insert_array);
|
||
|
|
|
||
|
|
if (!empty($needs_confirmation)) {
|
||
|
|
$maintainer_email = (new RsvTimetableRepository())->get_maintainer_email($reservation->timetable_id);
|
||
|
|
|
||
|
|
if ($maintainer_email) {
|
||
|
|
$code = $this->create_confirmation($reservation_id, $id);
|
||
|
|
$this->deferred_events[] = new RsvTimetableReservationPendingEvent(
|
||
|
|
$reservation_id,
|
||
|
|
$reservation,
|
||
|
|
$code,
|
||
|
|
$maintainer_email
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$reservation_service = new RsvReservationService();
|
||
|
|
$reservation_service->confirmation_state_changed($reservation_id);
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->deferred_events[] = new RsvTimetableReservationCreatedEvent($reservation);
|
||
|
|
|
||
|
|
return $id;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Dispatch (and clear) the events buffered by create(). Callers must invoke
|
||
|
|
* this *after* committing the transaction that wraps create().
|
||
|
|
*/
|
||
|
|
public function flush_deferred_events(): void {
|
||
|
|
$events = $this->deferred_events;
|
||
|
|
$this->deferred_events = [];
|
||
|
|
foreach ($events as $event) {
|
||
|
|
RsvEventDispatcher::dispatch($event);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function get_by_timetable(int $timetable_id, ?int $limit = null, int $skip = 0): array {
|
||
|
|
return $this->repo->get_by_timetable($timetable_id, $limit, $skip);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function count_by_timetable(int $timetable_id): int {
|
||
|
|
return $this->repo->count_by_timetable($timetable_id);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function get_reservations_on_date(int $timetable_id, DateTime $date_utc): array {
|
||
|
|
return $this->repo->get_reservations_on_date($timetable_id, $date_utc);
|
||
|
|
}
|
||
|
|
}
|