feat: split summary DTOs into typed sub-objects

This commit is contained in:
Björn Fromme
2026-04-12 11:23:19 +02:00
parent 69705052b3
commit aa997826f8
18 changed files with 224 additions and 113 deletions
@@ -136,7 +136,7 @@ class Step4Controller extends AbstractController
// Success: Store booking data in flash for conversion tracking // Success: Store booking data in flash for conversion tracking
$bookingCreateContext = $this->createContextFactory->createWithParticipantPrices($bookingCreateDto); $bookingCreateContext = $this->createContextFactory->createWithParticipantPrices($bookingCreateDto);
$this->addFlash('booking_number', $bookingResponse->bookingNumber); $this->addFlash('booking_number', $bookingResponse->bookingNumber);
$this->addFlash('booking_total', $bookingCreateContext->summaryData->payableAmount); $this->addFlash('booking_total', $bookingCreateContext->summaryData->vouchers->payableAmount);
$this->addFlash('booking_travel_name', $bookingCreateDto->travel->label); $this->addFlash('booking_travel_name', $bookingCreateDto->travel->label);
$this->clearTravelDataCache($bookingCreateDto); $this->clearTravelDataCache($bookingCreateDto);
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_CREATE); $this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_CREATE);
+3 -6
View File
@@ -4,22 +4,19 @@ declare(strict_types=1);
namespace App\Form\Model; namespace App\Form\Model;
use App\BusProNet\Model\Room;
/** /**
* Bundles the data needed to render the booking create flow. * Bundles the data needed to render the booking create flow.
*/ */
class BookingCreateContext class BookingCreateContext
{ {
/** /**
* @param array{by_pax: array<int, Room>, by_room: array<int, Room>} $groupedRooms * @param array<int, ParticipantCardDataDto>|null $cardsData
* @param array<int, ParticipantCardDataDto>|null $cardsData * @param array<int, float>|null $participantPrices
* @param array<int, float>|null $participantPrices
*/ */
public function __construct( public function __construct(
public readonly BookingDto $bookingDto, public readonly BookingDto $bookingDto,
public readonly BookingSummaryDto $summaryData, public readonly BookingSummaryDto $summaryData,
public readonly array $groupedRooms, public readonly RoomGroupsDto $groupedRooms,
public readonly ?array $cardsData = null, public readonly ?array $cardsData = null,
public readonly bool $isSubmitted = false, public readonly bool $isSubmitted = false,
public readonly ?array $participantPrices = null, public readonly ?array $participantPrices = null,
+1 -2
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Form\Model; namespace App\Form\Model;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Booking; use App\BusProNet\Model\Booking;
/** /**
@@ -18,7 +17,7 @@ class BookingEditContext
public function __construct( public function __construct(
public readonly BookingDto $bookingDto, public readonly BookingDto $bookingDto,
public readonly ?Booking $bookingData, public readonly ?Booking $bookingData,
public readonly ?BaseData $mutableData, public readonly ?BookingMutabilityDto $mutableData,
public readonly BookingSummaryDto $summaryData, public readonly BookingSummaryDto $summaryData,
public readonly ?array $cardsData = null, public readonly ?array $cardsData = null,
public readonly bool $isDirty = false, public readonly bool $isDirty = false,
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Form\Model;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\MutableData;
/**
* Typed wrapper for edit-mode mutability data.
*/
final class BookingMutabilityDto
{
public function __construct(
public readonly ?MutableData $participantCount,
public readonly ?MutableData $participantData,
public readonly ?MutableData $transportation,
public readonly ?MutableData $pickup,
public readonly ?MutableData $accommodation,
public readonly ?MutableData $additionalServices,
) {
}
public static function fromBaseData(?BaseData $baseData): ?self
{
if (null === $baseData) {
return null;
}
return new self(
participantCount: self::getItem($baseData, MutableData::CATEGORY_PARTICIPANT_COUNT),
participantData: self::getItem($baseData, MutableData::CATEGORY_PARTICIPANT_DATA),
transportation: self::getItem($baseData, MutableData::CATEGORY_TRANSPORTATION),
pickup: self::getItem($baseData, MutableData::CATEGORY_PICKUP),
accommodation: self::getItem($baseData, MutableData::CATEGORY_ACCOMMODATION),
additionalServices: self::getItem($baseData, MutableData::CATEGORY_ADDITIONAL_SERVICES),
);
}
private static function getItem(BaseData $baseData, string $key): ?MutableData
{
$item = $baseData->getItemByKey($key);
return $item instanceof MutableData ? $item : null;
}
}
+3 -16
View File
@@ -6,30 +6,17 @@ namespace App\Form\Model;
/** /**
* DTO containing all booking summary data for sidebar display. * DTO containing all booking summary data for sidebar display.
*
* Provides pricing breakdowns, room assignments, participant counts,
* and CMS product information in a single typed object.
*/ */
class BookingSummaryDto class BookingSummaryDto
{ {
/** /**
* @param array<int, RoomSelectionDto> $selectedRooms Selected room DTOs from booking * @param array<int, RoomSelectionDto> $selectedRooms Selected room DTOs from booking
* @param int $participantCount Total participant count from room capacity
* @param float $totalPrice Total price before voucher deductions
* @param float $payableAmount Amount after voucher deductions
* @param array<array{room: mixed, count: int}> $groupedSelectedRooms Rooms grouped by participant assignments
* @param array<int, int> $assignmentCounts Room ID to participant count mapping
* @param array $pricingData Detailed pricing breakdown
* @param array|null $cmsData CMS product data (images, etc.)
*/ */
public function __construct( public function __construct(
public readonly array $selectedRooms, public readonly array $selectedRooms,
public readonly int $participantCount, public readonly int $participantCount,
public readonly float $totalPrice, public readonly BookingSummaryPricingDto $pricing,
public readonly float $payableAmount, public readonly BookingSummaryVoucherDto $vouchers,
public readonly array $groupedSelectedRooms,
public readonly array $assignmentCounts,
public readonly array $pricingData,
public readonly ?array $cmsData, public readonly ?array $cmsData,
) { ) {
} }
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Form\Model;
/**
* Typed pricing section for the booking summary.
*/
final class BookingSummaryPricingDto
{
/**
* @param array<int, array<string, mixed>> $rooms
* @param array<int, array<string, mixed>> $services
* @param array<int, array<string, mixed>>|null $surcharges
* @param array<int, int> $assignmentCounts
*/
public function __construct(
public readonly array $rooms,
public readonly array $services,
public readonly ?array $surcharges,
public readonly array $assignmentCounts,
public readonly float $grandTotal,
) {
}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Form\Model;
/**
* Typed voucher section for the booking summary.
*/
final class BookingSummaryVoucherDto
{
public function __construct(
public readonly ?AcceptedVouchersDto $acceptedVouchers,
public readonly float $payableAmount,
) {
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Form\Model;
use App\BusProNet\Model\Room;
/**
* Typed room grouping used by the booking create flow.
*/
final class RoomGroupsDto
{
/**
* @param array<int, Room> $byPax
* @param array<int, Room> $byRoom
*/
public function __construct(
public readonly array $byPax,
public readonly array $byRoom,
) {
}
}
+4 -4
View File
@@ -6,6 +6,8 @@ namespace App\Service;
use App\Form\Model\BookingCreateContext; use App\Form\Model\BookingCreateContext;
use App\Form\Model\BookingDto; use App\Form\Model\BookingDto;
use App\Form\Model\BookingSummaryDto;
use App\Form\Model\RoomGroupsDto;
/** /**
* Prepares the shared view model for booking create. * Prepares the shared view model for booking create.
@@ -60,15 +62,13 @@ class BookingCreateContextFactory
} }
/** /**
* @return array{summaryData: \App\Form\Model\BookingSummaryDto, groupedRooms: array{by_pax: array<int, \App\BusProNet\Model\Room>, by_room: array<int, \App\BusProNet\Model\Room>}} * @return array{summaryData: BookingSummaryDto, groupedRooms: RoomGroupsDto}
*/ */
private function buildBaseContext(BookingDto $bookingDto, string $pricingMode): array private function buildBaseContext(BookingDto $bookingDto, string $pricingMode): array
{ {
return [ return [
'summaryData' => $this->summaryDataService->getSummaryData($bookingDto, $pricingMode), 'summaryData' => $this->summaryDataService->getSummaryData($bookingDto, $pricingMode),
'groupedRooms' => $this->roomSelectionService->groupRoomsBySelectionType( 'groupedRooms' => $this->roomSelectionService->groupRoomsBySelectionType($bookingDto->travel->getAvailableRooms()),
$bookingDto->travel->getAvailableRooms()
),
]; ];
} }
} }
+5 -2
View File
@@ -8,6 +8,7 @@ use App\BusProNet\Model\Booking;
use App\Form\Model\BookingDto; use App\Form\Model\BookingDto;
use App\Form\Model\BookingEditContext; use App\Form\Model\BookingEditContext;
use App\Form\Model\BookingSummaryDto; use App\Form\Model\BookingSummaryDto;
use App\Form\Model\BookingMutabilityDto;
/** /**
* Prepares the shared booking edit flow context. * Prepares the shared booking edit flow context.
@@ -37,7 +38,9 @@ class BookingEditContextFactory
return new BookingEditContext( return new BookingEditContext(
bookingDto: $bookingDto, bookingDto: $bookingDto,
bookingData: $bookingData, bookingData: $bookingData,
mutableData: $bookingData ? $this->travelDataService->getMutabilityData($bookingData->dateId) : null, mutableData: BookingMutabilityDto::fromBaseData(
$bookingData ? $this->travelDataService->getMutabilityData($bookingData->dateId) : null
),
summaryData: $this->createSummaryData($bookingDto), summaryData: $this->createSummaryData($bookingDto),
); );
} }
@@ -52,7 +55,7 @@ class BookingEditContextFactory
return new BookingEditContext( return new BookingEditContext(
bookingDto: $bookingDto, bookingDto: $bookingDto,
bookingData: $bookingData, bookingData: $bookingData,
mutableData: $this->travelDataService->getMutabilityData($bookingData->dateId), mutableData: BookingMutabilityDto::fromBaseData($this->travelDataService->getMutabilityData($bookingData->dateId)),
summaryData: $this->createSummaryData($bookingDto), summaryData: $this->createSummaryData($bookingDto),
cardsData: $this->participantCardDataService->getAllCardsDataWithValidation($bookingDto), cardsData: $this->participantCardDataService->getAllCardsDataWithValidation($bookingDto),
isDirty: $isDirty, isDirty: $isDirty,
+6 -4
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Service; namespace App\Service;
use App\BusProNet\Model\Room; use App\BusProNet\Model\Room;
use App\Form\Model\RoomGroupsDto;
/** /**
* Groups room data for booking steps. * Groups room data for booking steps.
@@ -13,10 +14,8 @@ class BookingRoomSelectionService
{ {
/** /**
* @param array<int, Room> $rooms * @param array<int, Room> $rooms
*
* @return array{by_pax: array<int, Room>, by_room: array<int, Room>}
*/ */
public function groupRoomsBySelectionType(array $rooms): array public function groupRoomsBySelectionType(array $rooms): RoomGroupsDto
{ {
$groups = [ $groups = [
Room::SELECTION_TYPE_BY_PAX => [], Room::SELECTION_TYPE_BY_PAX => [],
@@ -35,6 +34,9 @@ class BookingRoomSelectionService
uasort($groups[Room::SELECTION_TYPE_BY_PAX], static fn (Room $a, Room $b): int => $a->maxPax <=> $b->maxPax); uasort($groups[Room::SELECTION_TYPE_BY_PAX], static fn (Room $a, Room $b): int => $a->maxPax <=> $b->maxPax);
uasort($groups[Room::SELECTION_TYPE_BY_ROOM], static fn (Room $a, Room $b): int => $a->maxPax <=> $b->maxPax); uasort($groups[Room::SELECTION_TYPE_BY_ROOM], static fn (Room $a, Room $b): int => $a->maxPax <=> $b->maxPax);
return $groups; return new RoomGroupsDto(
byPax: $groups[Room::SELECTION_TYPE_BY_PAX],
byRoom: $groups[Room::SELECTION_TYPE_BY_ROOM],
);
} }
} }
+23 -22
View File
@@ -7,8 +7,11 @@ namespace App\Service;
use App\BusProNet\DataProvider\CountryDataProvider; use App\BusProNet\DataProvider\CountryDataProvider;
use App\BusProNet\Model\Hotel; use App\BusProNet\Model\Hotel;
use App\BusProNet\XmlLoader\HotelLoader; use App\BusProNet\XmlLoader\HotelLoader;
use App\Form\Model\AcceptedVouchersDto;
use App\Form\Model\BookingDto; use App\Form\Model\BookingDto;
use App\Form\Model\BookingSummaryDto; use App\Form\Model\BookingSummaryDto;
use App\Form\Model\BookingSummaryPricingDto;
use App\Form\Model\BookingSummaryVoucherDto;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
@@ -53,18 +56,6 @@ class BookingSummaryDataService
} }
} }
// Group selected rooms with counts (for display)
$groupedSelectedRooms = [];
foreach ($roomCounts as $roomId => $count) {
$room = $bookingDto->travel->getRoomById($roomId);
if (null !== $room) {
$groupedSelectedRooms[] = [
'room' => $room,
'count' => $count,
];
}
}
// Get detailed pricing breakdown // Get detailed pricing breakdown
$pricingData = $this->priceCalculator->getPricingBreakdown($bookingDto, $roomPricingMode); $pricingData = $this->priceCalculator->getPricingBreakdown($bookingDto, $roomPricingMode);
@@ -74,17 +65,29 @@ class BookingSummaryDataService
// Calculate participant count from room capacity (source of truth) // Calculate participant count from room capacity (source of truth)
$participantCount = $this->calculateParticipantCountFromRooms($bookingDto); $participantCount = $this->calculateParticipantCountFromRooms($bookingDto);
$acceptedVouchers = $bookingDto->getAcceptedVouchers($participantPrices);
// Calculate payable amount after voucher deductions // Calculate payable amount after voucher deductions
$payableAmount = $this->calculatePayableAmount($bookingDto, $pricingData['grandTotal'], $participantPrices); $payableAmount = $this->calculatePayableAmount($pricingData['grandTotal'], $acceptedVouchers);
$pricing = new BookingSummaryPricingDto(
rooms: $pricingData['rooms'],
services: $pricingData['services'],
surcharges: $pricingData['surcharges'] ?? null,
assignmentCounts: $roomCounts,
grandTotal: $pricingData['grandTotal'],
);
$vouchers = new BookingSummaryVoucherDto(
acceptedVouchers: $acceptedVouchers,
payableAmount: $payableAmount,
);
return new BookingSummaryDto( return new BookingSummaryDto(
selectedRooms: $selectedRooms, selectedRooms: $selectedRooms,
participantCount: $participantCount, participantCount: $participantCount,
totalPrice: $pricingData['grandTotal'], pricing: $pricing,
payableAmount: $payableAmount, vouchers: $vouchers,
groupedSelectedRooms: $groupedSelectedRooms,
assignmentCounts: $roomCounts,
pricingData: $pricingData,
cmsData: $cmsData, cmsData: $cmsData,
); );
} }
@@ -92,12 +95,10 @@ class BookingSummaryDataService
/** /**
* Calculates the payable amount after voucher deductions. * Calculates the payable amount after voucher deductions.
* *
* @param array<int, float> $participantPrices Prices per participant for percentage voucher calculation * @param AcceptedVouchersDto|null $acceptedVouchers Accepted vouchers for discount calculation
*/ */
private function calculatePayableAmount(BookingDto $bookingDto, float $grandTotal, array $participantPrices): float private function calculatePayableAmount(float $grandTotal, ?AcceptedVouchersDto $acceptedVouchers): float
{ {
$acceptedVouchers = $bookingDto->getAcceptedVouchers($participantPrices);
if (null === $acceptedVouchers) { if (null === $acceptedVouchers) {
return $grandTotal; return $grandTotal;
} }
+20 -20
View File
@@ -3,7 +3,7 @@
<div class="shrink-0 px-4 lg:px-8 py-4 lg:flex lg:flex-col lg:justify-center shadow-md relative z-10"> <div class="shrink-0 px-4 lg:px-8 py-4 lg:flex lg:flex-col lg:justify-center shadow-md relative z-10">
<div class="flex items-center justify-between pb-2"> <div class="flex items-center justify-between pb-2">
<span class="block uppercase font-semibold">Gesamtpreis</span> <span class="block uppercase font-semibold">Gesamtpreis</span>
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span> <span class="font-semibold">{{ summaryData.vouchers.payableAmount|format_currency('EUR') }}</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="block uppercase font-semibold">Buchungsübersicht</span> <span class="block uppercase font-semibold">Buchungsübersicht</span>
@@ -83,8 +83,8 @@
Zusatzleistungen Zusatzleistungen
</td> </td>
<td class="p-2 align-top w-24 text-right whitespace-nowrap"> <td class="p-2 align-top w-24 text-right whitespace-nowrap">
{% if mutableData.items['additional_services'].mutable %} {% if mutableData.additionalServices and mutableData.additionalServices.mutable %}
{{ mutableData.items['additional_services'].mutableBefore|date('d.m.Y') }} {{ mutableData.additionalServices.mutableBefore|date('d.m.Y') }}
{% else %} {% else %}
nicht mehr möglich nicht mehr möglich
{% endif %} {% endif %}
@@ -95,8 +95,8 @@
Beförderung Beförderung
</td> </td>
<td class="p-2 align-top w-24 text-right whitespace-nowrap"> <td class="p-2 align-top w-24 text-right whitespace-nowrap">
{% if mutableData.items['transportation'].mutable %} {% if mutableData.transportation and mutableData.transportation.mutable %}
{{ mutableData.items['transportation'].mutableBefore|date('d.m.Y') }} {{ mutableData.transportation.mutableBefore|date('d.m.Y') }}
{% else %} {% else %}
nicht mehr möglich nicht mehr möglich
{% endif %} {% endif %}
@@ -107,8 +107,8 @@
Zustiege Zustiege
</td> </td>
<td class="p-2 align-top w-24 text-right whitespace-nowrap"> <td class="p-2 align-top w-24 text-right whitespace-nowrap">
{% if mutableData.items['pickup'].mutable %} {% if mutableData.pickup and mutableData.pickup.mutable %}
{{ mutableData.items['pickup'].mutableBefore|date('d.m.Y') }} {{ mutableData.pickup.mutableBefore|date('d.m.Y') }}
{% else %} {% else %}
nicht mehr möglich nicht mehr möglich
{% endif %} {% endif %}
@@ -119,8 +119,8 @@
Unterkunft Unterkunft
</td> </td>
<td class="p-2 align-top w-24 text-right whitespace-nowrap"> <td class="p-2 align-top w-24 text-right whitespace-nowrap">
{% if mutableData.items['accommodation'].mutable %} {% if mutableData.accommodation and mutableData.accommodation.mutable %}
{{ mutableData.items['accommodation'].mutableBefore|date('d.m.Y') }} {{ mutableData.accommodation.mutableBefore|date('d.m.Y') }}
{% else %} {% else %}
nicht mehr möglich nicht mehr möglich
{% endif %} {% endif %}
@@ -131,13 +131,13 @@
{% endif %} {% endif %}
{# Rooms Section #} {# Rooms Section #}
{% if summaryData.pricingData.rooms is not empty %} {% if summaryData.pricing.rooms is not empty %}
<div class="border border-primary-bg mb-4"> <div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold"> <div class="p-2 bg-primary-bg uppercase font-semibold">
Unterkunft Unterkunft
</div> </div>
<table class="w-full table-fixed"> <table class="w-full table-fixed">
{% for roomPricing in summaryData.pricingData.rooms %} {% for roomPricing in summaryData.pricing.rooms %}
<tr> <tr>
<td class="p-2 align-top"> <td class="p-2 align-top">
{{ roomPricing.quantity }}x {{ roomPricing.label }} {{ roomPricing.quantity }}x {{ roomPricing.label }}
@@ -152,12 +152,12 @@
{% endif %} {% endif %}
{# Services Section #} {# Services Section #}
{% if summaryData.pricingData.services is not empty %} {% if summaryData.pricing.services is not empty %}
<div class="border border-primary-bg mb-4"> <div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold"> <div class="p-2 bg-primary-bg uppercase font-semibold">
Leistungen Leistungen
</div> </div>
{% for serviceGroup in summaryData.pricingData.services %} {% for serviceGroup in summaryData.pricing.services %}
<div class="p-2 text-primary-dark/70 uppercase font-semibold"> <div class="p-2 text-primary-dark/70 uppercase font-semibold">
{{ serviceGroup.groupName }} {{ serviceGroup.groupName }}
</div> </div>
@@ -178,13 +178,13 @@
{% endif %} {% endif %}
{# Surcharges Section (Edit mode only) #} {# Surcharges Section (Edit mode only) #}
{% if summaryData.pricingData.surcharges is defined and summaryData.pricingData.surcharges is not empty %} {% if summaryData.pricing.surcharges is not empty %}
<div class="border border-primary-bg mb-4"> <div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold"> <div class="p-2 bg-primary-bg uppercase font-semibold">
Zu-/Abschläge Zu-/Abschläge
</div> </div>
<table class="w-full table-fixed"> <table class="w-full table-fixed">
{% for surchargePricing in summaryData.pricingData.surcharges %} {% for surchargePricing in summaryData.pricing.surcharges %}
<tr> <tr>
<td class="px-2 pb-2 align-top"> <td class="px-2 pb-2 align-top">
{{ surchargePricing.participantCount }}x {{ surchargePricing.label }} {{ surchargePricing.participantCount }}x {{ surchargePricing.label }}
@@ -199,9 +199,9 @@
{% endif %} {% endif %}
{# Total Section #} {# Total Section #}
{% if summaryData.totalPrice > 0 %} {% if summaryData.pricing.grandTotal > 0 %}
{# Show subtotal and voucher discounts when vouchers are applied #} {# Show subtotal and voucher discounts when vouchers are applied #}
{% set acceptedVouchers = bookingDto.getAcceptedVouchers() %} {% set acceptedVouchers = summaryData.vouchers.acceptedVouchers %}
{% if acceptedVouchers and acceptedVouchers.hasVouchers() %} {% if acceptedVouchers and acceptedVouchers.hasVouchers() %}
<div class="border border-primary-bg mb-4"> <div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold"> <div class="p-2 bg-primary-bg uppercase font-semibold">
@@ -214,7 +214,7 @@
Gesamtpreis Gesamtpreis
</td> </td>
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap"> <td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
{{ summaryData.totalPrice|format_currency('EUR') }} {{ summaryData.pricing.grandTotal|format_currency('EUR') }}
</td> </td>
</tr> </tr>
{# Voucher discounts breakdown #} {# Voucher discounts breakdown #}
@@ -239,12 +239,12 @@
{# Amount to pay after vouchers #} {# Amount to pay after vouchers #}
<div class="flex items-center justify-between px-2 pb-2"> <div class="flex items-center justify-between px-2 pb-2">
<span class="block uppercase font-semibold">Zu zahlen</span> <span class="block uppercase font-semibold">Zu zahlen</span>
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span> <span class="font-semibold">{{ summaryData.vouchers.payableAmount|format_currency('EUR') }}</span>
</div> </div>
{% else %} {% else %}
<div class="flex items-center justify-between pb-2"> <div class="flex items-center justify-between pb-2">
<span class="block uppercase font-semibold">Gesamtpreis</span> <span class="block uppercase font-semibold">Gesamtpreis</span>
<span class="font-semibold">{{ summaryData.payableAmount|format_currency('EUR') }}</span> <span class="font-semibold">{{ summaryData.vouchers.payableAmount|format_currency('EUR') }}</span>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
+4 -4
View File
@@ -65,19 +65,19 @@
{% block room_selection_form %} {% block room_selection_form %}
<div id="room-selection-form"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}> <div id="room-selection-form"{% if htmx_oob_swap is defined and htmx_oob_swap %} hx-swap-oob="true"{% endif %}>
{{ form_errors(form) }} {{ form_errors(form) }}
{% if bookingCreateContext.groupedRooms.by_room is not empty %} {% if bookingCreateContext.groupedRooms.byRoom is not empty %}
<h3> <h3>
Zimmer Zimmer
</h3> </h3>
{% for roomId, room in bookingCreateContext.groupedRooms.by_room %} {% for roomId, room in bookingCreateContext.groupedRooms.byRoom %}
{{ _self.stepFormField(form.roomSelections[roomId]) }} {{ _self.stepFormField(form.roomSelections[roomId]) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if bookingCreateContext.groupedRooms.by_pax is not empty %} {% if bookingCreateContext.groupedRooms.byPax is not empty %}
<h3> <h3>
Betten Betten
</h3> </h3>
{% for roomId, room in bookingCreateContext.groupedRooms.by_pax %} {% for roomId, room in bookingCreateContext.groupedRooms.byPax %}
{{ _self.stepFormField(form.roomSelections[roomId]) }} {{ _self.stepFormField(form.roomSelections[roomId]) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
+12 -12
View File
@@ -20,7 +20,7 @@
<div class="shrink-0 px-4 lg:px-8 py-4 lg:flex lg:flex-col lg:justify-center shadow-md relative z-10"> <div class="shrink-0 px-4 lg:px-8 py-4 lg:flex lg:flex-col lg:justify-center shadow-md relative z-10">
<div class="flex items-center justify-between pb-2"> <div class="flex items-center justify-between pb-2">
<span class="block uppercase font-semibold">Gesamtpreis</span> <span class="block uppercase font-semibold">Gesamtpreis</span>
<span class="font-semibold">{{ bookingCreateContext.summaryData.payableAmount|format_currency('EUR') }}</span> <span class="font-semibold">{{ bookingCreateContext.summaryData.vouchers.payableAmount|format_currency('EUR') }}</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="block uppercase font-semibold">Buchungsübersicht</span> <span class="block uppercase font-semibold">Buchungsübersicht</span>
@@ -132,18 +132,18 @@
</h3> </h3>
{# Rooms Section #} {# Rooms Section #}
{% if bookingCreateContext.summaryData.pricingData.rooms is not empty %} {% if bookingCreateContext.summaryData.pricing.rooms is not empty %}
<div class="border border-primary-bg mb-4"> <div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold"> <div class="p-2 bg-primary-bg uppercase font-semibold">
Unterkunft Unterkunft
</div> </div>
<table class="w-full table-fixed"> <table class="w-full table-fixed">
{% for roomPricing in bookingCreateContext.summaryData.pricingData.rooms %} {% for roomPricing in bookingCreateContext.summaryData.pricing.rooms %}
<tr> <tr>
<td class="p-2 align-top"> <td class="p-2 align-top">
<div>{{ roomPricing.quantity }}x {{ roomPricing.label }}</div> <div>{{ roomPricing.quantity }}x {{ roomPricing.label }}</div>
{% if bookingCreateContext.summaryData.assignmentCounts[roomPricing.roomId] is defined %} {% if bookingCreateContext.summaryData.pricing.assignmentCounts[roomPricing.roomId] is defined %}
<div class="text-sm text-gray-600">{{ bookingCreateContext.summaryData.assignmentCounts[roomPricing.roomId] }} Person(en) belegt</div> <div class="text-sm text-gray-600">{{ bookingCreateContext.summaryData.pricing.assignmentCounts[roomPricing.roomId] }} Person(en) belegt</div>
{% endif %} {% endif %}
</td> </td>
<td class="p-2 align-top w-24 text-right whitespace-nowrap"> <td class="p-2 align-top w-24 text-right whitespace-nowrap">
@@ -156,12 +156,12 @@
{% endif %} {% endif %}
{# Services Section #} {# Services Section #}
{% if bookingCreateContext.summaryData.pricingData.services is not empty %} {% if bookingCreateContext.summaryData.pricing.services is not empty %}
<div class="border border-primary-bg mb-4"> <div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold"> <div class="p-2 bg-primary-bg uppercase font-semibold">
Leistungen Leistungen
</div> </div>
{% for serviceGroup in bookingCreateContext.summaryData.pricingData.services %} {% for serviceGroup in bookingCreateContext.summaryData.pricing.services %}
<div class="p-2 text-primary-dark/70 uppercase font-semibold"> <div class="p-2 text-primary-dark/70 uppercase font-semibold">
{{ serviceGroup.groupName }} {{ serviceGroup.groupName }}
</div> </div>
@@ -462,8 +462,8 @@
{% endfor %} {% endfor %}
{# Grand Total #} {# Grand Total #}
{% if bookingCreateContext.summaryData.pricingData.grandTotal is defined and bookingCreateContext.summaryData.pricingData.grandTotal > 0 %} {% if bookingCreateContext.summaryData.pricing.grandTotal > 0 %}
{% set acceptedVouchers = bookingCreateContext.bookingDto.getAcceptedVouchers() %} {% set acceptedVouchers = bookingCreateContext.summaryData.vouchers.acceptedVouchers %}
{% if acceptedVouchers and acceptedVouchers.hasVouchers() %} {% if acceptedVouchers and acceptedVouchers.hasVouchers() %}
<div class="border border-primary-bg mb-4"> <div class="border border-primary-bg mb-4">
<div class="p-2 bg-primary-bg uppercase font-semibold"> <div class="p-2 bg-primary-bg uppercase font-semibold">
@@ -475,7 +475,7 @@
Gesamtpreis Gesamtpreis
</td> </td>
<td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap"> <td class="px-2 pb-2 align-top w-24 text-right whitespace-nowrap">
{{ bookingCreateContext.summaryData.pricingData.grandTotal|format_currency('EUR') }} {{ bookingCreateContext.summaryData.pricing.grandTotal|format_currency('EUR') }}
</td> </td>
</tr> </tr>
{% for voucher in acceptedVouchers.vouchers %} {% for voucher in acceptedVouchers.vouchers %}
@@ -498,12 +498,12 @@
</div> </div>
<div class="flex items-center justify-between px-2 pb-4"> <div class="flex items-center justify-between px-2 pb-4">
<span class="block uppercase font-semibold">Zu zahlen</span> <span class="block uppercase font-semibold">Zu zahlen</span>
<span class="font-semibold">{{ bookingCreateContext.summaryData.payableAmount|format_currency('EUR') }}</span> <span class="font-semibold">{{ bookingCreateContext.summaryData.vouchers.payableAmount|format_currency('EUR') }}</span>
</div> </div>
{% else %} {% else %}
<div class="flex items-center justify-between pb-4"> <div class="flex items-center justify-between pb-4">
<span class="block uppercase font-semibold">Gesamtpreis</span> <span class="block uppercase font-semibold">Gesamtpreis</span>
<span class="font-semibold">{{ bookingCreateContext.summaryData.pricingData.grandTotal|format_currency('EUR') }}</span> <span class="font-semibold">{{ bookingCreateContext.summaryData.pricing.grandTotal|format_currency('EUR') }}</span>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
@@ -8,6 +8,7 @@ use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel; use App\BusProNet\Model\Travel;
use App\Form\Model\BookingCreateContext; use App\Form\Model\BookingCreateContext;
use App\Form\Model\BookingDto; use App\Form\Model\BookingDto;
use App\Form\Model\RoomGroupsDto;
use App\Form\Model\ParticipantCardDataDto; use App\Form\Model\ParticipantCardDataDto;
use App\Form\Model\ParticipantCardPriceDto; use App\Form\Model\ParticipantCardPriceDto;
use App\Form\Model\BookingSummaryDto; use App\Form\Model\BookingSummaryDto;
@@ -56,13 +57,14 @@ class BookingCreateContextFactoryTest extends TestCase
->willReturn($summaryData); ->willReturn($summaryData);
$roomSelectionService = $this->createMock(BookingRoomSelectionService::class); $roomSelectionService = $this->createMock(BookingRoomSelectionService::class);
$groupedRooms = new RoomGroupsDto(
byPax: [$roomByPax->id => $roomByPax],
byRoom: [$roomByRoom->id => $roomByRoom],
);
$roomSelectionService->expects($this->once()) $roomSelectionService->expects($this->once())
->method('groupRoomsBySelectionType') ->method('groupRoomsBySelectionType')
->with([$roomByRoom->id => $roomByRoom, $roomByPax->id => $roomByPax]) ->with([$roomByRoom->id => $roomByRoom, $roomByPax->id => $roomByPax])
->willReturn([ ->willReturn($groupedRooms);
Room::SELECTION_TYPE_BY_PAX => [$roomByPax->id => $roomByPax],
Room::SELECTION_TYPE_BY_ROOM => [$roomByRoom->id => $roomByRoom],
]);
$participantCardDataService = $this->createMock(ParticipantCardDataService::class); $participantCardDataService = $this->createMock(ParticipantCardDataService::class);
$participantCardDataService->expects($this->never()) $participantCardDataService->expects($this->never())
@@ -86,10 +88,7 @@ class BookingCreateContextFactoryTest extends TestCase
$this->assertSame($summaryData, $context->summaryData); $this->assertSame($summaryData, $context->summaryData);
$this->assertSame(null, $context->cardsData); $this->assertSame(null, $context->cardsData);
$this->assertFalse($context->isSubmitted); $this->assertFalse($context->isSubmitted);
$this->assertSame([ $this->assertSame($groupedRooms, $context->groupedRooms);
Room::SELECTION_TYPE_BY_PAX => [$roomByPax->id => $roomByPax],
Room::SELECTION_TYPE_BY_ROOM => [$roomByRoom->id => $roomByRoom],
], $context->groupedRooms);
} }
public function testCreateWithParticipantCardsBuildsStep2Context(): void public function testCreateWithParticipantCardsBuildsStep2Context(): void
@@ -124,13 +123,14 @@ class BookingCreateContextFactoryTest extends TestCase
->willReturn($summaryData); ->willReturn($summaryData);
$roomSelectionService = $this->createMock(BookingRoomSelectionService::class); $roomSelectionService = $this->createMock(BookingRoomSelectionService::class);
$groupedRooms = new RoomGroupsDto(
byPax: [],
byRoom: [$room->id => $room],
);
$roomSelectionService->expects($this->once()) $roomSelectionService->expects($this->once())
->method('groupRoomsBySelectionType') ->method('groupRoomsBySelectionType')
->with([$room->id => $room]) ->with([$room->id => $room])
->willReturn([ ->willReturn($groupedRooms);
Room::SELECTION_TYPE_BY_PAX => [],
Room::SELECTION_TYPE_BY_ROOM => [$room->id => $room],
]);
$participantCardDataService = $this->createMock(ParticipantCardDataService::class); $participantCardDataService = $this->createMock(ParticipantCardDataService::class);
$participantCardDataService->expects($this->once()) $participantCardDataService->expects($this->once())
@@ -181,13 +181,14 @@ class BookingCreateContextFactoryTest extends TestCase
->willReturn($summaryData); ->willReturn($summaryData);
$roomSelectionService = $this->createMock(BookingRoomSelectionService::class); $roomSelectionService = $this->createMock(BookingRoomSelectionService::class);
$groupedRooms = new RoomGroupsDto(
byPax: [],
byRoom: [$room->id => $room],
);
$roomSelectionService->expects($this->once()) $roomSelectionService->expects($this->once())
->method('groupRoomsBySelectionType') ->method('groupRoomsBySelectionType')
->with([$room->id => $room]) ->with([$room->id => $room])
->willReturn([ ->willReturn($groupedRooms);
Room::SELECTION_TYPE_BY_PAX => [],
Room::SELECTION_TYPE_BY_ROOM => [$room->id => $room],
]);
$participantCardDataService = $this->createMock(ParticipantCardDataService::class); $participantCardDataService = $this->createMock(ParticipantCardDataService::class);
$participantCardDataService->expects($this->never()) $participantCardDataService->expects($this->never())
@@ -6,8 +6,10 @@ namespace App\Tests\Service;
use App\BusProNet\Model\BaseData; use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Booking; use App\BusProNet\Model\Booking;
use App\BusProNet\Model\MutableData;
use App\BusProNet\Model\Travel; use App\BusProNet\Model\Travel;
use App\Form\Model\BookingDto; use App\Form\Model\BookingDto;
use App\Form\Model\BookingMutabilityDto;
use App\Form\Model\BookingEditContext; use App\Form\Model\BookingEditContext;
use App\Form\Model\BookingSummaryDto; use App\Form\Model\BookingSummaryDto;
use App\Service\BookingEditContextFactory; use App\Service\BookingEditContextFactory;
@@ -51,7 +53,9 @@ class BookingEditContextFactoryTest extends TestCase
$bookingData = new Booking(); $bookingData = new Booking();
$bookingData->dateId = 1234; $bookingData->dateId = 1234;
$mutableData = new BaseData([]); $mutableData = new BaseData([
MutableData::CATEGORY_ADDITIONAL_SERVICES => new MutableData(MutableData::CATEGORY_ADDITIONAL_SERVICES, true, new \DateTimeImmutable('2030-01-02')),
]);
$summaryData = $this->createMock(BookingSummaryDto::class); $summaryData = $this->createMock(BookingSummaryDto::class);
$summaryDataService = $this->createMock(BookingSummaryDataService::class); $summaryDataService = $this->createMock(BookingSummaryDataService::class);
@@ -77,7 +81,9 @@ class BookingEditContextFactoryTest extends TestCase
$this->assertInstanceOf(BookingEditContext::class, $context); $this->assertInstanceOf(BookingEditContext::class, $context);
$this->assertSame($bookingDto, $context->bookingDto); $this->assertSame($bookingDto, $context->bookingDto);
$this->assertSame($bookingData, $context->bookingData); $this->assertSame($bookingData, $context->bookingData);
$this->assertSame($mutableData, $context->mutableData); $this->assertInstanceOf(BookingMutabilityDto::class, $context->mutableData);
$this->assertSame($mutableData->getItemByKey(MutableData::CATEGORY_ADDITIONAL_SERVICES), $context->mutableData->additionalServices);
$this->assertNull($context->mutableData->transportation);
$this->assertSame($summaryData, $context->summaryData); $this->assertSame($summaryData, $context->summaryData);
} }
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Tests\Service; namespace App\Tests\Service;
use App\BusProNet\Model\Room; use App\BusProNet\Model\Room;
use App\Form\Model\RoomGroupsDto;
use App\Service\BookingRoomSelectionService; use App\Service\BookingRoomSelectionService;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -27,8 +28,9 @@ class BookingRoomSelectionServiceTest extends TestCase
$groups = $this->service->groupRoomsBySelectionType($rooms); $groups = $this->service->groupRoomsBySelectionType($rooms);
$this->assertSame([10, 12], array_keys($groups[Room::SELECTION_TYPE_BY_PAX])); $this->assertInstanceOf(RoomGroupsDto::class, $groups);
$this->assertSame([11], array_keys($groups[Room::SELECTION_TYPE_BY_ROOM])); $this->assertSame([10, 12], array_keys($groups->byPax));
$this->assertSame([11], array_keys($groups->byRoom));
} }
private function createRoom(int $id, string $label, int $maxPax): Room private function createRoom(int $id, string $label, int $maxPax): Room