From 7aad54e6f2c2efa03c8c3810ad7f02858f2ad7f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Mon, 14 Sep 2026 15:02:04 +0200 Subject: [PATCH] fix: keep the booking room assignment when restoring an edit draft --- src/Service/BookingEditDraftMerger.php | 30 +++-- ...okingEditDraftMergerRoomAssignmentTest.php | 115 ++++++++++++++++++ 2 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 tests/Service/BookingEditDraftMergerRoomAssignmentTest.php diff --git a/src/Service/BookingEditDraftMerger.php b/src/Service/BookingEditDraftMerger.php index b0088be..be77510 100644 --- a/src/Service/BookingEditDraftMerger.php +++ b/src/Service/BookingEditDraftMerger.php @@ -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 $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 $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']; } diff --git a/tests/Service/BookingEditDraftMergerRoomAssignmentTest.php b/tests/Service/BookingEditDraftMergerRoomAssignmentTest.php new file mode 100644 index 0000000..ba88652 --- /dev/null +++ b/tests/Service/BookingEditDraftMergerRoomAssignmentTest.php @@ -0,0 +1,115 @@ +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 $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 $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; + } +}