feat: accommodation booking type and status as dedicated properties
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user