feat: add booking time ranges and repeating series

This commit is contained in:
fromme committed 2026-09-13 17:34:28 +02:00
1 parent 59feca2f8e
commit d433d271d0
29 files changed
+2135 -180

No files matched your search

+69 -15
View File
@@ -4,18 +4,22 @@ declare(strict_types=1);
namespace App\Tests\Repository;
use App\Booking\TimeRange;
use App\Entity\Booking;
use App\Entity\Resource;
use App\Entity\ResourceType;
use App\Entity\User;
use App\Repository\BookingRepository;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* Hits the real database, because the behaviour under test is the database's: the unique
* constraint is what actually prevents double-booking.
* Hits the real database, because these are queries.
*
* Note what is NOT here any more: the double-booking guard. It used to be a unique index on
* (resource, date) and was tested by asserting the insert blew up. A booking is now a time range,
* which no unique index can guard, so the guard lives in BookingManager and is tested there --
* what this class covers is the overlap query that guard relies on.
*
* Each test runs inside a transaction that is rolled back afterwards.
*/
@@ -38,7 +42,30 @@ class BookingRepositoryTest extends KernelTestCase
parent::tearDown();
}
public function testTheSameResourceCannotBeBookedTwiceOnOneDay(): void
public function testFindOverlappingSeesASpanThatCollides(): void
{
$desk = $this->resource('D1');
$date = new \DateTimeImmutable('2026-10-01');
$this->persist(new Booking($this->user('[email protected]'), $desk, $date, TimeRange::fromMinutes(9 * 60, 13 * 60)));
$this->em->flush();
self::assertCount(1, $this->bookings->findOverlapping($desk, $date, TimeRange::fromMinutes(11 * 60, 15 * 60)));
}
public function testFindOverlappingTreatsTouchingSpansAsFree(): void
{
$desk = $this->resource('D1');
$date = new \DateTimeImmutable('2026-10-01');
$this->persist(new Booking($this->user('[email protected]'), $desk, $date, TimeRange::fromMinutes(9 * 60, 13 * 60)));
$this->em->flush();
// Half-open: 09:00-13:00 and 13:00-17:00 do not collide.
self::assertSame([], $this->bookings->findOverlapping($desk, $date, TimeRange::fromMinutes(13 * 60, 17 * 60)));
}
public function testAnAllDayBookingBlocksEverySpan(): void
{
$desk = $this->resource('D1');
$date = new \DateTimeImmutable('2026-10-01');
@@ -46,12 +73,7 @@ class BookingRepositoryTest extends KernelTestCase
$this->persist(new Booking($this->user('[email protected]'), $desk, $date));
$this->em->flush();
$this->persist(new Booking($this->user('[email protected]'), $desk, $date));
// The controller catches exactly this and re-renders the fragment with an inline error,
// rather than pre-checking, which would be racy.
$this->expectException(UniqueConstraintViolationException::class);
$this->em->flush();
self::assertCount(1, $this->bookings->findOverlapping($desk, $date, TimeRange::fromMinutes(8 * 60, 8 * 60 + 15)));
}
public function testTheSameResourceOnAnotherDayIsFine(): void
@@ -66,17 +88,31 @@ class BookingRepositoryTest extends KernelTestCase
self::assertCount(1, $this->bookings->findForDate(new \DateTimeImmutable('2026-10-01')));
}
public function testATimeComponentDoesNotCreateASecondSlotForTheSameDay(): void
public function testATimeComponentOnTheDateDoesNotShiftWhichDayABookingIsOn(): void
{
$desk = $this->resource('D1');
$this->persist(new Booking($this->user('[email protected]'), $desk, new \DateTimeImmutable('2026-10-01 08:00')));
$this->persist(new Booking($this->user('[email protected]'), $desk, new \DateTimeImmutable('2026-10-01 17:30')));
$this->em->flush();
$this->em->clear();
self::assertCount(1, $this->bookings->findForDate(new \DateTimeImmutable('2026-10-01')));
}
public function testFindForDateOnlyReturnsWhatOverlapsTheSpan(): void
{
$date = new \DateTimeImmutable('2026-10-01');
$user = $this->user('[email protected]');
$this->persist(new Booking($user, $this->resource('D1'), $date, TimeRange::fromMinutes(9 * 60, 12 * 60)));
$this->persist(new Booking($user, $this->resource('D2'), $date, TimeRange::fromMinutes(14 * 60, 17 * 60)));
$this->em->flush();
$this->persist(new Booking($this->user('[email protected]'), $desk, new \DateTimeImmutable('2026-10-01 17:30')));
$morning = $this->bookings->findForDate($date, TimeRange::fromMinutes(8 * 60, 13 * 60));
$this->expectException(UniqueConstraintViolationException::class);
$this->em->flush();
self::assertCount(1, $morning);
self::assertSame('D1', $morning[0]->getResource()->getLabel());
self::assertCount(2, $this->bookings->findForDate($date));
}
public function testFindForDateCarriesTheBookingHolderForTheTeamView(): void
@@ -92,6 +128,24 @@ class BookingRepositoryTest extends KernelTestCase
self::assertSame('D1', $bookings[0]->getResource()->getLabel());
}
public function testCountPerDateOnlyCountsWhatOverlapsTheSpan(): void
{
$user = $this->user('[email protected]');
$this->persist(new Booking($user, $this->resource('D1'), new \DateTimeImmutable('2026-10-01'), TimeRange::fromMinutes(9 * 60, 12 * 60)));
$this->persist(new Booking($this->user('[email protected]'), $this->resource('D2'), new \DateTimeImmutable('2026-10-01'), TimeRange::fromMinutes(14 * 60, 17 * 60)));
$this->em->flush();
$counts = $this->bookings->countPerDateInRange(
new \DateTimeImmutable('2026-10-01'),
new \DateTimeImmutable('2026-10-31'),
TimeRange::fromMinutes(8 * 60, 13 * 60),
);
// The calendar number has to mean the same thing as the colours on the plan, so it is
// filtered by the same span.
self::assertSame(['2026-10-01' => 1], $counts);
}
public function testCountPerDateInRangeIsKeyedByDay(): void
{
$user = $this->user('[email protected]');