Files
Reservair/includes/Services/RsvReservationService.php
Martin Slachta 0d829845c4 initial
2026-06-11 19:03:29 +02:00

132 lines
4.9 KiB
PHP

<?php
use Reservair\Database\Db;
use Reservair\Logger\Logger;
class RsvReservationService {
private RsvReservationRepository $repo;
public function __construct() {
$this->repo = new RsvReservationRepository();
}
public function get_all(?int $limit = null, int $skip = 0) {
return $this->repo->get_all($limit, $skip);
}
public function count_all(): int {
return $this->repo->count_all();
}
public function get(int $id) {
return $this->repo->get($id);
}
public function get_detail(int $id): ?array {
return $this->repo->get_detail($id);
}
public function create(RsvReservation $reservation) {
// Serialise the availability-check + insert per timetable so two
// concurrent bookings for the last free slot can't both pass the
// capacity check and oversell. Locks are taken in a stable order to
// avoid lock-ordering deadlocks and released only after the commit.
$timetable_ids = array_values(array_unique(array_map(
fn($tr) => $tr->timetable_id,
$reservation->timetable_reservations
)));
sort($timetable_ids);
foreach ($timetable_ids as $tid) {
if (!Db::acquire_lock($this->booking_lock_name($tid))) {
throw new \RuntimeException('Could not acquire booking lock for timetable ' . $tid);
}
}
$timetable_reservation_service = new RsvTimetableReservationService();
try {
Db::begin_transaction();
try {
$reservation_id = $this->repo->insert($reservation->to_array());
foreach ($reservation->timetable_reservations as $timetable_reservation) {
$timetable_reservation_service->create($reservation_id, $timetable_reservation);
}
Db::commit();
} catch (Exception $e) {
Db::rollback();
Logger::error($e);
throw new \Exception($e);
}
// Only now that the rows are durably committed do we let listeners
// (maintainer emails, calendar sync) observe the new reservation.
$timetable_reservation_service->flush_deferred_events();
$this->confirmation_state_changed($reservation_id);
return $reservation_id;
} finally {
foreach (array_reverse($timetable_ids) as $tid) {
Db::release_lock($this->booking_lock_name($tid));
}
}
}
private function booking_lock_name(int $timetable_id): string {
return Db::prefix() . 'rsv_booking_' . $timetable_id;
}
/** Delete a reservation and its dependent timetable reservations and confirmations. */
public function delete(int $id): void {
$this->repo->delete($id);
}
public function confirmation_state_changed(int $reservation_id): void {
// Terminal state is persisted, so this is idempotent: once a reservation
// is confirmed or refused, repeat calls (e.g. a Google Calendar re-sync)
// short-circuit and never re-dispatch the closing events.
if ($this->repo->is_resolved($reservation_id)) {
Logger::info('Reservation already resolved: ' . $reservation_id);
return;
}
// A reservation with no timetable items has nothing to confirm or refuse;
// bailing out avoids mislabelling it as refused.
if ($this->repo->count_subitems($reservation_id) === 0) {
Logger::info('Reservation has not subitems: ' . $reservation_id);
return;
}
$timetable_service = new RsvTimetableReservationService();
$all_confirmed = $this->repo->are_subitems_confirmed($reservation_id);
$any_pending = $timetable_service->has_pending_confirmation($reservation_id);
if ($all_confirmed) {
$this->repo->set_resolved_state($reservation_id, true);
RsvEventDispatcher::dispatch(new RsvReservationConfirmedEvent($reservation_id));
$form_submit_id = $this->repo->get_form_submit_id($reservation_id);
if ($form_submit_id !== null) {
RsvEventDispatcher::dispatch(new RsvFormSubmitClosedEvent($form_submit_id, $reservation_id, true));
}
return;
}
if (!$any_pending) {
// All items resolved but not all confirmed — at least one refused.
$this->repo->set_resolved_state($reservation_id, false);
RsvEventDispatcher::dispatch(new RsvReservationRefusedEvent($reservation_id));
$form_submit_id = $this->repo->get_form_submit_id($reservation_id);
if ($form_submit_id !== null) {
RsvEventDispatcher::dispatch(new RsvFormSubmitClosedEvent($form_submit_id, $reservation_id, false));
}
return;
}
Logger::info('Waiting for pending confirmations on reservation: ' . $reservation_id);
}
}