feat: split nights off an accommodation booking as separate periods
addresses #869f6z36v
This commit is contained in:
1 parent
972839286c
commit
7001bc0cf2
35 files changed
+2221
-25
No files matched your search
@@ -5,12 +5,16 @@ declare(strict_types=1);
|
||||
namespace App\Tests\Entity\Groups;
|
||||
|
||||
use App\Entity\Groups\AccommodationBooking;
|
||||
use App\Entity\Groups\AccommodationBookingPeriod;
|
||||
use App\Entity\User;
|
||||
use App\Enum\Groups\AccommodationBookingOrigin;
|
||||
use App\Enum\Groups\AccommodationBookingPeriodPosition;
|
||||
use App\Enum\Groups\AccommodationBookingStatus;
|
||||
use App\Exception\ImplausibleBookingPriceException;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
use Symfony\Component\Validator\Validation;
|
||||
|
||||
class AccommodationBookingTest extends TestCase
|
||||
{
|
||||
@@ -209,6 +213,197 @@ class AccommodationBookingTest extends TestCase
|
||||
self::assertSame('Skiclub Nord', $booking->getGroupName());
|
||||
}
|
||||
|
||||
public function testACopyCarriesItsOwnSplitOffPeriods(): void
|
||||
{
|
||||
// The periods are part of the quote being varied, but editing them on the copy must
|
||||
// not reach back into the original.
|
||||
$booking = $this->confirmedBooking();
|
||||
$booking->addPeriod((new AccommodationBookingPeriod(
|
||||
AccommodationBookingPeriodPosition::Leading,
|
||||
new \DateTimeImmutable('2026-03-01'),
|
||||
new \DateTimeImmutable('2026-03-02'),
|
||||
))->setLines([['label' => 'Vorausteam', 'amount' => 30000]]));
|
||||
$original = $booking->getPeriods()->first();
|
||||
|
||||
$copy = clone $booking;
|
||||
$copied = $copy->getPeriods()->first();
|
||||
|
||||
self::assertCount(1, $copy->getPeriods());
|
||||
self::assertNotSame($original, $copied);
|
||||
self::assertSame($copy, $copied->getBooking());
|
||||
self::assertNull($copied->getId());
|
||||
self::assertSame($original->getLines(), $copied->getLines());
|
||||
self::assertSame($booking, $original->getBooking());
|
||||
}
|
||||
|
||||
public function testWithoutPeriodsTheMainPeriodIsTheWholeStay(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-08');
|
||||
|
||||
self::assertEquals(new \DateTimeImmutable('2026-03-01'), $booking->getMainDateFrom());
|
||||
self::assertEquals(new \DateTimeImmutable('2026-03-08'), $booking->getMainDateTo());
|
||||
self::assertSame(7, $booking->getMainNights());
|
||||
self::assertSame(0, $booking->getPeriodsTotal());
|
||||
}
|
||||
|
||||
public function testTheMainPeriodIsWhatRemainsBetweenTheSplitOffPeriods(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-08');
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Trailing, '2026-03-07', '2026-03-08', 2000));
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Leading, '2026-03-01', '2026-03-03', 3000));
|
||||
|
||||
self::assertEquals(new \DateTimeImmutable('2026-03-03'), $booking->getMainDateFrom());
|
||||
self::assertEquals(new \DateTimeImmutable('2026-03-07'), $booking->getMainDateTo());
|
||||
self::assertSame(4, $booking->getMainNights());
|
||||
self::assertSame(7, $booking->getNights(), 'the stay itself is unchanged');
|
||||
self::assertSame(5000, $booking->getPeriodsTotal());
|
||||
}
|
||||
|
||||
public function testPeriodsThatTileTheEdgesOfTheStayAreValid(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-08');
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Leading, '2026-03-01', '2026-03-03', 0));
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Trailing, '2026-03-07', '2026-03-08', 0));
|
||||
|
||||
self::assertCount(0, $this->validate($booking));
|
||||
}
|
||||
|
||||
public function testMovingTheArrivalAwayFromALeadingPeriodIsReportedOnTheArrival(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-02', '2026-03-08');
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Leading, '2026-03-01', '2026-03-03', 0));
|
||||
|
||||
$violations = $this->validate($booking);
|
||||
|
||||
self::assertCount(1, $violations);
|
||||
self::assertSame('dateFrom', $violations[0]->getPropertyPath());
|
||||
}
|
||||
|
||||
public function testASecondPeriodOnTheSameSideIsRefused(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-08');
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Leading, '2026-03-01', '2026-03-02', 0));
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Leading, '2026-03-01', '2026-03-02', 0));
|
||||
|
||||
self::assertCount(1, $this->validate($booking));
|
||||
}
|
||||
|
||||
public function testMovingTheDepartureAwayFromATrailingPeriodIsReportedOnTheDeparture(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-09');
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Trailing, '2026-03-07', '2026-03-08', 0));
|
||||
|
||||
$violations = $this->validate($booking);
|
||||
|
||||
self::assertCount(1, $violations);
|
||||
self::assertSame('dateTo', $violations[0]->getPropertyPath());
|
||||
}
|
||||
|
||||
public function testTheGroupMustKeepAtLeastOneNight(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-03');
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Leading, '2026-03-01', '2026-03-02', 0));
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Trailing, '2026-03-02', '2026-03-03', 0));
|
||||
|
||||
$violations = $this->validate($booking);
|
||||
|
||||
self::assertCount(1, $violations);
|
||||
self::assertSame('dateTo', $violations[0]->getPropertyPath());
|
||||
}
|
||||
|
||||
public function testAPeriodWithoutANightIsRefused(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-08');
|
||||
$booking->addPeriod($this->period(AccommodationBookingPeriodPosition::Leading, '2026-03-01', '2026-03-01', 0));
|
||||
|
||||
self::assertCount(1, $this->validate($booking));
|
||||
}
|
||||
|
||||
public function testSplittingOffAttachesThePeriodToItsEdgeOfTheStay(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-08');
|
||||
|
||||
$first = $booking->splitOffPeriod(AccommodationBookingPeriodPosition::Leading, 2);
|
||||
$last = $booking->splitOffPeriod(AccommodationBookingPeriodPosition::Trailing, 1);
|
||||
|
||||
self::assertSame(['2026-03-01', '2026-03-03'], $this->range($first));
|
||||
self::assertSame(['2026-03-07', '2026-03-08'], $this->range($last));
|
||||
self::assertSame(4, $booking->getMainNights());
|
||||
self::assertCount(0, $this->validate($booking));
|
||||
}
|
||||
|
||||
public function testEachSideCanOnlyBeSplitOnce(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-08');
|
||||
$booking->splitOffPeriod(AccommodationBookingPeriodPosition::Leading, 1);
|
||||
|
||||
self::assertFalse($booking->canSplitOffPeriod(AccommodationBookingPeriodPosition::Leading));
|
||||
self::assertTrue($booking->canSplitOffPeriod(AccommodationBookingPeriodPosition::Trailing));
|
||||
|
||||
$this->expectException(\LogicException::class);
|
||||
$booking->splitOffPeriod(AccommodationBookingPeriodPosition::Leading, 1);
|
||||
}
|
||||
|
||||
public function testResizingKeepsAPeriodAtItsEdge(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-10');
|
||||
$period = $booking->splitOffPeriod(AccommodationBookingPeriodPosition::Trailing, 1);
|
||||
|
||||
$booking->resizePeriod($period, 3);
|
||||
|
||||
self::assertSame(['2026-03-07', '2026-03-10'], $this->range($period));
|
||||
self::assertSame(6, $booking->getMainNights());
|
||||
}
|
||||
|
||||
public function testRemovingAPeriodGivesItsNightsBackAndFreesTheSide(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-08');
|
||||
$period = $booking->splitOffPeriod(AccommodationBookingPeriodPosition::Leading, 2);
|
||||
|
||||
$booking->removePeriod($period);
|
||||
|
||||
self::assertNull($period->getBooking());
|
||||
self::assertSame(7, $booking->getMainNights());
|
||||
self::assertTrue($booking->canSplitOffPeriod(AccommodationBookingPeriodPosition::Leading));
|
||||
}
|
||||
|
||||
public function testGrowingAPeriodIntoTheLastNightOfTheGroupIsRefused(): void
|
||||
{
|
||||
$booking = $this->stay('2026-03-01', '2026-03-04');
|
||||
$period = $booking->splitOffPeriod(AccommodationBookingPeriodPosition::Leading, 1);
|
||||
|
||||
$booking->resizePeriod($period, 3);
|
||||
|
||||
self::assertCount(1, $this->validate($booking));
|
||||
}
|
||||
|
||||
/** @return array{string, string} */
|
||||
private function range(AccommodationBookingPeriod $period): array
|
||||
{
|
||||
return [$period->getDateFrom()->format('Y-m-d'), $period->getDateTo()->format('Y-m-d')];
|
||||
}
|
||||
|
||||
private function stay(string $from, string $to): AccommodationBooking
|
||||
{
|
||||
return (new AccommodationBooking())
|
||||
->setDateFrom(new \DateTimeImmutable($from))
|
||||
->setDateTo(new \DateTimeImmutable($to));
|
||||
}
|
||||
|
||||
private function period(AccommodationBookingPeriodPosition $position, string $from, string $to, int $amount): AccommodationBookingPeriod
|
||||
{
|
||||
return (new AccommodationBookingPeriod($position, new \DateTimeImmutable($from), new \DateTimeImmutable($to)))
|
||||
->setLines([['label' => 'Vorausteam', 'amount' => $amount]]);
|
||||
}
|
||||
|
||||
private function validate(AccommodationBooking $booking): ConstraintViolationListInterface
|
||||
{
|
||||
return Validation::createValidatorBuilder()
|
||||
->enableAttributeMapping()
|
||||
->getValidator()
|
||||
->validate($booking);
|
||||
}
|
||||
|
||||
/**
|
||||
* A record that has been all the way through the lifecycle, so a copy of it has the most
|
||||
* to reset.
|
||||
|
||||
Reference in new issue
Block a user