fix: allow updating room assignments in edit mode
This commit is contained in:
@@ -162,6 +162,8 @@ class BookingDataProcessor
|
|||||||
$roomSelection->label = $room->label;
|
$roomSelection->label = $room->label;
|
||||||
$roomSelection->price = $room->price;
|
$roomSelection->price = $room->price;
|
||||||
$roomSelection->quantity = $bookingRoom->totalCount; // Actual quantity from booking XML (anzahl)
|
$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;
|
$dto->roomSelections[] = $roomSelection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ class BookingPayloadBuilder
|
|||||||
'@idverpflegung' => $room->boardId,
|
'@idverpflegung' => $room->boardId,
|
||||||
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
|
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
|
||||||
'@abreise' => $room->dateTo ? $room->dateTo->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)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -456,21 +456,12 @@ class BookingPayloadBuilder
|
|||||||
$availableRooms = $bookingDto->travel->getAvailableRooms();
|
$availableRooms = $bookingDto->travel->getAvailableRooms();
|
||||||
$payload['ferienzielunterbringungen']['ferienzielunterbringung'] = [];
|
$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) {
|
foreach ($roomMap as $roomId => $participantIds) {
|
||||||
$room = $availableRooms[$roomId] ?? null;
|
$room = $availableRooms[$roomId] ?? null;
|
||||||
if (null === $room) {
|
if (null === $room) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$quantity = $roomQuantities[$roomId] ?? 1;
|
|
||||||
$uniqueParticipantIds = array_unique($participantIds);
|
$uniqueParticipantIds = array_unique($participantIds);
|
||||||
|
|
||||||
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
|
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
|
||||||
@@ -479,7 +470,7 @@ class BookingPayloadBuilder
|
|||||||
'@idverpflegung' => $room->boardId,
|
'@idverpflegung' => $room->boardId,
|
||||||
'@anreise' => $bookingDto->travel->dateFrom->format('d.m.Y'),
|
'@anreise' => $bookingDto->travel->dateFrom->format('d.m.Y'),
|
||||||
'@abreise' => $bookingDto->travel->dateTo->format('d.m.Y'),
|
'@abreise' => $bookingDto->travel->dateTo->format('d.m.Y'),
|
||||||
'@anzahl' => $quantity,
|
'@anzahl' => count($uniqueParticipantIds),
|
||||||
'@zuordnung' => implode(',', $uniqueParticipantIds),
|
'@zuordnung' => implode(',', $uniqueParticipantIds),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,11 @@ class ParticipantEditDto
|
|||||||
#[Assert\Callback(groups: ['booking_create', 'booking_edit'])]
|
#[Assert\Callback(groups: ['booking_create', 'booking_edit'])]
|
||||||
public function validateEmailUniqueness(ExecutionContextInterface $context): void
|
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
|
// Skip validation for applicant (index 0) - applicant's email can be shared with dependents
|
||||||
if (true === $this->participant->isApplicant()) {
|
if (true === $this->participant->isApplicant()) {
|
||||||
return;
|
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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,10 @@ declare(strict_types=1);
|
|||||||
namespace App\Form\Service;
|
namespace App\Form\Service;
|
||||||
|
|
||||||
use App\BusProNet\Utility\DirectionMapper;
|
use App\BusProNet\Utility\DirectionMapper;
|
||||||
use App\Form\Model\BookingDto;
|
|
||||||
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
||||||
|
use App\Form\Service\Condition\AccommodationMutabilityCondition;
|
||||||
use App\Form\Service\Condition\AdditionalServicesMutabilityCondition;
|
use App\Form\Service\Condition\AdditionalServicesMutabilityCondition;
|
||||||
use App\Form\Service\Condition\AgeRangeCondition;
|
use App\Form\Service\Condition\AgeRangeCondition;
|
||||||
use App\Form\Service\Condition\BookingModeCondition;
|
|
||||||
use App\Form\Service\Condition\CompositeCondition;
|
use App\Form\Service\Condition\CompositeCondition;
|
||||||
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
||||||
use App\Form\Service\Condition\FieldValueCondition;
|
use App\Form\Service\Condition\FieldValueCondition;
|
||||||
@@ -99,10 +98,10 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
'static_text' => $personalDataHiddenCondition,
|
'static_text' => $personalDataHiddenCondition,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Room assignments are fixed in edit mode - always render as static text
|
// Room assignments are readonly when BPN indicates accommodation is not mutable
|
||||||
// The assignedRoomId value passes through unchanged from the loaded booking data
|
$accommodationMutabilityCondition = new AccommodationMutabilityCondition();
|
||||||
$this->fieldStateConditions['assignedRoomId'] = [
|
$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
|
// 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
|
// 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)
|
// Count current occupancy (excluding current participant)
|
||||||
$occupancy = 0;
|
$occupancy = 0;
|
||||||
@@ -174,7 +181,12 @@ class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandle
|
|||||||
return; // Shouldn't happen
|
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);
|
$currentOccupancy = count($participantsWithRoom);
|
||||||
$spacesNeeded = ($currentOccupancy + 1) - $totalCapacity;
|
$spacesNeeded = ($currentOccupancy + 1) - $totalCapacity;
|
||||||
|
|
||||||
|
|||||||
@@ -237,8 +237,8 @@ class ParticipantCardDataService
|
|||||||
} else {
|
} else {
|
||||||
$groups[] = 'booking_edit';
|
$groups[] = 'booking_edit';
|
||||||
|
|
||||||
// In edit mode, add strict_required only if applicant is immutable
|
// Strict validation in edit mode except for internal agency bookings
|
||||||
if (false === ($bookingDto->participants[0]?->mutable ?? true)) {
|
if (false === $bookingDto->isInternalAgencyBooking()) {
|
||||||
$groups[] = 'strict_required';
|
$groups[] = 'strict_required';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// Build pricing array
|
||||||
foreach ($roomGroups as $roomId => $data) {
|
foreach ($roomGroups as $roomId => $data) {
|
||||||
$room = $data['room'];
|
$room = $data['room'];
|
||||||
@@ -144,10 +148,14 @@ class RoomPricingCalculator
|
|||||||
// Calculate average unit price (price per person)
|
// Calculate average unit price (price per person)
|
||||||
$unitPrice = $participantCount > 0 ? $totalPrice / $participantCount : 0.0;
|
$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[] = [
|
$roomPricing[] = [
|
||||||
'roomId' => $room->id,
|
'roomId' => $room->id,
|
||||||
'label' => $room->label,
|
'label' => $room->label,
|
||||||
'quantity' => $room->totalCount,
|
'quantity' => $quantity,
|
||||||
'participantCount' => $participantCount,
|
'participantCount' => $participantCount,
|
||||||
'unitPrice' => $unitPrice,
|
'unitPrice' => $unitPrice,
|
||||||
'totalPrice' => $totalPrice,
|
'totalPrice' => $totalPrice,
|
||||||
|
|||||||
@@ -104,6 +104,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Tests\Form\Model;
|
namespace App\Tests\Form\Model;
|
||||||
|
|
||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\BusProNet\XmlLoader\AgencyLoader;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Form\Model\ParticipantEditDto;
|
use App\Form\Model\ParticipantEditDto;
|
||||||
@@ -373,6 +374,46 @@ class ParticipantEditDtoTest extends TestCase
|
|||||||
$this->assertCount(0, $violations5);
|
$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)
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user