diff --git a/assets/images/logo_alpinevacation.svg b/assets/images/logo_alpinevacation.svg
index 8d246bc..cad4fbe 100644
--- a/assets/images/logo_alpinevacation.svg
+++ b/assets/images/logo_alpinevacation.svg
@@ -1,33 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/assets/images/logo_cllt.svg b/assets/images/logo_cllt.svg
index 4af82bf..5ca7c51 100644
--- a/assets/images/logo_cllt.svg
+++ b/assets/images/logo_cllt.svg
@@ -1,33 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/migrations/Version20260806120000.php b/migrations/Version20260806120000.php
new file mode 100644
index 0000000..af55417
--- /dev/null
+++ b/migrations/Version20260806120000.php
@@ -0,0 +1,46 @@
+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');
+ }
+}
diff --git a/src/Controller/Admin/AccommodationBooking/EditController.php b/src/Controller/Admin/AccommodationBooking/EditController.php
index 25e8ed8..cdbd247 100644
--- a/src/Controller/Admin/AccommodationBooking/EditController.php
+++ b/src/Controller/Admin/AccommodationBooking/EditController.php
@@ -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);
diff --git a/src/Controller/Groups/OfferController.php b/src/Controller/Groups/OfferController.php
index b899aa3..abcd424 100644
--- a/src/Controller/Groups/OfferController.php
+++ b/src/Controller/Groups/OfferController.php
@@ -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);
}
diff --git a/src/Controller/Groups/Step4Controller.php b/src/Controller/Groups/Step4Controller.php
index 4c0164a..0c19801 100644
--- a/src/Controller/Groups/Step4Controller.php
+++ b/src/Controller/Groups/Step4Controller.php
@@ -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);
}
}
diff --git a/src/Entity/Groups/AccommodationBooking.php b/src/Entity/Groups/AccommodationBooking.php
index 39a4b2b..86c5965 100644
--- a/src/Entity/Groups/AccommodationBooking.php
+++ b/src/Entity/Groups/AccommodationBooking.php
@@ -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;
diff --git a/src/Enum/Groups/AccommodationBookingStatus.php b/src/Enum/Groups/AccommodationBookingStatus.php
index a54032c..21e0b93 100644
--- a/src/Enum/Groups/AccommodationBookingStatus.php
+++ b/src/Enum/Groups/AccommodationBookingStatus.php
@@ -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',
};
}
diff --git a/src/Enum/Groups/AccommodationBookingType.php b/src/Enum/Groups/AccommodationBookingType.php
new file mode 100644
index 0000000..3d0e82f
--- /dev/null
+++ b/src/Enum/Groups/AccommodationBookingType.php
@@ -0,0 +1,17 @@
+ 'Anfrage',
+ self::Booking => 'Buchung',
+ };
+ }
+}
diff --git a/src/Form/Admin/Groups/AccommodationBookingCreateType.php b/src/Form/Admin/Groups/AccommodationBookingCreateType.php
index bcf7199..43742bd 100644
--- a/src/Form/Admin/Groups/AccommodationBookingCreateType.php
+++ b/src/Form/Admin/Groups/AccommodationBookingCreateType.php
@@ -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(),
])
diff --git a/src/Model/AccommodationBookingApiResponse.php b/src/Model/AccommodationBookingApiResponse.php
index 765ce0b..960e255 100644
--- a/src/Model/AccommodationBookingApiResponse.php
+++ b/src/Model/AccommodationBookingApiResponse.php
@@ -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();
diff --git a/src/Service/AccommodationBookingService.php b/src/Service/AccommodationBookingService.php
index 65d0aa4..9c9dc98 100644
--- a/src/Service/AccommodationBookingService.php
+++ b/src/Service/AccommodationBookingService.php
@@ -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();
diff --git a/templates/admin/accommodation_booking/index.html.twig b/templates/admin/accommodation_booking/index.html.twig
index 93259c1..654c78b 100644
--- a/templates/admin/accommodation_booking/index.html.twig
+++ b/templates/admin/accommodation_booking/index.html.twig
@@ -60,7 +60,7 @@
{% endif %}
davon Kinder (4–{{ booking.accommodation.maxAdolescentAge }} Jahre)
{{ booking.childrenCount }}
{% endif %}
+
Art
+
{{ booking.type.label }}
Status
{{ booking.status.label }}
{% if booking.acceptedAt is not null %}
diff --git a/templates/email/accommodation_booking_customer.html.twig b/templates/email/accommodation_booking_customer.html.twig
index 775db76..fa99155 100644
--- a/templates/email/accommodation_booking_customer.html.twig
+++ b/templates/email/accommodation_booking_customer.html.twig
@@ -2,11 +2,11 @@
{% block body %}
{% if accessLink %}
-
{{ booking.inquiry ? 'Dein Angebot ist bereit' : 'Deine Buchung ist bestätigt' }}
+
{{ booking.accepted ? 'Deine Buchung ist bestätigt' : 'Dein Angebot ist bereit' }}
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 @@