feat: accommodation booking type and status as dedicated properties
This commit is contained in:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.4 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260806120000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Split the accommodation booking status into a status and a type';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE accommodation_booking ADD type VARCHAR(20) DEFAULT NULL');
|
||||
|
||||
// An old 'booking' row with an accepted_at came from an accepted offer, so it is an
|
||||
// Anfrage; without one it was placed directly. Rows in 'draft'/'discarded' carry no
|
||||
// trace of their kind any more — they are mapped to 'inquiry', which is where the
|
||||
// office path produces them.
|
||||
$this->addSql("UPDATE accommodation_booking SET type = CASE
|
||||
WHEN status = 'booking' AND accepted_at IS NULL THEN 'booking'
|
||||
ELSE 'inquiry' END");
|
||||
|
||||
$this->addSql("UPDATE accommodation_booking SET status = CASE
|
||||
WHEN status = 'inquiry' THEN 'open'
|
||||
WHEN status = 'booking' THEN 'accepted'
|
||||
ELSE status END");
|
||||
|
||||
$this->addSql('ALTER TABLE accommodation_booking CHANGE type type VARCHAR(20) NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql("UPDATE accommodation_booking SET status = CASE
|
||||
WHEN status IN ('draft', 'discarded') THEN status
|
||||
WHEN type = 'inquiry' AND status = 'open' THEN 'inquiry'
|
||||
ELSE 'booking' END");
|
||||
|
||||
$this->addSql('ALTER TABLE accommodation_booking DROP type');
|
||||
}
|
||||
}
|
||||
@@ -127,9 +127,9 @@ class EditController extends AbstractController
|
||||
$this->entityManager->persist($booking);
|
||||
$this->entityManager->flush();
|
||||
|
||||
// Entering Anfrage or Buchung is what the customer needs to hear about: the offer
|
||||
// Entering Offen or Bestätigt is what the customer needs to hear about: the offer
|
||||
// becomes viewable, or the booking is confirmed. Entwurf and Absage stay silent.
|
||||
$notifiableStatuses = [AccommodationBookingStatus::Inquiry, AccommodationBookingStatus::Booking];
|
||||
$notifiableStatuses = [AccommodationBookingStatus::Open, AccommodationBookingStatus::Accepted];
|
||||
if ($previousStatus !== $booking->getStatus() && in_array($booking->getStatus(), $notifiableStatuses, true)) {
|
||||
$this->bookingService->issueAccessLink($booking);
|
||||
$this->bookingService->sendCustomerConfirmationEmail($booking);
|
||||
|
||||
@@ -93,7 +93,7 @@ class OfferController extends AbstractController
|
||||
return $this->redirectToOfferPage($request, $uuid);
|
||||
}
|
||||
|
||||
if (!$booking->isInquiry()) {
|
||||
if (!$booking->isInquiry() || !$booking->isOpen()) {
|
||||
return $this->redirectToOfferPage($request, $uuid);
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class Step4Controller extends AbstractAccommodationController
|
||||
{
|
||||
$prices = $this->bookingService->loadPrices($dto, $accommodation);
|
||||
$booking = $this->bookingService->finalizeBooking($dto, $accommodation, $prices, $services);
|
||||
$this->addFlash('groups_booking_result', $booking->getStatus()->value);
|
||||
$this->addFlash('groups_booking_result', $booking->getType()->value);
|
||||
$this->sessionManager->clear($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Entity\TimestampableEntity;
|
||||
use App\Entity\TimestampableEntityInterface;
|
||||
use App\Entity\User;
|
||||
use App\Enum\Groups\AccommodationBookingStatus;
|
||||
use App\Enum\Groups\AccommodationBookingType;
|
||||
use App\Enum\Groups\AdditionalServiceType;
|
||||
use App\Repository\Groups\AccommodationBookingRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
@@ -79,6 +80,14 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
|
||||
#[ORM\Column(length: 20, enumType: AccommodationBookingStatus::class)]
|
||||
private AccommodationBookingStatus $status = AccommodationBookingStatus::Draft;
|
||||
|
||||
/**
|
||||
* What kind of record this is — an offer the customer still has to accept, or a
|
||||
* directly placed, binding booking. Independent of the status: an accepted offer
|
||||
* keeps its Inquiry type, which is how offer-originated bookings stay recognisable.
|
||||
*/
|
||||
#[ORM\Column(length: 20, enumType: AccommodationBookingType::class)]
|
||||
private AccommodationBookingType $type = AccommodationBookingType::Inquiry;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $accessLinkIssuedAt = null;
|
||||
|
||||
@@ -353,14 +362,14 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
|
||||
return AccommodationBookingStatus::Draft === $this->status;
|
||||
}
|
||||
|
||||
public function isInquiry(): bool
|
||||
public function isOpen(): bool
|
||||
{
|
||||
return AccommodationBookingStatus::Inquiry === $this->status;
|
||||
return AccommodationBookingStatus::Open === $this->status;
|
||||
}
|
||||
|
||||
public function isBooking(): bool
|
||||
public function isAccepted(): bool
|
||||
{
|
||||
return AccommodationBookingStatus::Booking === $this->status;
|
||||
return AccommodationBookingStatus::Accepted === $this->status;
|
||||
}
|
||||
|
||||
public function isDiscarded(): bool
|
||||
@@ -368,6 +377,28 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
|
||||
return AccommodationBookingStatus::Discarded === $this->status;
|
||||
}
|
||||
|
||||
public function getType(): AccommodationBookingType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setType(AccommodationBookingType $type): self
|
||||
{
|
||||
$this->type = $type;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isInquiry(): bool
|
||||
{
|
||||
return AccommodationBookingType::Inquiry === $this->type;
|
||||
}
|
||||
|
||||
public function isBooking(): bool
|
||||
{
|
||||
return AccommodationBookingType::Booking === $this->type;
|
||||
}
|
||||
|
||||
public function getAccessLinkIssuedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->accessLinkIssuedAt;
|
||||
|
||||
@@ -5,16 +5,16 @@ namespace App\Enum\Groups;
|
||||
enum AccommodationBookingStatus: string
|
||||
{
|
||||
case Draft = 'draft';
|
||||
case Inquiry = 'inquiry';
|
||||
case Booking = 'booking';
|
||||
case Open = 'open';
|
||||
case Accepted = 'accepted';
|
||||
case Discarded = 'discarded';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Draft => 'Entwurf',
|
||||
self::Inquiry => 'Anfrage',
|
||||
self::Booking => 'Buchung',
|
||||
self::Open => 'Offen',
|
||||
self::Accepted => 'Bestätigt',
|
||||
self::Discarded => 'Absage',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum\Groups;
|
||||
|
||||
enum AccommodationBookingType: string
|
||||
{
|
||||
case Inquiry = 'inquiry';
|
||||
case Booking = 'booking';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Inquiry => 'Anfrage',
|
||||
self::Booking => 'Buchung',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,8 @@ class AccommodationBookingCreateType extends AbstractType
|
||||
'class' => AccommodationBookingStatus::class,
|
||||
'choices' => [
|
||||
AccommodationBookingStatus::Draft,
|
||||
AccommodationBookingStatus::Inquiry,
|
||||
AccommodationBookingStatus::Booking,
|
||||
AccommodationBookingStatus::Open,
|
||||
AccommodationBookingStatus::Accepted,
|
||||
],
|
||||
'choice_label' => fn (AccommodationBookingStatus $status) => $status->label(),
|
||||
])
|
||||
|
||||
@@ -17,6 +17,9 @@ final readonly class AccommodationBookingApiResponse
|
||||
#[Groups(['api:single'])]
|
||||
public string $status;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public string $type;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
public ?\DateTimeImmutable $dateFrom;
|
||||
@@ -89,6 +92,7 @@ final readonly class AccommodationBookingApiResponse
|
||||
{
|
||||
$this->uuid = $booking->getUuid();
|
||||
$this->status = $booking->getStatus()->value;
|
||||
$this->type = $booking->getType()->value;
|
||||
$this->dateFrom = $booking->getDateFrom();
|
||||
$this->dateTo = $booking->getDateTo();
|
||||
$this->nights = $booking->getNights();
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Entity\Groups\AccommodationPrice;
|
||||
use App\Entity\Groups\AdditionalService;
|
||||
use App\Entity\Groups\BoardService;
|
||||
use App\Enum\Groups\AccommodationBookingStatus;
|
||||
use App\Enum\Groups\AccommodationBookingType;
|
||||
use App\Form\Model\AccommodationBookingDto;
|
||||
use App\Model\AccommodationBookingQueryParams;
|
||||
use App\Model\CmsHotelData;
|
||||
@@ -265,11 +266,11 @@ class AccommodationBookingService
|
||||
$booking->setPaxCount($dto->paxCount);
|
||||
$booking->setMinorsCount($dto->minorsCount);
|
||||
$booking->setChildrenCount($dto->childrenCount);
|
||||
$booking->setStatus(
|
||||
$dto->forceInquiry || $this->computeInquiryStatus($dto, $prices)->isInquiry
|
||||
? AccommodationBookingStatus::Inquiry
|
||||
: AccommodationBookingStatus::Booking
|
||||
);
|
||||
// An inquiry arrives live — the office has nothing to prepare first, so it goes
|
||||
// straight to Offen. A direct booking is binding on arrival, hence Bestätigt.
|
||||
$isInquiry = $dto->forceInquiry || $this->computeInquiryStatus($dto, $prices)->isInquiry;
|
||||
$booking->setType($isInquiry ? AccommodationBookingType::Inquiry : AccommodationBookingType::Booking);
|
||||
$booking->setStatus($isInquiry ? AccommodationBookingStatus::Open : AccommodationBookingStatus::Accepted);
|
||||
|
||||
// Freeze board service as scalar fields (no FK)
|
||||
if (null !== $dto->selectedBoardServiceId) {
|
||||
@@ -432,9 +433,9 @@ class AccommodationBookingService
|
||||
}
|
||||
|
||||
/**
|
||||
* Issues the access link only for a confirmed booking. Inquiries from the public flow
|
||||
* Issues the access link only for a direct booking. Inquiries from the public flow
|
||||
* deliberately get no link — the office prepares the offer first and issues the link
|
||||
* when it switches the booking to Anfrage.
|
||||
* when it publishes the offer.
|
||||
*/
|
||||
public function issueAccessLinkForDirectBooking(AccommodationBooking $booking): void
|
||||
{
|
||||
@@ -452,7 +453,7 @@ class AccommodationBookingService
|
||||
*/
|
||||
public function sendCustomerConfirmationEmail(AccommodationBooking $booking): void
|
||||
{
|
||||
if (!$booking->isInquiry()) {
|
||||
if ($booking->isAccepted()) {
|
||||
$subject = 'Deine Buchung ist bestätigt';
|
||||
} elseif (null !== $booking->getAccessLinkIssuedAt()) {
|
||||
$subject = 'Dein Angebot ist bereit';
|
||||
@@ -481,16 +482,18 @@ class AccommodationBookingService
|
||||
}
|
||||
|
||||
/**
|
||||
* Transitions a booking from inquiry to booking and notifies office and customer.
|
||||
* Idempotent — a no-op (including no emails) if the booking is already accepted.
|
||||
* Accepts an open offer and notifies office and customer. The type stays Anfrage —
|
||||
* only the status moves to Bestätigt, so an offer-originated booking remains
|
||||
* distinguishable from a direct one.
|
||||
* Idempotent — a no-op (including no emails) if the offer is not open any more.
|
||||
*/
|
||||
public function acceptBooking(AccommodationBooking $booking): void
|
||||
{
|
||||
if (!$booking->isInquiry()) {
|
||||
if (!$booking->isInquiry() || !$booking->isOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$booking->setStatus(AccommodationBookingStatus::Booking);
|
||||
$booking->setStatus(AccommodationBookingStatus::Accepted);
|
||||
$booking->setAcceptedAt(new \DateTimeImmutable());
|
||||
$this->entityManager->flush();
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ booking.status.label }}
|
||||
{{ booking.type.label }} · {{ booking.status.label }}
|
||||
</td>
|
||||
<td>
|
||||
{% set discounts = [] %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{% block content %}
|
||||
<twig:page:heading>
|
||||
{{ booking.status.label }}
|
||||
{{ booking.type.label }} · {{ booking.status.label }}
|
||||
{{ booking.groupName }}
|
||||
</twig:page:heading>
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
<dt class="font-medium">davon Kinder (4–{{ booking.accommodation.maxAdolescentAge }} Jahre)</dt>
|
||||
<dd>{{ booking.childrenCount }}</dd>
|
||||
{% endif %}
|
||||
<dt class="font-medium">Art</dt>
|
||||
<dd>{{ booking.type.label }}</dd>
|
||||
<dt class="font-medium">Status</dt>
|
||||
<dd>{{ booking.status.label }}</dd>
|
||||
{% if booking.acceptedAt is not null %}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
{% block body %}
|
||||
{% if accessLink %}
|
||||
<h1>{{ booking.inquiry ? 'Dein Angebot ist bereit' : 'Deine Buchung ist bestätigt' }}</h1>
|
||||
<h1>{{ booking.accepted ? 'Deine Buchung ist bestätigt' : 'Dein Angebot ist bereit' }}</h1>
|
||||
|
||||
<p>
|
||||
Hallo {{ booking.firstName }},
|
||||
{% if booking.inquiry %}
|
||||
{% if not booking.accepted %}
|
||||
vielen Dank für deine Anfrage! Wir haben sie geprüft — dein Angebot wartet auf dich.
|
||||
{% else %}
|
||||
vielen Dank! Deine Buchung ist bei uns eingegangen und bestätigt.
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<p>
|
||||
<a href="{{ accessLink }}" class="button">
|
||||
{{ booking.inquiry ? 'Angebot ansehen & bestätigen' : 'Buchung ansehen' }}
|
||||
{{ booking.accepted ? 'Buchung ansehen' : 'Angebot ansehen & bestätigen' }}
|
||||
</a>
|
||||
</p>
|
||||
{% else %}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
{% import _self as macros %}
|
||||
|
||||
{% block title %}{{ booking.inquiry ? 'Dein Angebot' : 'Deine Buchung' }}{% endblock %}
|
||||
{% block title %}{{ booking.accepted ? 'Deine Buchung' : 'Dein Angebot' }}{% endblock %}
|
||||
|
||||
{% block background %}bg-outer bg-outer--summer{% endblock %}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<hgroup>
|
||||
<h1 class="text-2xl lg:text-4xl font-bold mb-2">
|
||||
{{ booking.inquiry ? 'Dein Angebot' : 'Deine Buchung' }}
|
||||
{{ booking.accepted ? 'Deine Buchung' : 'Dein Angebot' }}
|
||||
</h1>
|
||||
<p class="text-2xl">
|
||||
{{ ctx.accommodation.name }}
|
||||
@@ -63,7 +63,7 @@
|
||||
{% if booking.boardServiceLabel or booking.additionalServices | length > 0 %}
|
||||
<div>
|
||||
<h2 class="text-xl font-bold mb-2">
|
||||
{{ booking.inquiry ? 'Angefragte' : 'Gebuchte' }} Leistungen
|
||||
{{ booking.accepted ? 'Gebuchte' : 'Angefragte' }} Leistungen
|
||||
</h2>
|
||||
<ul class="list-disc pl-4">
|
||||
{% if booking.boardServiceLabel %}
|
||||
@@ -76,7 +76,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if booking.inquiry %}
|
||||
{% if booking.inquiry and booking.open %}
|
||||
<div>
|
||||
<button type="button"
|
||||
class="button button--primary w-full"
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user