132 lines
4.9 KiB
PHP
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);
|
||
|
|
}
|
||
|
|
}
|