From e5aaaeba012f79db3c2c61df7c7f2a2e42d50cdb Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Thu, 8 Feb 2024 16:32:42 +0100 Subject: [PATCH] fix(appointments): allow 5 minute increments for rounding in slot bookings Signed-off-by: Anna Larch --- .../Appointments/AvailabilityGenerator.php | 19 ++-- .../AvailabilityGeneratorTest.php | 95 ++++++++++++++++--- 2 files changed, 96 insertions(+), 18 deletions(-) diff --git a/lib/Service/Appointments/AvailabilityGenerator.php b/lib/Service/Appointments/AvailabilityGenerator.php index e25d38f30f..9adabeac3b 100644 --- a/lib/Service/Appointments/AvailabilityGenerator.php +++ b/lib/Service/Appointments/AvailabilityGenerator.php @@ -67,8 +67,8 @@ public function generate(AppointmentConfig $config, // E.g. 5m slots should only be available at 10:20 and 10:25, not at 10:17 // when the user opens the page at 10:17. // But only do this when the time isn't already a "pretty" time - if ($earliestStart % $config->getIncrement() !== 0) { - $roundTo = (int)round(($config->getIncrement()) / 300) * 300; + if ($earliestStart % $config->getLength() !== 0) { + $roundTo = (int)round(($config->getLength()) / 300) * 300; $earliestStart = (int)ceil($earliestStart / $roundTo) * $roundTo; } @@ -94,8 +94,7 @@ public function generate(AppointmentConfig $config, $timeZone = $availabilityRule['timezoneId']; $slots = $availabilityRule['slots']; - $applicableSlots = $this->filterDates($start, $slots, $timeZone); - + $applicableSlots = $this->filterDates($start, $slots, $timeZone, $config->getLength()); $intervals = []; foreach ($applicableSlots as $slot) { if ($slot->getEnd() <= $earliestStart || $slot->getStart() >= $latestEnd) { @@ -121,7 +120,7 @@ public function generate(AppointmentConfig $config, * * @return Interval[] */ - private function filterDates(int $start, array $availabilityArray, string $timeZone): array { + private function filterDates(int $start, array $availabilityArray, string $timeZone, int $duration): array { $tz = new DateTimeZone($timeZone); // First, transform all timestamps to DateTime Objects $availabilityRules = []; @@ -131,9 +130,15 @@ private function filterDates(int $start, array $availabilityArray, string $timeZ continue; } foreach ($availabilitySlots as $slot) { + // Fix "not-pretty" timeslots + // A slot from 10:10 to 10:40 could be generated but isn't bookable + // So we round them to the next highest time that is pretty for that slot + $roundTo = (int)round(($duration) / 300) * 300; + $prettyStart = (int)ceil($slot['start'] / $roundTo) * $roundTo; + $prettyEnd = (int)ceil($slot['end'] / $roundTo) * $roundTo; $availabilityRules[$key][] = [ - 'start' => (new DateTimeImmutable())->setTimezone($tz)->setTimestamp($slot['start']), - 'end' => (new DateTimeImmutable())->setTimezone($tz)->setTimestamp($slot['end']) + 'start' => (new DateTimeImmutable())->setTimezone($tz)->setTimestamp($prettyStart), + 'end' => (new DateTimeImmutable())->setTimezone($tz)->setTimestamp($prettyEnd) ]; } } diff --git a/tests/php/unit/Service/Appointments/AvailabilityGeneratorTest.php b/tests/php/unit/Service/Appointments/AvailabilityGeneratorTest.php index bd4107f782..6b846ab31b 100644 --- a/tests/php/unit/Service/Appointments/AvailabilityGeneratorTest.php +++ b/tests/php/unit/Service/Appointments/AvailabilityGeneratorTest.php @@ -80,7 +80,7 @@ public function testNoAvailabilitySetRoundToFive(): void { self::assertCount(1, $slots); self::assertEquals(0, $slots[0]->getStart() % 900); - self::assertEquals(4500, $slots[0]->getStart()); + self::assertEquals(5400, $slots[0]->getStart()); } public function testNoAvailabilitySetRoundWithSpecificTimes(): void { @@ -93,37 +93,50 @@ public function testNoAvailabilitySetRoundWithSpecificTimes(): void { self::assertCount(1, $slots); self::assertEquals(0, $slots[0]->getStart() % 900); - self::assertEquals(3600, ($slots[0]->getEnd() - $slots[0]->getStart())); + self::assertEquals(2700, ($slots[0]->getEnd() - $slots[0]->getStart())); self::assertEquals( - [new Interval(1637837100, 1637840700)], + [new Interval(1637838000, 1637840700)], $slots, ); } - public function testNoAvailabilitySetRoundWithIncrement(): void { + public function testNoAvailabilitySetRoundWithIncrementForHalfHour(): void { $config = new AppointmentConfig(); - $config->setLength(5400); + $config->setLength(3600); $config->setIncrement(3600); $config->setAvailability(null); - $slots = $this->generator->generate($config, 1 * 5400, 2 * 5400); + $slots = $this->generator->generate($config, 1 * 1800, 3 * 3600); self::assertCount(1, $slots); self::assertEquals(0, $slots[0]->getStart() % 3600); - self::assertEquals(7200, $slots[0]->getStart()); + self::assertEquals(3600, $slots[0]->getStart()); + } + + public function testNoAvailabilitySetRoundWithIncrementForFullHour(): void { + $config = new AppointmentConfig(); + $config->setLength(3600); + $config->setIncrement(3600); + $config->setAvailability(null); + + $slots = $this->generator->generate($config, 1 * 3600, 2 * 3600); + + self::assertCount(1, $slots); + self::assertEquals(0, $slots[0]->getStart() % 3600); + self::assertEquals(3600, $slots[0]->getStart()); } public function testNoAvailabilitySetRoundToPrettyNumbers(): void { $config = new AppointmentConfig(); - $config->setLength(5400); + $config->setLength(3550); $config->setIncrement(300); $config->setAvailability(null); - $slots = $this->generator->generate($config, 1 * 5400 + 1, 2 * 5400 + 1); + $slots = $this->generator->generate($config, 1 * 3550 + 1, 2 * 3550 + 1); self::assertCount(1, $slots); self::assertEquals(0, $slots[0]->getStart() % 300); - self::assertEquals(5700, $slots[0]->getStart()); + self::assertEquals(3600, $slots[0]->getStart()); } public function testNoAvailabilitySetRoundWithFourtyMinutes(): void { @@ -149,7 +162,7 @@ public function testNoAvailabilitySetRoundWithFourtyMinutesNotPretty(): void { self::assertCount(1, $slots); self::assertEquals(0, $slots[0]->getStart() % 300); - self::assertEquals(2700, $slots[0]->getStart()); + self::assertEquals(4800, $slots[0]->getStart()); } public function testNoAvailabilityButEndDate(): void { @@ -610,4 +623,64 @@ public function testAucklandAndViennaComplexRuleNoResult(): void { $slots = $this->generator->generate($config, $wednesdayMidnight->getTimestamp(), $thursdayMidnight->getTimestamp()); self::assertCount(0, $slots); } + + public function testViennaComplexRuleForBooking(): void { + $tz = new DateTimeZone('Europe/Vienna'); + $dateTime = (new DateTimeImmutable())->setTimezone($tz)->setDate(2021, 11, 22); + $config = new AppointmentConfig(); + $config->setLength(3600); + $config->setIncrement(3600); + $config->setAvailability(json_encode([ + 'timezoneId' => $tz->getName(), + 'slots' => [ + 'MO' => [ + [ + 'start' => $dateTime->setTime(8, 0)->getTimestamp(), + 'end' => $dateTime->setTime(12, 0)->getTimestamp(), + ], + [ + 'start' => $dateTime->setTime(14, 0)->getTimestamp(), + 'end' => $dateTime->setTime(18, 0)->getTimestamp(), + ] + ], + 'TU' => [ + [ + 'start' => $dateTime->setTime(8, 30)->getTimestamp(), + 'end' => $dateTime->setTime(11, 45)->getTimestamp(), + ] + ], + 'WE' => [ + [ + 'start' => $dateTime->setTime(13, 10)->getTimestamp(), + 'end' => $dateTime->setTime(16, 0)->getTimestamp(), + ] + ], + 'TH' => [ + [ + 'start' => $dateTime->setTime(19, 0)->getTimestamp(), + 'end' => $dateTime->setTime(23, 59)->getTimestamp(), + ] + ], + 'FR' => [ + [ + 'start' => $dateTime->setTime(6, 0)->getTimestamp(), + 'end' => $dateTime->setTime(8, 0)->getTimestamp(), + ] + ], + 'SA' => [ + [ + 'start' => $dateTime->setTime(1, 52)->getTimestamp(), + 'end' => $dateTime->setTime(17, 0)->getTimestamp(), + ] + ], + 'SU' => [], + ] + ], JSON_THROW_ON_ERROR)); + $mondayMidnight = (new DateTimeImmutable())->setDate(2021, 11, 13)->setTime(13, 10); + $sundayMidnight = $mondayMidnight->modify('+1 hour'); + + $slots = $this->generator->generate($config, $mondayMidnight->getTimestamp(), $sundayMidnight->getTimestamp()); + + self::assertCount(1, $slots); + } }