feat: add two- and four-weekly series and drop the weekday picker
This commit is contained in:
1 parent
8b88384d68
commit
faef4d70a8
10 files changed
+166
-103
No files matched your search
@@ -101,27 +101,62 @@ class BookingManagerTest extends KernelTestCase
|
||||
|
||||
public function testASeriesMaterialisesOneBookingPerOccurrence(): void
|
||||
{
|
||||
// 2026-10-05 is a Monday.
|
||||
$series = new BookingSeries(
|
||||
$this->user('[email protected]'),
|
||||
$this->resource('D1'),
|
||||
RecurrenceRule::WEEKLY,
|
||||
new \DateTimeImmutable('2026-10-05'),
|
||||
new \DateTimeImmutable('2026-10-30'),
|
||||
TimeRange::fromMinutes(540, 720),
|
||||
[1, 4],
|
||||
);
|
||||
// 2026-10-05 is a Monday; the Mondays up to the 30th are the 5th, 12th, 19th and 26th.
|
||||
$series = $this->weeklySeries();
|
||||
|
||||
$result = $this->manager->bookSeries($series);
|
||||
|
||||
self::assertSame(8, $result->createdCount());
|
||||
self::assertSame(4, $result->createdCount());
|
||||
self::assertSame(0, $result->skippedCount());
|
||||
foreach ($result->created as $booking) {
|
||||
self::assertContains((int) $booking->getDate()->format('N'), [1, 4]);
|
||||
self::assertSame(1, (int) $booking->getDate()->format('N'));
|
||||
self::assertSame($series, $booking->getSeries());
|
||||
}
|
||||
}
|
||||
|
||||
public function testATwoWeeklySeriesSkipsEveryOtherWeek(): void
|
||||
{
|
||||
$result = $this->manager->bookSeries(new BookingSeries(
|
||||
$this->user('[email protected]'),
|
||||
$this->resource('D1'),
|
||||
RecurrenceRule::TWO_WEEKLY,
|
||||
new \DateTimeImmutable('2026-10-05'),
|
||||
new \DateTimeImmutable('2026-11-30'),
|
||||
TimeRange::fromMinutes(540, 720),
|
||||
));
|
||||
|
||||
self::assertSame(5, $result->createdCount());
|
||||
self::assertSame(
|
||||
['2026-10-05', '2026-10-19', '2026-11-02', '2026-11-16', '2026-11-30'],
|
||||
array_map(static fn (Booking $booking): string => $booking->getDate()->format('Y-m-d'), $result->created),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The property the weekday field was removed in favour of: a stride that is a whole number of
|
||||
* weeks lands on the same weekday every time, so nothing has to store which one it is. A
|
||||
* four-weekly series is the one where this is easy to get wrong by reaching for months.
|
||||
*/
|
||||
public function testAFourWeeklySeriesAlwaysLandsOnTheSameWeekday(): void
|
||||
{
|
||||
$result = $this->manager->bookSeries(new BookingSeries(
|
||||
$this->user('[email protected]'),
|
||||
$this->resource('D1'),
|
||||
RecurrenceRule::FOUR_WEEKLY,
|
||||
new \DateTimeImmutable('2026-10-05'),
|
||||
new \DateTimeImmutable('2026-12-31'),
|
||||
TimeRange::fromMinutes(540, 720),
|
||||
));
|
||||
|
||||
self::assertSame(4, $result->createdCount());
|
||||
foreach ($result->created as $booking) {
|
||||
self::assertSame(1, (int) $booking->getDate()->format('N'), 'every occurrence is a Monday');
|
||||
}
|
||||
// Crossing a month boundary is where a "monthly" reading would diverge: 28 days after
|
||||
// 2 November is 30 November, not 2 December.
|
||||
self::assertSame('2026-11-30', $result->created[2]->getDate()->format('Y-m-d'));
|
||||
}
|
||||
|
||||
public function testADailySeriesCoversEveryDay(): void
|
||||
{
|
||||
$result = $this->manager->bookSeries(new BookingSeries(
|
||||
@@ -142,7 +177,8 @@ class BookingManagerTest extends KernelTestCase
|
||||
public function testASeriesSkipsDaysThatAreAlreadyTaken(): void
|
||||
{
|
||||
$desk = $this->resource('D1');
|
||||
$this->manager->book($this->user('[email protected]'), $desk, new \DateTimeImmutable('2026-10-08'), TimeRange::fromMinutes(600, 660));
|
||||
// The 12th is one of the series' own Mondays.
|
||||
$this->manager->book($this->user('[email protected]'), $desk, new \DateTimeImmutable('2026-10-12'), TimeRange::fromMinutes(600, 660));
|
||||
|
||||
$result = $this->manager->bookSeries(new BookingSeries(
|
||||
$this->user('[email protected]'),
|
||||
@@ -151,11 +187,10 @@ class BookingManagerTest extends KernelTestCase
|
||||
new \DateTimeImmutable('2026-10-05'),
|
||||
new \DateTimeImmutable('2026-10-30'),
|
||||
TimeRange::fromMinutes(540, 720),
|
||||
[1, 4],
|
||||
));
|
||||
|
||||
self::assertSame(7, $result->createdCount());
|
||||
self::assertSame(['8.10.'], $result->skippedDates());
|
||||
self::assertSame(3, $result->createdCount());
|
||||
self::assertSame(['12.10.'], $result->skippedDates());
|
||||
self::assertFalse($result->isCompletelyBlocked());
|
||||
}
|
||||
|
||||
@@ -232,7 +267,7 @@ class BookingManagerTest extends KernelTestCase
|
||||
|
||||
$removed = $this->manager->endSeries($series, new \DateTimeImmutable('2026-10-05'));
|
||||
|
||||
self::assertSame(8, $removed);
|
||||
self::assertSame(4, $removed);
|
||||
self::assertSame([], $this->em->getRepository(Booking::class)->findBy(['series' => $id]));
|
||||
// Nothing of the series is left, so the series row goes too -- note $series has no id
|
||||
// any more at this point, which is why the lookup goes through the captured one.
|
||||
@@ -244,10 +279,11 @@ class BookingManagerTest extends KernelTestCase
|
||||
$series = $this->weeklySeries();
|
||||
$this->manager->bookSeries($series);
|
||||
|
||||
// Leaves the Mondays on the 5th and 12th, removes the 19th and 26th.
|
||||
$removed = $this->manager->endSeries($series, new \DateTimeImmutable('2026-10-15'));
|
||||
|
||||
self::assertSame(5, $removed);
|
||||
self::assertCount(3, $this->em->getRepository(Booking::class)->findBy(['series' => $series]));
|
||||
self::assertSame(2, $removed);
|
||||
self::assertCount(2, $this->em->getRepository(Booking::class)->findBy(['series' => $series]));
|
||||
self::assertSame('2026-10-14', $series->getEndDate()->format('Y-m-d'));
|
||||
}
|
||||
|
||||
@@ -260,7 +296,6 @@ class BookingManagerTest extends KernelTestCase
|
||||
new \DateTimeImmutable('2026-10-05'),
|
||||
new \DateTimeImmutable('2026-10-30'),
|
||||
TimeRange::fromMinutes(540, 720),
|
||||
[1, 4],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -322,18 +322,37 @@ class BookingControllerTest extends WebTestCase
|
||||
$this->client->loginUser($this->user('[email protected]'));
|
||||
$desk = $this->resource('D1');
|
||||
|
||||
// 2026-10-05 is a Monday.
|
||||
// 2026-10-05 is a Monday, and the weekday is taken from it rather than asked for --
|
||||
// the Mondays up to the 30th are the 5th, 12th, 19th and 26th.
|
||||
$response = $this->submitBookingModal($desk, '2026-10-05', [
|
||||
'startTime' => '09:00',
|
||||
'endTime' => '12:00',
|
||||
'repeat' => 'weekly',
|
||||
'weekdays' => ['1', '4'],
|
||||
'until' => '2026-10-30',
|
||||
]);
|
||||
|
||||
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
self::assertCount(8, $this->em->getRepository(Booking::class)->findBy(['resource' => $desk]));
|
||||
self::assertStringContainsString('8 Termine gebucht', (string) $response->getContent());
|
||||
$occurrences = $this->em->getRepository(Booking::class)->findBy(['resource' => $desk]);
|
||||
self::assertCount(4, $occurrences);
|
||||
self::assertStringContainsString('4 Termine gebucht', (string) $response->getContent());
|
||||
foreach ($occurrences as $booking) {
|
||||
self::assertSame(1, (int) $booking->getDate()->format('N'));
|
||||
}
|
||||
}
|
||||
|
||||
public function testAFourWeeklySeriesIsCreatedAndReported(): void
|
||||
{
|
||||
$this->client->loginUser($this->user('[email protected]'));
|
||||
$desk = $this->resource('D1');
|
||||
|
||||
$response = $this->submitBookingModal($desk, '2026-10-05', [
|
||||
'allDay' => '1',
|
||||
'repeat' => 'four_weekly',
|
||||
'until' => '2026-12-31',
|
||||
]);
|
||||
|
||||
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
self::assertCount(4, $this->em->getRepository(Booking::class)->findBy(['resource' => $desk]));
|
||||
}
|
||||
|
||||
public function testASeriesWithoutAnEndDateIsRejected(): void
|
||||
@@ -349,20 +368,6 @@ class BookingControllerTest extends WebTestCase
|
||||
self::assertStringContainsString('Bitte ein Enddatum für die Serie angeben.', (string) $response->getContent());
|
||||
}
|
||||
|
||||
public function testAWeeklySeriesWithoutWeekdaysIsRejected(): void
|
||||
{
|
||||
$this->client->loginUser($this->user('[email protected]'));
|
||||
|
||||
$response = $this->submitBookingModal($this->resource('D1'), '2026-10-05', [
|
||||
'allDay' => '1',
|
||||
'repeat' => 'weekly',
|
||||
'until' => '2026-10-30',
|
||||
]);
|
||||
|
||||
self::assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode());
|
||||
self::assertStringContainsString('Bitte mindestens einen Wochentag wählen.', (string) $response->getContent());
|
||||
}
|
||||
|
||||
public function testASeriesRunningTooFarOutIsRejected(): void
|
||||
{
|
||||
$this->client->loginUser($this->user('[email protected]'));
|
||||
@@ -417,7 +422,6 @@ class BookingControllerTest extends WebTestCase
|
||||
'startTime' => '09:00',
|
||||
'endTime' => '12:00',
|
||||
'repeat' => 'weekly',
|
||||
'weekdays' => ['1'],
|
||||
'until' => '2026-10-26',
|
||||
]);
|
||||
|
||||
@@ -451,10 +455,11 @@ class BookingControllerTest extends WebTestCase
|
||||
/**
|
||||
* Renders the modal, reads its CSRF token, and posts the given booking fields back to it.
|
||||
*
|
||||
* Posted explicitly rather than through DomCrawler's form(): the weekday checkboxes are an
|
||||
* expanded multiple choice rendered as booking[weekdays][], which the crawler's field
|
||||
* registry indexes positionally, so assigning ['1', '4'] sets the second checkbox instead of
|
||||
* the fourth. The token still comes off the rendered form, so the round trip is real.
|
||||
* Posted explicitly rather than through DomCrawler's form(), which lets a test name only the
|
||||
* fields it cares about and leave the rest at their rendered defaults. The token still comes
|
||||
* off the rendered form, so the round trip is real. (This originally worked around the
|
||||
* weekday checkboxes, which the crawler indexed positionally; they are gone, but posting
|
||||
* directly is still the simpler way to drive the modal.)
|
||||
*
|
||||
* @param array<string, mixed> $fields
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user