fix: allow updating room assignments in edit mode

This commit is contained in:
Björn Fromme
2026-01-30 17:29:26 +01:00
parent 0aa4bb5055
commit 6b205c7a86
11 changed files with 191 additions and 21 deletions
@@ -162,6 +162,8 @@ class BookingDataProcessor
$roomSelection->label = $room->label;
$roomSelection->price = $room->price;
$roomSelection->quantity = $bookingRoom->totalCount; // Actual quantity from booking XML (anzahl)
$roomSelection->capacity = $room->minPax ?? 1;
$roomSelection->maxQuantity = $bookingRoom->totalCount + ($room->available ?? 0);
$dto->roomSelections[] = $roomSelection;
}
}
@@ -172,7 +172,7 @@ class BookingPayloadBuilder
'@idverpflegung' => $room->boardId,
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
'@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null,
'@anzahl' => $room->totalCount,
'@anzahl' => count($uniqueMapping),
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
];
}
@@ -456,21 +456,12 @@ class BookingPayloadBuilder
$availableRooms = $bookingDto->travel->getAvailableRooms();
$payload['ferienzielunterbringungen']['ferienzielunterbringung'] = [];
// Build room selection quantity lookup
$roomQuantities = [];
foreach ($bookingDto->roomSelections as $selection) {
if ($selection->quantity > 0) {
$roomQuantities[$selection->id] = $selection->quantity;
}
}
foreach ($roomMap as $roomId => $participantIds) {
$room = $availableRooms[$roomId] ?? null;
if (null === $room) {
continue;
}
$quantity = $roomQuantities[$roomId] ?? 1;
$uniqueParticipantIds = array_unique($participantIds);
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
@@ -479,7 +470,7 @@ class BookingPayloadBuilder
'@idverpflegung' => $room->boardId,
'@anreise' => $bookingDto->travel->dateFrom->format('d.m.Y'),
'@abreise' => $bookingDto->travel->dateTo->format('d.m.Y'),
'@anzahl' => $quantity,
'@anzahl' => count($uniqueParticipantIds),
'@zuordnung' => implode(',', $uniqueParticipantIds),
];
}
+5
View File
@@ -132,6 +132,11 @@ class ParticipantEditDto
#[Assert\Callback(groups: ['booking_create', 'booking_edit'])]
public function validateEmailUniqueness(ExecutionContextInterface $context): void
{
// Skip for internal agency bookings — staff use shared placeholder emails
if ($this->bookingContext->isInternalAgencyBooking()) {
return;
}
// Skip validation for applicant (index 0) - applicant's email can be shared with dependents
if (true === $this->participant->isApplicant()) {
return;
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Form\Service\Condition;
use App\Form\Model\BookingDto;
use App\Form\Service\Contract\FieldConditionInterface;
/**
* Condition that checks if accommodation (room assignments) are mutable in the edit flow.
*/
class AccommodationMutabilityCondition implements FieldConditionInterface
{
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
{
return false === $bookingDto->travel->roomsMutable;
}
public function getDependentFields(): array
{
return [];
}
public function getDescription(): string
{
return 'Accommodation is not mutable (edit flow)';
}
}
+4 -5
View File
@@ -5,11 +5,10 @@ declare(strict_types=1);
namespace App\Form\Service;
use App\BusProNet\Utility\DirectionMapper;
use App\Form\Model\BookingDto;
use App\Form\Service\Abstract\AbstractFieldStateProvider;
use App\Form\Service\Condition\AccommodationMutabilityCondition;
use App\Form\Service\Condition\AdditionalServicesMutabilityCondition;
use App\Form\Service\Condition\AgeRangeCondition;
use App\Form\Service\Condition\BookingModeCondition;
use App\Form\Service\Condition\CompositeCondition;
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
use App\Form\Service\Condition\FieldValueCondition;
@@ -99,10 +98,10 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
'static_text' => $personalDataHiddenCondition,
];
// Room assignments are fixed in edit mode - always render as static text
// The assignedRoomId value passes through unchanged from the loaded booking data
// Room assignments are readonly when BPN indicates accommodation is not mutable
$accommodationMutabilityCondition = new AccommodationMutabilityCondition();
$this->fieldStateConditions['assignedRoomId'] = [
'static_text' => new BookingModeCondition(BookingDto::MODE_EDIT),
'static_text' => $accommodationMutabilityCondition,
];
// Show remarks room field only when room with code 'mbz' (single bed) is selected
@@ -122,7 +122,14 @@ class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandle
}
// Calculate total capacity for this room type
$totalCapacity = $roomSelection->quantity * $roomSelection->capacity;
// In edit mode, use maxQuantity which includes available rooms beyond the booked count.
// In create mode, use quantity which is the user's room selection from step 1.
if (BookingDto::MODE_EDIT === $bookingDto->getMode()) {
$effectiveQuantity = $roomSelection->maxQuantity;
} else {
$effectiveQuantity = $roomSelection->quantity ?? 0;
}
$totalCapacity = $effectiveQuantity * $roomSelection->capacity;
// Count current occupancy (excluding current participant)
$occupancy = 0;
@@ -174,7 +181,12 @@ class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandle
return; // Shouldn't happen
}
$totalCapacity = $roomSelection->quantity * $roomSelection->capacity;
if (BookingDto::MODE_EDIT === $bookingDto->getMode()) {
$effectiveQuantity = $roomSelection->maxQuantity;
} else {
$effectiveQuantity = $roomSelection->quantity ?? 0;
}
$totalCapacity = $effectiveQuantity * $roomSelection->capacity;
$currentOccupancy = count($participantsWithRoom);
$spacesNeeded = ($currentOccupancy + 1) - $totalCapacity;
+2 -2
View File
@@ -237,8 +237,8 @@ class ParticipantCardDataService
} else {
$groups[] = 'booking_edit';
// In edit mode, add strict_required only if applicant is immutable
if (false === ($bookingDto->participants[0]?->mutable ?? true)) {
// Strict validation in edit mode except for internal agency bookings
if (false === $bookingDto->isInternalAgencyBooking()) {
$groups[] = 'strict_required';
}
}
+9 -1
View File
@@ -135,6 +135,10 @@ class RoomPricingCalculator
}
}
// Count current participant assignments from the DTO (reflects live state
// including new assignments not yet in the booking entity)
$assignmentCounts = $bookingDto->getRoomAssignmentCounts();
// Build pricing array
foreach ($roomGroups as $roomId => $data) {
$room = $data['room'];
@@ -144,10 +148,14 @@ class RoomPricingCalculator
// Calculate average unit price (price per person)
$unitPrice = $participantCount > 0 ? $totalPrice / $participantCount : 0.0;
// Use live assignment count for quantity display so the sidebar reflects
// new room assignments that haven't been persisted to the booking entity yet
$quantity = $assignmentCounts[$roomId] ?? $room->totalCount;
$roomPricing[] = [
'roomId' => $room->id,
'label' => $room->label,
'quantity' => $room->totalCount,
'quantity' => $quantity,
'participantCount' => $participantCount,
'unitPrice' => $unitPrice,
'totalPrice' => $totalPrice,
+12
View File
@@ -104,6 +104,18 @@
{% endif %}
</td>
</tr>
<tr>
<td class="p-2 align-top">
Unterkunft
</td>
<td class="p-2 align-top w-24 text-right whitespace-nowrap">
{% if mutableData.items['accommodation'].mutable %}
{{ mutableData.items['accommodation'].mutableBefore|date('d.m.Y') }}
{% else %}
nicht mehr möglich
{% endif %}
</td>
</tr>
</table>
</div>
{% endif %}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Tests\Form\Model;
use App\BusProNet\Model\Travel;
use App\BusProNet\XmlLoader\AgencyLoader;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Model\ParticipantEditDto;
@@ -373,6 +374,46 @@ class ParticipantEditDtoTest extends TestCase
$this->assertCount(0, $violations5);
}
public function testInternalAgencyBookingSkipsEmailUniqueness(): void
{
$bookingDto = $this->createBookingDtoWithParticipants([
$this->createAdultParticipant('[email protected]'),
$this->createAdultParticipant('[email protected]'),
$this->createAdultParticipant('[email protected]'),
]);
// Mark as internal agency booking
$bookingDto->agencyCode = AgencyLoader::INTERNAL_AGENCY_CODE;
// Validate second participant (non-applicant adult with duplicate email)
$wrapper = new ParticipantEditDto(
participant: $bookingDto->participants[1],
bookingContext: $bookingDto,
);
$violations = $this->validator->validate($wrapper, null, ['booking_create']);
$this->assertCount(0, $violations, 'Internal agency bookings should skip email uniqueness validation');
}
public function testNonAgencyBookingStillEnforcesEmailUniqueness(): void
{
$bookingDto = $this->createBookingDtoWithParticipants([
$this->createAdultParticipant('[email protected]'),
$this->createAdultParticipant('[email protected]'),
]);
// Non-agency booking (default agencyCode is null)
$wrapper = new ParticipantEditDto(
participant: $bookingDto->participants[1],
bookingContext: $bookingDto,
);
$violations = $this->validator->validate($wrapper, null, ['booking_create']);
$this->assertCount(1, $violations, 'Non-agency bookings should still enforce email uniqueness');
}
// =========================================
// Ski Pass Validation Tests (Baby Age Exemption)
// =========================================
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Tests\Form\Service\Condition;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingDto;
use App\Form\Service\Condition\AccommodationMutabilityCondition;
use PHPUnit\Framework\TestCase;
class AccommodationMutabilityConditionTest extends TestCase
{
private AccommodationMutabilityCondition $condition;
protected function setUp(): void
{
$this->condition = new AccommodationMutabilityCondition();
}
public function testReturnsFalseWhenRoomsMutable(): void
{
$travel = new Travel();
$travel->roomsMutable = true;
$bookingDto = new BookingDto($travel, 1);
$result = $this->condition->evaluate($bookingDto, 0, []);
$this->assertFalse($result, 'Should return false when rooms are mutable (field should be editable)');
}
public function testReturnsTrueWhenRoomsNotMutable(): void
{
$travel = new Travel();
$travel->roomsMutable = false;
$bookingDto = new BookingDto($travel, 1);
$result = $this->condition->evaluate($bookingDto, 0, []);
$this->assertTrue($result, 'Should return true when rooms are not mutable (field should be static text)');
}
public function testDefaultTravelHasMutableRooms(): void
{
$travel = new Travel();
$bookingDto = new BookingDto($travel, 1);
$result = $this->condition->evaluate($bookingDto, 0, []);
$this->assertFalse($result, 'Default travel should have mutable rooms');
}
public function testGetDependentFieldsReturnsEmptyArray(): void
{
$result = $this->condition->getDependentFields();
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetDescriptionReturnsString(): void
{
$result = $this->condition->getDescription();
$this->assertIsString($result);
$this->assertStringContainsString('not mutable', $result);
}
}