Files
Reservair/includes/Services/RsvTimetableReservationService.php
T
Martin Slachta 8e264b8892 (#3) - templating
2026-06-14 07:21:07 +02:00

193 lines
7.0 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) {
Logger::error("No available capacity for timetable_id: $timetable_id, start_utc: " . $start_utc->format('Y-m-d H:i:s') . ", end_utc: " . $end_utc->format('Y-m-d H:i:s'));
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) {
Logger::error("Overlapping capacity start_time: " . (int) $overlapping_capacity[0]->start_time . " is after the reservation 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);
error_log('reservations: ' . json_encode($reservations));
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);
}
}