feat: accommodation booking type and status as dedicated properties

This commit is contained in:
Björn Fromme
2026-08-06 08:52:09 +02:00
parent 2cfa87f44b
commit aa8fa5c1b2
22 changed files with 218 additions and 151 deletions
@@ -25,32 +25,32 @@ use Symfony\Component\HttpFoundation\Response;
*/
class EditControllerTest extends TestCase
{
public function testTransitionToInquiryIssuesAccessLinkAndSendsCustomerEmail(): void
public function testTransitionToOpenIssuesAccessLinkAndSendsCustomerEmail(): void
{
$bookingService = $this->assertNotified(AccommodationBookingStatus::Inquiry);
$bookingService = $this->assertNotified(AccommodationBookingStatus::Open);
$this->submitStatusChange(AccommodationBookingStatus::Draft, AccommodationBookingStatus::Inquiry, $bookingService);
$this->submitStatusChange(AccommodationBookingStatus::Draft, AccommodationBookingStatus::Open, $bookingService);
}
public function testTransitionToBookingIssuesAccessLinkAndSendsCustomerEmail(): void
public function testTransitionToAcceptedIssuesAccessLinkAndSendsCustomerEmail(): void
{
$bookingService = $this->assertNotified(AccommodationBookingStatus::Booking);
$bookingService = $this->assertNotified(AccommodationBookingStatus::Accepted);
$this->submitStatusChange(AccommodationBookingStatus::Draft, AccommodationBookingStatus::Booking, $bookingService);
$this->submitStatusChange(AccommodationBookingStatus::Draft, AccommodationBookingStatus::Accepted, $bookingService);
}
public function testTransitionToDiscardedSendsNothing(): void
{
$bookingService = $this->assertNotNotified();
$this->submitStatusChange(AccommodationBookingStatus::Inquiry, AccommodationBookingStatus::Discarded, $bookingService);
$this->submitStatusChange(AccommodationBookingStatus::Open, AccommodationBookingStatus::Discarded, $bookingService);
}
public function testSavingWithoutStatusChangeSendsNothing(): void
{
$bookingService = $this->assertNotNotified();
$this->submitStatusChange(AccommodationBookingStatus::Booking, AccommodationBookingStatus::Booking, $bookingService);
$this->submitStatusChange(AccommodationBookingStatus::Accepted, AccommodationBookingStatus::Accepted, $bookingService);
}
private function assertNotified(AccommodationBookingStatus $expected): AccommodationBookingService
@@ -8,6 +8,7 @@ use App\Controller\Api\AccommodationBookingController;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\AccommodationBooking;
use App\Enum\Groups\AccommodationBookingStatus;
use App\Enum\Groups\AccommodationBookingType;
use App\Repository\Groups\AccommodationBookingRepository;
use App\Service\AccommodationBookingBreakdownCalculator;
use App\Service\AccommodationBookingService;
@@ -55,7 +56,8 @@ class AccommodationBookingControllerTest extends TestCase
$booking->setAccommodationDiscount(10);
$booking->setBoardServiceDiscount(20);
$booking->setAdditionalServicesDiscount(30);
$booking->setStatus(AccommodationBookingStatus::Booking);
$booking->setStatus(AccommodationBookingStatus::Accepted);
$booking->setType(AccommodationBookingType::Booking);
$booking->setAcceptedAt(new \DateTimeImmutable('2026-07-15T10:00:00+00:00'));
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
@@ -85,7 +87,8 @@ class AccommodationBookingControllerTest extends TestCase
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
self::assertSame([
'uuid' => $booking->getUuid(),
'status' => 'booking',
'status' => 'accepted',
'type' => 'booking',
'dateFrom' => '2026-07-20',
'dateTo' => '2026-07-25',
'nights' => 5,
@@ -135,7 +138,7 @@ class AccommodationBookingControllerTest extends TestCase
self::assertArrayNotHasKey('currency', $payload['accommodation']);
}
public function testSingleReturnsInquiryStatusWhenBookingIsInquiry(): void
public function testSingleReturnsOpenStatusAndInquiryTypeForAnOffer(): void
{
$booking = new AccommodationBooking();
$booking->setDateFrom(new \DateTimeImmutable('2026-08-01'));
@@ -144,7 +147,8 @@ class AccommodationBookingControllerTest extends TestCase
$booking->setFirstName('Tom');
$booking->setLastName('Beispiel');
$booking->setEmail('[email protected]');
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$booking->setType(AccommodationBookingType::Inquiry);
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
$bookingRepository
@@ -160,7 +164,8 @@ class AccommodationBookingControllerTest extends TestCase
$response = $controller->single($booking->getUuid());
$payload = json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);
self::assertSame('inquiry', $payload['status']);
self::assertSame('open', $payload['status']);
self::assertSame('inquiry', $payload['type']);
self::assertNull($payload['priceBreakdown']);
self::assertNull($payload['acceptedAt']);
}
@@ -186,7 +191,7 @@ class AccommodationBookingControllerTest extends TestCase
self::assertSame(['message' => 'Not found'], $payload);
}
public function testAcceptTransitionsInquiryToBookingAndSetsAcceptedAt(): void
public function testAcceptAcceptsOpenOfferAndSetsAcceptedAt(): void
{
$booking = new AccommodationBooking();
$booking->setDateFrom(new \DateTimeImmutable('2026-08-01'));
@@ -195,7 +200,7 @@ class AccommodationBookingControllerTest extends TestCase
$booking->setFirstName('Tom');
$booking->setLastName('Beispiel');
$booking->setEmail('[email protected]');
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
$bookingRepository
@@ -213,7 +218,7 @@ class AccommodationBookingControllerTest extends TestCase
->method('acceptBooking')
->with($booking)
->willReturnCallback(static function (AccommodationBooking $b): void {
$b->setStatus(AccommodationBookingStatus::Booking);
$b->setStatus(AccommodationBookingStatus::Accepted);
$b->setAcceptedAt(new \DateTimeImmutable());
});
@@ -223,8 +228,9 @@ class AccommodationBookingControllerTest extends TestCase
$payload = json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
self::assertSame('booking', $payload['status']);
self::assertSame(AccommodationBookingStatus::Booking, $booking->getStatus());
self::assertSame('accepted', $payload['status']);
self::assertSame('inquiry', $payload['type']);
self::assertSame(AccommodationBookingStatus::Accepted, $booking->getStatus());
self::assertNotNull($booking->getAcceptedAt());
self::assertSame($booking->getAcceptedAt()->format(\DATE_ATOM), $payload['acceptedAt']);
}
@@ -240,7 +246,7 @@ class AccommodationBookingControllerTest extends TestCase
$booking->setFirstName('Tom');
$booking->setLastName('Beispiel');
$booking->setEmail('[email protected]');
$booking->setStatus(AccommodationBookingStatus::Booking);
$booking->setStatus(AccommodationBookingStatus::Accepted);
$booking->setAcceptedAt($acceptedAt);
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
@@ -261,7 +267,7 @@ class AccommodationBookingControllerTest extends TestCase
$payload = json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
self::assertSame('booking', $payload['status']);
self::assertSame('accepted', $payload['status']);
self::assertSame($acceptedAt, $booking->getAcceptedAt());
self::assertSame($acceptedAt->format(\DATE_ATOM), $payload['acceptedAt']);
}
@@ -26,7 +26,7 @@ class OfferControllerTest extends TestCase
public function testAccessAuthorizesSessionAndRedirectsToView(): void
{
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
$bookingRepository->method('findOneBy')->with(['uuid' => $booking->getUuid()])->willReturn($booking);
@@ -151,7 +151,7 @@ class OfferControllerTest extends TestCase
public function testViewRendersOfferWhenSessionAuthorized(): void
{
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$booking->setAccommodation(new Accommodation());
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
@@ -207,7 +207,7 @@ class OfferControllerTest extends TestCase
public function testConfirmGetRendersModalWithFreshForm(): void
{
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
$bookingRepository->method('findOneBy')->with(['uuid' => $booking->getUuid()])->willReturn($booking);
@@ -238,7 +238,7 @@ class OfferControllerTest extends TestCase
public function testConfirmPostValidAcceptsBookingAndRedirects(): void
{
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
$bookingRepository->method('findOneBy')->willReturn($booking);
@@ -273,7 +273,7 @@ class OfferControllerTest extends TestCase
public function testConfirmPostValidViaHtmxReturnsHxRedirect(): void
{
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
$bookingRepository->method('findOneBy')->willReturn($booking);
@@ -308,7 +308,7 @@ class OfferControllerTest extends TestCase
public function testConfirmPostInvalidReRendersModalWithoutAccepting(): void
{
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
$bookingRepository->method('findOneBy')->willReturn($booking);
@@ -342,7 +342,7 @@ class OfferControllerTest extends TestCase
public function testConfirmRedirectsWhenAlreadyAccepted(): void
{
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Booking);
$booking->setStatus(AccommodationBookingStatus::Accepted);
$bookingRepository = $this->createMock(AccommodationBookingRepository::class);
$bookingRepository->method('findOneBy')->willReturn($booking);
@@ -462,7 +462,7 @@ final class TestableOfferController extends OfferController
$this->renderedParameters = $parameters;
if ('groups/booking/offer.html.twig' === $view) {
$content = $parameters['booking']->isInquiry() ? '<html>offer</html>' : '<html>confirmed</html>';
$content = $parameters['booking']->isAccepted() ? '<html>confirmed</html>' : '<html>offer</html>';
} else {
$content = '<html>unavailable</html>';
}
@@ -7,7 +7,7 @@ namespace App\Tests\Controller\Groups;
use App\Controller\Groups\Step4Controller;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\AccommodationBooking;
use App\Enum\Groups\AccommodationBookingStatus;
use App\Enum\Groups\AccommodationBookingType;
use App\Form\Model\AccommodationBookingDto;
use App\Service\AccommodationBookingService;
use App\Service\AccommodationSessionManager;
@@ -102,7 +102,7 @@ class Step4ControllerTest extends TestCase
->setCalendarCode('HOTEL')
->setMaxAdolescentAge(17);
$booking = (new AccommodationBooking())->setStatus(AccommodationBookingStatus::Inquiry);
$booking = (new AccommodationBooking())->setType(AccommodationBookingType::Inquiry);
$bookingService = $this->createMock(AccommodationBookingService::class);
$bookingService->method('loadAccommodation')->with(1)->willReturn($accommodation);
@@ -275,7 +275,7 @@ class Step4ControllerTest extends TestCase
->setCalendarCode('HOTEL')
->setMaxAdolescentAge(17);
$booking = (new AccommodationBooking())->setStatus(AccommodationBookingStatus::Booking);
$booking = (new AccommodationBooking())->setType(AccommodationBookingType::Booking);
$bookingService = $this->createMock(AccommodationBookingService::class);
$bookingService->method('loadAccommodation')->with(1)->willReturn($accommodation);
@@ -47,8 +47,8 @@ class AccommodationBookingTypeTest extends TestCase
*/
public static function customerFacingStatuses(): iterable
{
yield 'inquiry' => [AccommodationBookingStatus::Inquiry];
yield 'booking' => [AccommodationBookingStatus::Booking];
yield 'open' => [AccommodationBookingStatus::Open];
yield 'accepted' => [AccommodationBookingStatus::Accepted];
}
/**
@@ -67,7 +67,7 @@ class AccommodationBookingTypeTest extends TestCase
public function testPromotedBookingRequiresContactData(): void
{
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$violations = $this->validate($booking, ['Default', 'edit']);
@@ -9,6 +9,7 @@ use App\Entity\Groups\Accommodation;
use App\Entity\Groups\AccommodationBooking;
use App\Entity\Groups\AccommodationPrice;
use App\Enum\Groups\AccommodationBookingStatus;
use App\Enum\Groups\AccommodationBookingType;
use App\Model\AccommodationBookingQueryParams;
use App\Repository\Groups\AccommodationPriceRepository;
use App\Repository\Groups\AccommodationRepository;
@@ -66,7 +67,7 @@ class AccommodationBookingServiceTest extends TestCase
$service = $this->createServiceWithAccommodation(entityManager: $entityManager);
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Booking);
$booking->setType(AccommodationBookingType::Booking);
$service->issueAccessLinkForDirectBooking($booking);
@@ -81,7 +82,7 @@ class AccommodationBookingServiceTest extends TestCase
$service = $this->createServiceWithAccommodation(entityManager: $entityManager);
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setType(AccommodationBookingType::Inquiry);
$service->issueAccessLinkForDirectBooking($booking);
@@ -96,7 +97,7 @@ class AccommodationBookingServiceTest extends TestCase
$service = $this->createServiceWithAccommodation(entityManager: $entityManager);
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Booking);
$booking->setType(AccommodationBookingType::Booking);
$issuedAt = new \DateTimeImmutable('2026-01-01');
$booking->setAccessLinkIssuedAt($issuedAt);
@@ -113,7 +114,7 @@ class AccommodationBookingServiceTest extends TestCase
$service = $this->createServiceWithAccommodation(entityManager: $entityManager);
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$service->issueAccessLink($booking);
@@ -128,7 +129,7 @@ class AccommodationBookingServiceTest extends TestCase
$service = $this->createServiceWithAccommodation(entityManager: $entityManager);
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$issuedAt = new \DateTimeImmutable('2026-01-01');
$booking->setAccessLinkIssuedAt($issuedAt);
@@ -141,7 +142,7 @@ class AccommodationBookingServiceTest extends TestCase
{
$booking = new AccommodationBooking();
$booking->setEmail('[email protected]');
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$booking->setAccessLinkIssuedAt(new \DateTimeImmutable());
$linkSigner = $this->createMock(AccommodationBookingLinkSigner::class);
@@ -168,7 +169,7 @@ class AccommodationBookingServiceTest extends TestCase
{
$booking = new AccommodationBooking();
$booking->setEmail('[email protected]');
$booking->setStatus(AccommodationBookingStatus::Booking);
$booking->setStatus(AccommodationBookingStatus::Accepted);
$booking->setAccessLinkIssuedAt(new \DateTimeImmutable());
$linkSigner = $this->createMock(AccommodationBookingLinkSigner::class);
@@ -197,7 +198,7 @@ class AccommodationBookingServiceTest extends TestCase
{
$booking = new AccommodationBooking();
$booking->setEmail('[email protected]');
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$breakdownCalculator = $this->createMock(AccommodationBookingBreakdownCalculator::class);
$breakdownCalculator->method('compute')->willReturn(null);
@@ -220,7 +221,7 @@ class AccommodationBookingServiceTest extends TestCase
{
$booking = new AccommodationBooking();
$booking->setEmail('[email protected]');
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$breakdownCalculator = $this->createMock(AccommodationBookingBreakdownCalculator::class);
$breakdownCalculator->method('compute')->willReturn(null);
@@ -240,7 +241,7 @@ class AccommodationBookingServiceTest extends TestCase
{
$booking = new AccommodationBooking();
$booking->setEmail('[email protected]');
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$previousIssuedAt = new \DateTimeImmutable('2026-01-01');
$booking->setAccessLinkIssuedAt($previousIssuedAt);
@@ -264,13 +265,13 @@ class AccommodationBookingServiceTest extends TestCase
self::assertNotSame($previousIssuedAt, $booking->getAccessLinkIssuedAt());
}
public function testAcceptBookingTransitionsInquiryToBookingAndSendsNotifications(): void
public function testAcceptBookingAcceptsOpenInquiryAndSendsNotifications(): void
{
$entityManager = $this->createMock(EntityManagerInterface::class);
$entityManager->expects(self::once())->method('flush');
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Open);
$booking->setEmail('[email protected]');
$mailer = $this->createMock(Mailer::class);
@@ -288,7 +289,8 @@ class AccommodationBookingServiceTest extends TestCase
$service->acceptBooking($booking);
self::assertSame(AccommodationBookingStatus::Booking, $booking->getStatus());
self::assertSame(AccommodationBookingStatus::Accepted, $booking->getStatus());
self::assertSame(AccommodationBookingType::Inquiry, $booking->getType(), 'an accepted offer stays an Anfrage');
self::assertNotNull($booking->getAcceptedAt());
}
@@ -303,17 +305,37 @@ class AccommodationBookingServiceTest extends TestCase
$service = $this->createServiceWithAccommodation(entityManager: $entityManager, mailer: $mailer);
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Booking);
$booking->setStatus(AccommodationBookingStatus::Accepted);
$service->acceptBooking($booking);
self::assertNull($booking->getAcceptedAt());
}
public function testAcceptBookingNoOpsForOfferThatIsNotOpenYet(): void
{
$entityManager = $this->createMock(EntityManagerInterface::class);
$entityManager->expects(self::never())->method('flush');
$mailer = $this->createMock(Mailer::class);
$mailer->expects(self::never())->method('createAndSendEmail');
$service = $this->createServiceWithAccommodation(entityManager: $entityManager, mailer: $mailer);
$booking = new AccommodationBooking();
$booking->setType(AccommodationBookingType::Inquiry);
$booking->setStatus(AccommodationBookingStatus::Draft);
$service->acceptBooking($booking);
self::assertSame(AccommodationBookingStatus::Draft, $booking->getStatus());
self::assertNull($booking->getAcceptedAt());
}
public function testSendOfferAcceptedNotificationEmailLogsAndSwallowsMailerFailures(): void
{
$booking = new AccommodationBooking();
$booking->setStatus(AccommodationBookingStatus::Booking);
$booking->setStatus(AccommodationBookingStatus::Accepted);
$mailer = $this->createMock(Mailer::class);
$mailer->method('createAndSendEmail')->willThrowException(new \RuntimeException('SMTP down'));
@@ -330,7 +352,7 @@ class AccommodationBookingServiceTest extends TestCase
{
$booking = new AccommodationBooking();
$booking->setEmail('[email protected]');
$booking->setStatus(AccommodationBookingStatus::Booking);
$booking->setStatus(AccommodationBookingStatus::Accepted);
$mailer = $this->createMock(Mailer::class);
$mailer->method('createAndSendEmail')->willThrowException(new \RuntimeException('SMTP down'));