fix: keep the booking room assignment when restoring an edit draft

This commit is contained in:
2026-09-14 15:02:04 +02:00
parent b41820c39d
commit 7aad54e6f2
2 changed files with 138 additions and 7 deletions
+23 -7
View File
@@ -18,8 +18,9 @@ use App\Form\Model\ParticipantDto;
*
* Service selections are gated by travel-level mutability flags (additionalServicesMutable,
* transportationServicesMutable, pickupsMutable). Insurance is always applied regardless
* of mutability. Merge strategy (only apply if resolves to a valid service) is used for
* single-select fields; overwrite strategy is used for multi-select and boolean fields.
* of mutability. Merge strategy (only apply if it resolves against the travel data) is used
* for single-select fields, room assignment included; overwrite strategy is used for
* multi-select, boolean and free-text fields.
*/
class BookingEditDraftMerger
{
@@ -52,7 +53,7 @@ class BookingEditDraftMerger
// Room assignment
if (true === isset($data['roomAssignment']) && true === is_array($data['roomAssignment'])) {
$this->applyRoomAssignment($participant, $data['roomAssignment']);
$this->applyRoomAssignment($participant, $data['roomAssignment'], $travel);
}
// License plate
@@ -156,12 +157,27 @@ class BookingEditDraftMerger
}
}
/** @param array<string, mixed> $data */
private function applyRoomAssignment(ParticipantDto $participant, array $data): void
/**
* Applies the drafted room assignment to the participant.
*
* The room id uses the same merge strategy as single-select services: it is only
* applied when it resolves to a room of the current travel. A draft may therefore move
* a participant to another room, but never unassign one. Edit mode renders the room as
* static text and never submits assignedRoomId, so a null in the draft is only ever an
* artefact of a snapshot taken before BusPro reported the assignment - replaying it
* would drop the participant's room, both in the UI and in the outbound zuordnung.
*
* remarksRoom keeps overwrite semantics: it is free text the user can deliberately clear.
*
* @param array<string, mixed> $data
*/
private function applyRoomAssignment(ParticipantDto $participant, array $data, Travel $travel): void
{
if (true === array_key_exists('assignedRoomId', $data)) {
$participant->assignedRoomId = $data['assignedRoomId'];
$assignedRoomId = $data['assignedRoomId'] ?? null;
if (null !== $assignedRoomId && null !== $travel->getRoomById((int) $assignedRoomId)) {
$participant->assignedRoomId = (int) $assignedRoomId;
}
if (true === array_key_exists('remarksRoom', $data)) {
$participant->remarksRoom = $data['remarksRoom'];
}
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Service\BookingEditDraftMerger;
use PHPUnit\Framework\TestCase;
/**
* Tests that draft restoration never unassigns a room.
*
* Edit mode renders the room as static text and never submits assignedRoomId, so a null in
* the draft payload is always an artefact of a snapshot taken before BusPro reported the
* assignment - typically for seats added to a group booking shortly before the draft was
* saved. Replaying such a null would strip the room from the UI and from the outbound
* zuordnung, so the room id uses the same merge strategy as single-select services.
*/
class BookingEditDraftMergerRoomAssignmentTest extends TestCase
{
private BookingEditDraftMerger $merger;
protected function setUp(): void
{
$this->merger = new BookingEditDraftMerger();
}
public function testDraftDoesNotUnassignRoomWhenDraftValueIsNull(): void
{
$participant = new ParticipantDto();
$participant->mutable = true;
$participant->assignedRoomId = 74;
$travel = $this->createTravel([74 => $this->createRoom(74, 'Bett im Mehrbettzimmer')]);
$this->apply($travel, $participant, ['assignedRoomId' => null]);
$this->assertSame(74, $participant->assignedRoomId);
}
public function testDraftAppliesRoomThatResolvesAgainstTravel(): void
{
$participant = new ParticipantDto();
$participant->mutable = true;
$participant->assignedRoomId = 74;
$travel = $this->createTravel([
74 => $this->createRoom(74, 'Bett im Mehrbettzimmer'),
75 => $this->createRoom(75, 'Doppelzimmer'),
]);
$this->apply($travel, $participant, ['assignedRoomId' => 75]);
$this->assertSame(75, $participant->assignedRoomId);
}
public function testDraftSkipsRoomThatIsUnknownToTravel(): void
{
$participant = new ParticipantDto();
$participant->mutable = true;
$participant->assignedRoomId = 74;
$travel = $this->createTravel([74 => $this->createRoom(74, 'Bett im Mehrbettzimmer')]);
$this->apply($travel, $participant, ['assignedRoomId' => 999]);
$this->assertSame(74, $participant->assignedRoomId);
}
public function testDraftClearsRoomRemarks(): void
{
$participant = new ParticipantDto();
$participant->mutable = true;
$participant->assignedRoomId = 74;
$participant->remarksRoom = 'Bitte mit Lisa';
$travel = $this->createTravel([74 => $this->createRoom(74, 'Bett im Mehrbettzimmer')]);
$this->apply($travel, $participant, ['assignedRoomId' => null, 'remarksRoom' => null]);
$this->assertNull($participant->remarksRoom);
$this->assertSame(74, $participant->assignedRoomId);
}
/** @param array<string, mixed> $roomAssignment */
private function apply(Travel $travel, ParticipantDto $participant, array $roomAssignment): void
{
$dto = new BookingDto($travel, 1);
$dto->participants = [1 => $participant];
$this->merger->apply($dto, 1, $participant, ['roomAssignment' => $roomAssignment], $travel);
}
/** @param array<int, Room> $rooms */
private function createTravel(array $rooms): Travel
{
$travel = new Travel();
$travel->rooms = $rooms;
return $travel;
}
private function createRoom(int $id, string $label): Room
{
$room = new Room();
$room->id = $id;
$room->label = $label;
return $room;
}
}