feat: refactored accommodation booking status model

This commit is contained in:
Björn Fromme
2026-08-19 11:46:52 +02:00
parent e8accbb3ed
commit 5a3957e143
42 changed files with 1190 additions and 277 deletions
+37 -20
View File
@@ -9,8 +9,8 @@ use App\Entity\BlameableEntityInterface;
use App\Entity\TimestampableEntity;
use App\Entity\TimestampableEntityInterface;
use App\Entity\User;
use App\Enum\Groups\AccommodationBookingOrigin;
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;
@@ -81,12 +81,12 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
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.
* How this record came about — through an offer the customer had to accept, or as a
* booking the customer placed directly. Orthogonal to the status and never changed
* after creation, so it stays truthful at every point of the lifecycle.
*/
#[ORM\Column(length: 20, enumType: AccommodationBookingType::class)]
private AccommodationBookingType $type = AccommodationBookingType::Inquiry;
#[ORM\Column(length: 20, enumType: AccommodationBookingOrigin::class)]
private AccommodationBookingOrigin $origin = AccommodationBookingOrigin::Offer;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $accessLinkIssuedAt = null;
@@ -118,14 +118,15 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
private ?string $lastName = null;
/**
* The `offer` group covers the one field a customer-facing booking cannot do without:
* the access link and every notification are addressed to it. It is enforced on
* creation as soon as the booking is not a draft, while `edit` additionally demands
* the rest of the contact data.
* The one field a customer-facing booking cannot do without — the access link and every
* notification are addressed to it. A record on its way to the customer is validated with
* `edit`, which additionally demands the rest of the contact data; the actions that
* actually reach out (SendOfferController, ConfirmController) check the address again on
* their own, since a draft may legitimately be saved without one.
*/
#[ORM\Column(length: 255, nullable: true)]
#[Assert\NotBlank(groups: ['edit', 'offer'])]
#[Assert\Email(groups: ['edit', 'offer'])]
#[Assert\NotBlank(groups: ['edit'])]
#[Assert\Email(groups: ['edit'])]
private ?string $email = null;
#[ORM\Column(length: 50, nullable: true)]
@@ -376,6 +377,11 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
return AccommodationBookingStatus::Draft === $this->status;
}
public function isRequested(): bool
{
return AccommodationBookingStatus::Requested === $this->status;
}
public function isOpen(): bool
{
return AccommodationBookingStatus::Open === $this->status;
@@ -396,26 +402,37 @@ class AccommodationBooking implements BlameableEntityInterface, TimestampableEnt
return AccommodationBookingStatus::Discarded === $this->status;
}
public function getType(): AccommodationBookingType
public function getOrigin(): AccommodationBookingOrigin
{
return $this->type;
return $this->origin;
}
public function setType(AccommodationBookingType $type): self
public function setOrigin(AccommodationBookingOrigin $origin): self
{
$this->type = $type;
$this->origin = $origin;
return $this;
}
public function isInquiry(): bool
public function isFromOffer(): bool
{
return AccommodationBookingType::Inquiry === $this->type;
return AccommodationBookingOrigin::Offer === $this->origin;
}
public function isBooking(): bool
public function isDirectBooking(): bool
{
return AccommodationBookingType::Booking === $this->type;
return AccommodationBookingOrigin::Direct === $this->origin;
}
/**
* What to call this record wherever it is named next to its status. An offer becomes a
* booking the moment the customer commits, which is exactly what acceptedAt records — so
* this can never contradict the status the way a stored type could. The origin is a
* separate question ("where did this come from") and keeps its own label.
*/
public function recordLabel(): string
{
return null !== $this->acceptedAt ? 'Buchung' : 'Angebot';
}
public function getAccessLinkIssuedAt(): ?\DateTimeImmutable