feat: split nights off an accommodation booking as separate periods

addresses #869f6z36v
This commit is contained in:
fromme committed 2026-09-30 18:06:20 +02:00
1 parent 972839286c
commit 7001bc0cf2
35 files changed
+2221 -25

No files matched your search

+57 -1
View File
@@ -8,13 +8,16 @@ use App\Email\EmailAttachmentInterface;
use App\Email\Mailer;
use App\Entity\Groups\Accommodation;
use App\Entity\Groups\AccommodationBooking;
use App\Entity\Groups\AccommodationBookingPeriod;
use App\Entity\Groups\AccommodationPrice;
use App\Entity\Groups\AdditionalService;
use App\Entity\Groups\BoardService;
use App\Enum\Groups\AccommodationBookingOrigin;
use App\Enum\Groups\AccommodationBookingPeriodPosition;
use App\Enum\Groups\AccommodationBookingStatus;
use App\Exception\ImplausibleBookingPriceException;
use App\Form\Model\AccommodationBookingDto;
use App\Form\Model\AccommodationBookingPeriodDto;
use App\Model\AccommodationBookingContext;
use App\Model\AccommodationBookingQueryParams;
use App\Model\CmsHotelData;
@@ -25,11 +28,16 @@ use App\Repository\Groups\AdditionalServiceRepository;
use App\Repository\Groups\BoardServiceRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class AccommodationBookingService
{
private const int PRICING_VERSION = 1;
/**
* 2: the breakdown carries the split-off `periods` and `periodsTotal`, and `total` includes
* them. Version 1 snapshots are read as bookings without periods.
*/
private const int PRICING_VERSION = 2;
public function __construct(
private readonly AccommodationRepository $accommodationRepo,
@@ -381,6 +389,54 @@ class AccommodationBookingService
);
}
/**
* The three period changes below each take effect at once, like the house change: they
* reprice the booking, and the office wants to see the new price before it goes on.
* On violations nothing is written and the violations are returned for the modal.
*
* @throws ImplausibleBookingPriceException if the new total is too large to be a real price
*/
public function splitOffPeriod(AccommodationBooking $booking, AccommodationBookingPeriodPosition $position, AccommodationBookingPeriodDto $dto): ConstraintViolationListInterface
{
$booking->splitOffPeriod($position, (int) $dto->nights)
->setLabel($dto->label)
->setLines($dto->linesSnapshot());
return $this->commitPeriodChange($booking);
}
/** @throws ImplausibleBookingPriceException if the new total is too large to be a real price */
public function updatePeriod(AccommodationBooking $booking, AccommodationBookingPeriod $period, AccommodationBookingPeriodDto $dto): ConstraintViolationListInterface
{
$booking->resizePeriod($period, (int) $dto->nights);
$period->setLabel($dto->label)->setLines($dto->linesSnapshot());
return $this->commitPeriodChange($booking);
}
/** @throws ImplausibleBookingPriceException if the new total is too large to be a real price */
public function removePeriod(AccommodationBooking $booking, AccommodationBookingPeriod $period): ConstraintViolationListInterface
{
$booking->removePeriod($period);
return $this->commitPeriodChange($booking);
}
private function commitPeriodChange(AccommodationBooking $booking): ConstraintViolationListInterface
{
// Default only: the `edit` group asks for contact data, which a period change neither
// shows nor touches, and which the edit form already enforces where it belongs.
$violations = $this->validator->validate($booking, null, ['Default']);
if (count($violations) > 0) {
return $violations;
}
$this->refreshPriceSnapshot($booking);
$this->entityManager->flush();
return $violations;
}
public function sendNotificationEmail(AccommodationBooking $booking): void
{
$this->sendBookingEmail(