fix: pass BusPro room assignments through on edit
This commit is contained in:
1 parent
897a1ac7f9
commit
d50995a21c
7 files changed
+163
-45
No files matched your search
@@ -341,6 +341,7 @@ class BookingDataProcessor
|
|||||||
$this->serviceProcessor->processParticipantServices($participant, $bookingData, $travelData);
|
$this->serviceProcessor->processParticipantServices($participant, $bookingData, $travelData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->serviceProcessor->removeCanceledParticipantsFromRooms($bookingData);
|
||||||
$this->serviceProcessor->removeUnusedServices($bookingData);
|
$this->serviceProcessor->removeUnusedServices($bookingData);
|
||||||
$this->personalDataSynchronizer->updateParticipantPersonalData(
|
$this->personalDataSynchronizer->updateParticipantPersonalData(
|
||||||
$formData->participants,
|
$formData->participants,
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ use Psr\Log\LoggerInterface;
|
|||||||
* Processes participant service selections for booking updates.
|
* Processes participant service selections for booking updates.
|
||||||
*
|
*
|
||||||
* Handles the assignment and mapping of services (additional, transportation,
|
* Handles the assignment and mapping of services (additional, transportation,
|
||||||
* pickup, room, insurance) from participant DTOs to the booking model during
|
* pickup, insurance) from participant DTOs to the booking model during the
|
||||||
* the update request workflow.
|
* update request workflow. Rooms are not taken from the DTOs: they cannot be
|
||||||
|
* changed in edit mode, so the booking's own room assignments are sent back.
|
||||||
*/
|
*/
|
||||||
class ParticipantServiceProcessor
|
class ParticipantServiceProcessor
|
||||||
{
|
{
|
||||||
@@ -28,7 +29,7 @@ class ParticipantServiceProcessor
|
|||||||
* Resets all existing participant-to-service mappings to start with a clean slate.
|
* Resets all existing participant-to-service mappings to start with a clean slate.
|
||||||
*
|
*
|
||||||
* This ensures that service assignments are rebuilt from scratch based on current form selections.
|
* This ensures that service assignments are rebuilt from scratch based on current form selections.
|
||||||
* Includes resetting room mappings to allow room reassignments during edit.
|
* Room mappings are left alone, see removeCanceledParticipantsFromRooms().
|
||||||
*
|
*
|
||||||
* @param Booking $bookingData The booking data object containing services to reset
|
* @param Booking $bookingData The booking data object containing services to reset
|
||||||
*/
|
*/
|
||||||
@@ -39,7 +40,6 @@ class ParticipantServiceProcessor
|
|||||||
...$bookingData->transportationServices,
|
...$bookingData->transportationServices,
|
||||||
...$bookingData->pickups,
|
...$bookingData->pickups,
|
||||||
...$bookingData->dropOffs,
|
...$bookingData->dropOffs,
|
||||||
...$bookingData->rooms,
|
|
||||||
...$bookingData->insurances,
|
...$bookingData->insurances,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class ParticipantServiceProcessor
|
|||||||
* Processes all services for a single participant.
|
* Processes all services for a single participant.
|
||||||
*
|
*
|
||||||
* This orchestrator method handles the complete service assignment workflow for one participant,
|
* This orchestrator method handles the complete service assignment workflow for one participant,
|
||||||
* including additional services, transportation services, pickup locations, and room assignments.
|
* including additional services, transportation services, pickup locations, and insurance.
|
||||||
*
|
*
|
||||||
* @param ParticipantDto $participant The participant data from the form
|
* @param ParticipantDto $participant The participant data from the form
|
||||||
* @param Booking $bookingData The booking data object to update
|
* @param Booking $bookingData The booking data object to update
|
||||||
@@ -68,7 +68,6 @@ class ParticipantServiceProcessor
|
|||||||
$this->processTransportationServices($participant, $bookingData, $travelData);
|
$this->processTransportationServices($participant, $bookingData, $travelData);
|
||||||
$this->processPickupLocations($participant, $bookingData);
|
$this->processPickupLocations($participant, $bookingData);
|
||||||
$this->processDropOffLocations($participant, $bookingData);
|
$this->processDropOffLocations($participant, $bookingData);
|
||||||
$this->processRoomAssignment($participant, $bookingData);
|
|
||||||
$this->processInsurance($participant, $bookingData, $travelData);
|
$this->processInsurance($participant, $bookingData, $travelData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,27 +270,23 @@ class ParticipantServiceProcessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes room assignment for a participant.
|
* Removes canceled participants from the booking's room assignments.
|
||||||
*
|
*
|
||||||
* Maps the participant to their assigned room. Allows room reassignment during
|
* Room assignments cannot be changed in edit mode, so they are sent back as BusPro reports
|
||||||
* booking edits while maintaining the constraint that participants can only be
|
* them on the booking reloaded at submit, together with its anzahl. Rebuilding them from the
|
||||||
* assigned to room types that have already been booked.
|
* session would undo any reassignment made in BusPro while the edit was open, and send a
|
||||||
|
* zuordnung that no longer matches anzahl. Canceled participants are left out, as for every
|
||||||
|
* other service.
|
||||||
*
|
*
|
||||||
* @param ParticipantDto $participant The participant data from the form
|
* @param Booking $bookingData The booking data object to update
|
||||||
* @param Booking $bookingData The booking data object to update
|
|
||||||
*/
|
*/
|
||||||
private function processRoomAssignment(ParticipantDto $participant, Booking $bookingData): void
|
public function removeCanceledParticipantsFromRooms(Booking $bookingData): void
|
||||||
{
|
{
|
||||||
if (null === $participant->assignedRoomId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the room in the existing booking by ID
|
|
||||||
foreach ($bookingData->rooms as $room) {
|
foreach ($bookingData->rooms as $room) {
|
||||||
if ($room->id === $participant->assignedRoomId) {
|
$room->mapping = array_values(array_filter(
|
||||||
$room->mapping[] = $participant->index;
|
$room->mapping,
|
||||||
break;
|
fn (int $index) => 'S' !== ($bookingData->participantsStatus[$index] ?? null)
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,17 @@ class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandle
|
|||||||
return 'assignedRoomId';
|
return 'assignedRoomId';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips edit mode, where room assignments cannot be changed.
|
||||||
|
*
|
||||||
|
* The field is static text there, so a submitted value can only come from a replayed or
|
||||||
|
* hand-crafted POST. The edit request sends the booking's own assignments anyway.
|
||||||
|
*/
|
||||||
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
|
{
|
||||||
|
return BookingDto::MODE_EDIT !== $mode && parent::shouldProcess($submittedData, $mode, $participantIndex);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the assignedRoomId field for a specific participant.
|
* Processes the assignedRoomId field for a specific participant.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class BookingEditDraftMerger
|
|||||||
|
|
||||||
// Room assignment
|
// Room assignment
|
||||||
if (true === isset($data['roomAssignment']) && true === is_array($data['roomAssignment'])) {
|
if (true === isset($data['roomAssignment']) && true === is_array($data['roomAssignment'])) {
|
||||||
$this->applyRoomAssignment($participant, $data['roomAssignment'], $travel);
|
$this->applyRoomAssignment($participant, $data['roomAssignment']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// License plate
|
// License plate
|
||||||
@@ -177,26 +177,19 @@ class BookingEditDraftMerger
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the drafted room assignment to the participant.
|
* Applies the drafted room remarks to the participant.
|
||||||
*
|
*
|
||||||
* The room id uses the same merge strategy as single-select services: it is only
|
* The drafted assignedRoomId is ignored. Edit mode renders the room as static text and
|
||||||
* applied when it resolves to a room of the current travel. A draft may therefore move
|
* never submits it, so the draft only holds what BusPro reported when it was saved, and
|
||||||
* a participant to another room, but never unassign one. Edit mode renders the room as
|
* replaying it would undo a reassignment the office made since. The room always comes
|
||||||
* static text and never submits assignedRoomId, so a null in the draft is only ever an
|
* from the booking as loaded.
|
||||||
* 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.
|
* remarksRoom keeps overwrite semantics: it is free text the user can deliberately clear.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $data
|
* @param array<string, mixed> $data
|
||||||
*/
|
*/
|
||||||
private function applyRoomAssignment(ParticipantDto $participant, array $data, Travel $travel): void
|
private function applyRoomAssignment(ParticipantDto $participant, array $data): void
|
||||||
{
|
{
|
||||||
$assignedRoomId = $data['assignedRoomId'] ?? null;
|
|
||||||
if (null !== $assignedRoomId && null !== $travel->getRoomById((int) $assignedRoomId)) {
|
|
||||||
$participant->assignedRoomId = (int) $assignedRoomId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (true === array_key_exists('remarksRoom', $data)) {
|
if (true === array_key_exists('remarksRoom', $data)) {
|
||||||
$participant->remarksRoom = $data['remarksRoom'];
|
$participant->remarksRoom = $data['remarksRoom'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,6 +342,66 @@ class BookingDataProcessorTest extends TestCase
|
|||||||
$this->assertEquals('07.01.2024', $room['@abreise']);
|
$this->assertEquals('07.01.2024', $room['@abreise']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testUpdatePayloadSendsBookedRoomsUnchanged(): void
|
||||||
|
{
|
||||||
|
$formData = $this->createFormDataWithBookedRooms();
|
||||||
|
|
||||||
|
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
[95 => ['1', '1,2,3,4'], 3 => ['3', '5,6,7,8,9,10']],
|
||||||
|
$this->roomsInPayload($result)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdatePayloadDropsCanceledParticipantsFromRooms(): void
|
||||||
|
{
|
||||||
|
$formData = $this->createFormDataWithBookedRooms();
|
||||||
|
$formData->participants[5]->status = 'S';
|
||||||
|
$formData->booking->participantsStatus[5] = 'S';
|
||||||
|
|
||||||
|
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
[95 => ['1', '1,2,3,4'], 3 => ['3', '5,7,8,9,10']],
|
||||||
|
$this->roomsInPayload($result)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdatePayloadKeepsRoomChangesMadeInBusProDuringTheEdit(): void
|
||||||
|
{
|
||||||
|
$formData = $this->createFormDataWithBookedRooms();
|
||||||
|
|
||||||
|
// The office moved participant 4 into the doubles after the edit session was loaded
|
||||||
|
$formData->booking->rooms[95]->mapping = [0, 1, 2];
|
||||||
|
$formData->booking->rooms[3]->totalCount = 4;
|
||||||
|
$formData->booking->rooms[3]->mapping = [3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
|
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
[95 => ['1', '1,2,3'], 3 => ['4', '4,5,6,7,8,9,10']],
|
||||||
|
$this->roomsInPayload($result)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpdatePayloadKeepsARoomTypeAddedInBusProDuringTheEdit(): void
|
||||||
|
{
|
||||||
|
$formData = $this->createFormDataWithBookedRooms();
|
||||||
|
|
||||||
|
// The office moved participants 1 and 2 out of the 4-bed room into a new double
|
||||||
|
$formData->booking->rooms[95]->mapping = [2, 3];
|
||||||
|
$formData->booking->rooms[95]->totalCount = 1;
|
||||||
|
$formData->booking->rooms[7] = $this->createBookedRoom(7, 'DZ', 1, [0, 1]);
|
||||||
|
|
||||||
|
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
[95 => ['1', '3,4'], 3 => ['3', '5,6,7,8,9,10'], 7 => ['1', '1,2']],
|
||||||
|
$this->roomsInPayload($result)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testCommunicationObjectCreation(): void
|
public function testCommunicationObjectCreation(): void
|
||||||
{
|
{
|
||||||
$formData = $this->createFormDataWithoutExistingCommunication();
|
$formData = $this->createFormDataWithoutExistingCommunication();
|
||||||
@@ -526,6 +586,60 @@ class BookingDataProcessorTest extends TestCase
|
|||||||
return $formData;
|
return $formData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ten participants as BusPro reports them: four in one 4-bed room, six in three doubles.
|
||||||
|
*/
|
||||||
|
private function createFormDataWithBookedRooms(): BookingDto
|
||||||
|
{
|
||||||
|
$bookingDto = new BookingDto($this->createMockTravel(), 1);
|
||||||
|
$bookingDto->booking = $this->createMockBooking();
|
||||||
|
$bookingDto->booking->participants = [];
|
||||||
|
$bookingDto->booking->participantsStatus = [];
|
||||||
|
$bookingDto->participants = [];
|
||||||
|
|
||||||
|
for ($index = 0; $index < 10; ++$index) {
|
||||||
|
$bookingDto->booking->participants[$index] = $this->createMockPersonalData("Participant{$index}");
|
||||||
|
$bookingDto->booking->participantsStatus[$index] = 'F';
|
||||||
|
$bookingDto->participants[$index] = $this->createMockParticipantDto($index, 'F');
|
||||||
|
$bookingDto->participants[$index]->assignedRoomId = $index < 4 ? 95 : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bookingDto->booking->rooms = [
|
||||||
|
95 => $this->createBookedRoom(95, '4er', 1, [0, 1, 2, 3]),
|
||||||
|
3 => $this->createBookedRoom(3, 'DZ', 3, [4, 5, 6, 7, 8, 9]),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $bookingDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<int> $mapping */
|
||||||
|
private function createBookedRoom(int $id, string $category, int $totalCount, array $mapping): Room
|
||||||
|
{
|
||||||
|
$room = new Room();
|
||||||
|
$room->id = $id;
|
||||||
|
$room->category = $category;
|
||||||
|
$room->boardId = 2;
|
||||||
|
$room->totalCount = $totalCount;
|
||||||
|
$room->mapping = $mapping;
|
||||||
|
|
||||||
|
return $room;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*
|
||||||
|
* @return array<int, array{string, string}> Room id => [anzahl, zuordnung]
|
||||||
|
*/
|
||||||
|
private function roomsInPayload(array $payload): array
|
||||||
|
{
|
||||||
|
$rooms = [];
|
||||||
|
foreach ($payload['ferienzielunterbringungen']['ferienzielunterbringung'] as $room) {
|
||||||
|
$rooms[$room['@idzimmer']] = [(string) $room['@anzahl'], $room['@zuordnung']];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rooms;
|
||||||
|
}
|
||||||
|
|
||||||
private function createFormDataWithoutExistingCommunication(): BookingDto
|
private function createFormDataWithoutExistingCommunication(): BookingDto
|
||||||
{
|
{
|
||||||
$formData = $this->createCompleteFormData();
|
$formData = $this->createCompleteFormData();
|
||||||
|
|||||||
@@ -33,7 +33,12 @@ class ParticipantAssignedRoomFieldHandlerTest extends TestCase
|
|||||||
public function testShouldProcessReturnsTrueWhenFieldPresent(): void
|
public function testShouldProcessReturnsTrueWhenFieldPresent(): void
|
||||||
{
|
{
|
||||||
$this->assertTrue($this->handler->shouldProcess(['assignedRoomId' => '1'], BookingDto::MODE_CREATE, 0));
|
$this->assertTrue($this->handler->shouldProcess(['assignedRoomId' => '1'], BookingDto::MODE_CREATE, 0));
|
||||||
$this->assertTrue($this->handler->shouldProcess(['assignedRoomId' => ''], BookingDto::MODE_EDIT, 5));
|
$this->assertTrue($this->handler->shouldProcess(['assignedRoomId' => ''], BookingDto::MODE_CREATE, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldProcessReturnsFalseInEditMode(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->handler->shouldProcess(['assignedRoomId' => '1'], BookingDto::MODE_EDIT, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testShouldProcessReturnsFalseWhenFieldNotPresent(): void
|
public function testShouldProcessReturnsFalseWhenFieldNotPresent(): void
|
||||||
|
|||||||
@@ -13,13 +13,12 @@ use App\Service\ServiceAvailabilityCalculator;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests that draft restoration never unassigns a room.
|
* Tests that draft restoration never changes a room assignment.
|
||||||
*
|
*
|
||||||
* Edit mode renders the room as static text and never submits assignedRoomId, so a null in
|
* Edit mode renders the room as static text and never submits assignedRoomId, so the draft
|
||||||
* the draft payload is always an artefact of a snapshot taken before BusPro reported the
|
* only holds what BusPro reported when it was saved: a null for seats added to a group booking
|
||||||
* assignment - typically for seats added to a group booking shortly before the draft was
|
* shortly before, or a room the office has since reassigned. The room always comes from the
|
||||||
* saved. Replaying such a null would strip the room from the UI and from the outbound
|
* booking as loaded; only the free-text remarks are restored.
|
||||||
* zuordnung, so the room id uses the same merge strategy as single-select services.
|
|
||||||
*/
|
*/
|
||||||
class BookingEditDraftMergerRoomAssignmentTest extends TestCase
|
class BookingEditDraftMergerRoomAssignmentTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -43,7 +42,7 @@ class BookingEditDraftMergerRoomAssignmentTest extends TestCase
|
|||||||
$this->assertSame(74, $participant->assignedRoomId);
|
$this->assertSame(74, $participant->assignedRoomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDraftAppliesRoomThatResolvesAgainstTravel(): void
|
public function testDraftDoesNotMoveParticipantToAnotherRoom(): void
|
||||||
{
|
{
|
||||||
$participant = new ParticipantDto();
|
$participant = new ParticipantDto();
|
||||||
$participant->mutable = true;
|
$participant->mutable = true;
|
||||||
@@ -56,7 +55,7 @@ class BookingEditDraftMergerRoomAssignmentTest extends TestCase
|
|||||||
|
|
||||||
$this->apply($travel, $participant, ['assignedRoomId' => 75]);
|
$this->apply($travel, $participant, ['assignedRoomId' => 75]);
|
||||||
|
|
||||||
$this->assertSame(75, $participant->assignedRoomId);
|
$this->assertSame(74, $participant->assignedRoomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDraftSkipsRoomThatIsUnknownToTravel(): void
|
public function testDraftSkipsRoomThatIsUnknownToTravel(): void
|
||||||
|
|||||||
Reference in new issue
Block a user