feat: automatically assign rooms only for unique room type selection
This commit is contained in:
Generated
+12
-12
@@ -10450,28 +10450,28 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "webmozart/assert",
|
"name": "webmozart/assert",
|
||||||
"version": "1.11.0",
|
"version": "1.12.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/webmozarts/assert.git",
|
"url": "https://github.com/webmozarts/assert.git",
|
||||||
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991"
|
"reference": "541057574806f942c94662b817a50f63f7345360"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991",
|
"url": "https://api.github.com/repos/webmozarts/assert/zipball/541057574806f942c94662b817a50f63f7345360",
|
||||||
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991",
|
"reference": "541057574806f942c94662b817a50f63f7345360",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
|
"ext-date": "*",
|
||||||
|
"ext-filter": "*",
|
||||||
"php": "^7.2 || ^8.0"
|
"php": "^7.2 || ^8.0"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"suggest": {
|
||||||
"phpstan/phpstan": "<0.12.20",
|
"ext-intl": "",
|
||||||
"vimeo/psalm": "<4.6.1 || 4.6.2"
|
"ext-simplexml": "",
|
||||||
},
|
"ext-spl": ""
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^8.5.13"
|
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
@@ -10502,9 +10502,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/webmozarts/assert/issues",
|
"issues": "https://github.com/webmozarts/assert/issues",
|
||||||
"source": "https://github.com/webmozarts/assert/tree/1.11.0"
|
"source": "https://github.com/webmozarts/assert/tree/1.12.0"
|
||||||
},
|
},
|
||||||
"time": "2022-06-03T18:03:27+00:00"
|
"time": "2025-10-20T12:43:39+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
|
|||||||
@@ -254,6 +254,10 @@ class Step2Controller extends AbstractController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically assigns participants to rooms if they don't have room assignments yet.
|
* Automatically assigns participants to rooms if they don't have room assignments yet.
|
||||||
|
*
|
||||||
|
* Note: Auto-assignment only occurs when exactly one room type is selected.
|
||||||
|
* With multiple room types, users must manually select rooms to avoid UX issues
|
||||||
|
* with having to unselect preassigned rooms in individual participant forms.
|
||||||
*/
|
*/
|
||||||
private function autoAssignRoomsIfNeeded(BookingDto $bookingCreateDto): void
|
private function autoAssignRoomsIfNeeded(BookingDto $bookingCreateDto): void
|
||||||
{
|
{
|
||||||
@@ -270,5 +274,4 @@ class Step2Controller extends AbstractController
|
|||||||
$this->roomAssignmentService->assignParticipantsToRooms($bookingCreateDto);
|
$this->roomAssignmentService->assignParticipantsToRooms($bookingCreateDto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ class ParticipantCardDataService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate and format individual participant price.
|
* Calculate and format individual participant price.
|
||||||
|
*
|
||||||
|
* Returns a dash (-) when the price is zero and no room is assigned,
|
||||||
|
* indicating incomplete configuration rather than a zero-cost booking.
|
||||||
*/
|
*/
|
||||||
private function getFormattedPrice(BookingDto $bookingDto, int $index): string
|
private function getFormattedPrice(BookingDto $bookingDto, int $index): string
|
||||||
{
|
{
|
||||||
@@ -110,6 +113,12 @@ class ParticipantCardDataService
|
|||||||
|
|
||||||
$price = $prices[$index] ?? 0.0;
|
$price = $prices[$index] ?? 0.0;
|
||||||
|
|
||||||
|
// Display dash when price is zero and no room assigned (incomplete configuration)
|
||||||
|
$participant = $bookingDto->participants[$index] ?? null;
|
||||||
|
if (0.0 === $price && (null === $participant || null === $participant->assignedRoomId)) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
return number_format($price, 2, ',', '.').' €';
|
return number_format($price, 2, ',', '.').' €';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,30 @@ use App\Form\Model\BookingDto;
|
|||||||
*/
|
*/
|
||||||
class RoomAssignmentService
|
class RoomAssignmentService
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Determines if automatic room assignment should be performed.
|
||||||
|
*
|
||||||
|
* Auto-assignment is only performed when exactly one room type is selected to avoid
|
||||||
|
* UX issues with individual participant forms. With multiple room types, users should
|
||||||
|
* manually select rooms to avoid having to unselect preassigned rooms.
|
||||||
|
*
|
||||||
|
* @param BookingDto $dto The booking DTO containing room selections
|
||||||
|
*
|
||||||
|
* @return bool True if auto-assignment should proceed, false otherwise
|
||||||
|
*/
|
||||||
|
public function shouldAutoAssignRooms(BookingDto $dto): bool
|
||||||
|
{
|
||||||
|
$selectedRoomTypeCount = 0;
|
||||||
|
|
||||||
|
foreach ($dto->roomSelections as $roomSelection) {
|
||||||
|
if (null !== $roomSelection->quantity && $roomSelection->quantity > 0) {
|
||||||
|
++$selectedRoomTypeCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1 === $selectedRoomTypeCount;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically assigns participants to rooms based on selected room quantities and capacities.
|
* Automatically assigns participants to rooms based on selected room quantities and capacities.
|
||||||
*
|
*
|
||||||
@@ -26,10 +50,17 @@ class RoomAssignmentService
|
|||||||
* - 2x "Doppelzimmer" (capacity 2) = participants 0-1 → room A, participants 2-3 → room A
|
* - 2x "Doppelzimmer" (capacity 2) = participants 0-1 → room A, participants 2-3 → room A
|
||||||
* - 1x "3-Bett-Zimmer" (capacity 3) = participants 4-6 → room B
|
* - 1x "3-Bett-Zimmer" (capacity 3) = participants 4-6 → room B
|
||||||
*
|
*
|
||||||
|
* Note: Only performs assignment if shouldAutoAssignRooms() returns true.
|
||||||
|
*
|
||||||
* @param BookingDto $dto The booking DTO containing room selections and participants
|
* @param BookingDto $dto The booking DTO containing room selections and participants
|
||||||
*/
|
*/
|
||||||
public function assignParticipantsToRooms(BookingDto $dto): void
|
public function assignParticipantsToRooms(BookingDto $dto): void
|
||||||
{
|
{
|
||||||
|
// Skip auto-assignment if multiple room types selected
|
||||||
|
if (false === $this->shouldAutoAssignRooms($dto)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$participantIndex = 0;
|
$participantIndex = 0;
|
||||||
$selectedRooms = $dto->getSelectedRooms();
|
$selectedRooms = $dto->getSelectedRooms();
|
||||||
$availableRooms = $dto->travel->getAvailableRooms();
|
$availableRooms = $dto->travel->getAvailableRooms();
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ class ParticipantCardDataServiceTest extends TestCase
|
|||||||
$this->assertEquals('Unbekanntes Zimmer', $result['roomName']);
|
$this->assertEquals('Unbekanntes Zimmer', $result['roomName']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetCardDataWithZeroPrice(): void
|
public function testGetCardDataWithZeroPriceAndNoRoom(): void
|
||||||
{
|
{
|
||||||
$travel = new Travel();
|
$travel = new Travel();
|
||||||
$travel->rooms = [];
|
$travel->rooms = [];
|
||||||
@@ -175,6 +175,7 @@ class ParticipantCardDataServiceTest extends TestCase
|
|||||||
$participant = new ParticipantDto();
|
$participant = new ParticipantDto();
|
||||||
$participant->firstName = 'Max';
|
$participant->firstName = 'Max';
|
||||||
$participant->lastName = 'Mustermann';
|
$participant->lastName = 'Mustermann';
|
||||||
|
$participant->assignedRoomId = null; // No room assigned
|
||||||
|
|
||||||
$bookingDto = new BookingDto($travel, 1);
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
$bookingDto->participants = [$participant];
|
$bookingDto->participants = [$participant];
|
||||||
@@ -185,6 +186,36 @@ class ParticipantCardDataServiceTest extends TestCase
|
|||||||
|
|
||||||
$result = $this->service->getCardData($bookingDto, 0);
|
$result = $this->service->getCardData($bookingDto, 0);
|
||||||
|
|
||||||
|
// When no room assigned and price is zero, display dash (incomplete configuration)
|
||||||
|
$this->assertEquals('-', $result['price']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCardDataWithZeroPriceButRoomAssigned(): void
|
||||||
|
{
|
||||||
|
// Create test room
|
||||||
|
$room = new Room();
|
||||||
|
$room->id = 1;
|
||||||
|
$room->label = 'Doppelzimmer';
|
||||||
|
$room->price = 0.0;
|
||||||
|
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [$room];
|
||||||
|
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->firstName = 'Max';
|
||||||
|
$participant->lastName = 'Mustermann';
|
||||||
|
$participant->assignedRoomId = 1; // Room is assigned
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant];
|
||||||
|
|
||||||
|
$this->priceCalculator
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->willReturn([0.0]);
|
||||||
|
|
||||||
|
$result = $this->service->getCardData($bookingDto, 0);
|
||||||
|
|
||||||
|
// When room is assigned but price is zero, display formatted zero price
|
||||||
$this->assertEquals('0,00 €', $result['price']);
|
$this->assertEquals('0,00 €', $result['price']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Constants;
|
||||||
|
use App\BusProNet\Model\Room;
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Model\ParticipantDto;
|
||||||
|
use App\Form\Model\RoomSelectionDto;
|
||||||
|
use App\Service\RoomAssignmentService;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for RoomAssignmentService.
|
||||||
|
*/
|
||||||
|
class RoomAssignmentServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private RoomAssignmentService $service;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->service = new RoomAssignmentService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function shouldAutoAssignRoomsReturnsTrueForSingleRoomType(): void
|
||||||
|
{
|
||||||
|
$bookingDto = $this->createBookingDtoWithRoomSelections([
|
||||||
|
['roomId' => 1, 'quantity' => 3],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->service->shouldAutoAssignRooms($bookingDto);
|
||||||
|
|
||||||
|
self::assertTrue($result, 'Should auto-assign when exactly one room type is selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function shouldAutoAssignRoomsReturnsFalseForMultipleRoomTypes(): void
|
||||||
|
{
|
||||||
|
$bookingDto = $this->createBookingDtoWithRoomSelections([
|
||||||
|
['roomId' => 1, 'quantity' => 2],
|
||||||
|
['roomId' => 2, 'quantity' => 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->service->shouldAutoAssignRooms($bookingDto);
|
||||||
|
|
||||||
|
self::assertFalse($result, 'Should not auto-assign when multiple room types are selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function shouldAutoAssignRoomsReturnsFalseForNoRoomSelections(): void
|
||||||
|
{
|
||||||
|
$bookingDto = $this->createBookingDtoWithRoomSelections([]);
|
||||||
|
|
||||||
|
$result = $this->service->shouldAutoAssignRooms($bookingDto);
|
||||||
|
|
||||||
|
self::assertFalse($result, 'Should not auto-assign when no rooms are selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function shouldAutoAssignRoomsIgnoresZeroQuantityRooms(): void
|
||||||
|
{
|
||||||
|
$bookingDto = $this->createBookingDtoWithRoomSelections([
|
||||||
|
['roomId' => 1, 'quantity' => 2],
|
||||||
|
['roomId' => 2, 'quantity' => 0],
|
||||||
|
['roomId' => 3, 'quantity' => null],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->service->shouldAutoAssignRooms($bookingDto);
|
||||||
|
|
||||||
|
self::assertTrue($result, 'Should treat zero/null quantity as not selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function assignParticipantsToRoomsWorksWithSingleRoomType(): void
|
||||||
|
{
|
||||||
|
$bookingDto = $this->createBookingDtoWithRoomSelections([
|
||||||
|
['roomId' => 100, 'quantity' => 2, 'capacity' => 2],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 4 participants (2 rooms × 2 capacity)
|
||||||
|
$bookingDto->participants = [
|
||||||
|
new ParticipantDto(),
|
||||||
|
new ParticipantDto(),
|
||||||
|
new ParticipantDto(),
|
||||||
|
new ParticipantDto(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock available room
|
||||||
|
$room = new Room();
|
||||||
|
$room->id = 100;
|
||||||
|
$room->minPax = 2;
|
||||||
|
$room->available = 10;
|
||||||
|
$room->status = Constants::STATUS_AVAILABLE;
|
||||||
|
|
||||||
|
$bookingDto->travel->rooms = [$room];
|
||||||
|
|
||||||
|
$this->service->assignParticipantsToRooms($bookingDto);
|
||||||
|
|
||||||
|
// Verify all participants got assigned
|
||||||
|
self::assertSame(100, $bookingDto->participants[0]->assignedRoomId);
|
||||||
|
self::assertSame(100, $bookingDto->participants[1]->assignedRoomId);
|
||||||
|
self::assertSame(100, $bookingDto->participants[2]->assignedRoomId);
|
||||||
|
self::assertSame(100, $bookingDto->participants[3]->assignedRoomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function assignParticipantsToRoomsSkipsAssignmentWithMultipleRoomTypes(): void
|
||||||
|
{
|
||||||
|
$bookingDto = $this->createBookingDtoWithRoomSelections([
|
||||||
|
['roomId' => 100, 'quantity' => 1, 'capacity' => 2],
|
||||||
|
['roomId' => 200, 'quantity' => 1, 'capacity' => 2],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 4 participants (2 rooms × 2 capacity)
|
||||||
|
$bookingDto->participants = [
|
||||||
|
new ParticipantDto(),
|
||||||
|
new ParticipantDto(),
|
||||||
|
new ParticipantDto(),
|
||||||
|
new ParticipantDto(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock available rooms
|
||||||
|
$room1 = new Room();
|
||||||
|
$room1->id = 100;
|
||||||
|
$room1->minPax = 2;
|
||||||
|
$room1->available = 10;
|
||||||
|
$room1->status = Constants::STATUS_AVAILABLE;
|
||||||
|
|
||||||
|
$room2 = new Room();
|
||||||
|
$room2->id = 200;
|
||||||
|
$room2->minPax = 2;
|
||||||
|
$room2->available = 10;
|
||||||
|
$room2->status = Constants::STATUS_AVAILABLE;
|
||||||
|
|
||||||
|
$bookingDto->travel->rooms = [$room1, $room2];
|
||||||
|
|
||||||
|
$this->service->assignParticipantsToRooms($bookingDto);
|
||||||
|
|
||||||
|
// Verify NO participants got assigned (conditional logic skipped assignment)
|
||||||
|
self::assertNull($bookingDto->participants[0]->assignedRoomId);
|
||||||
|
self::assertNull($bookingDto->participants[1]->assignedRoomId);
|
||||||
|
self::assertNull($bookingDto->participants[2]->assignedRoomId);
|
||||||
|
self::assertNull($bookingDto->participants[3]->assignedRoomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function assignParticipantsToRoomsHandlesMixedRoomCapacities(): void
|
||||||
|
{
|
||||||
|
$bookingDto = $this->createBookingDtoWithRoomSelections([
|
||||||
|
['roomId' => 100, 'quantity' => 1, 'capacity' => 2],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create participants
|
||||||
|
$bookingDto->participants = [
|
||||||
|
new ParticipantDto(),
|
||||||
|
new ParticipantDto(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock available room with capacity 2
|
||||||
|
$room = new Room();
|
||||||
|
$room->id = 100;
|
||||||
|
$room->minPax = 2;
|
||||||
|
$room->available = 10;
|
||||||
|
$room->status = Constants::STATUS_AVAILABLE;
|
||||||
|
|
||||||
|
$bookingDto->travel->rooms = [$room];
|
||||||
|
|
||||||
|
$this->service->assignParticipantsToRooms($bookingDto);
|
||||||
|
|
||||||
|
// Both participants assigned to same room
|
||||||
|
self::assertSame(100, $bookingDto->participants[0]->assignedRoomId);
|
||||||
|
self::assertSame(100, $bookingDto->participants[1]->assignedRoomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createBookingDtoWithRoomSelections(array $selections): BookingDto
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [];
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->roomSelections = [];
|
||||||
|
|
||||||
|
foreach ($selections as $selection) {
|
||||||
|
$roomSelectionDto = new RoomSelectionDto();
|
||||||
|
$roomSelectionDto->roomId = $selection['roomId'];
|
||||||
|
$roomSelectionDto->quantity = $selection['quantity'];
|
||||||
|
if (isset($selection['capacity'])) {
|
||||||
|
$roomSelectionDto->capacity = $selection['capacity'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bookingDto->roomSelections[] = $roomSelectionDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bookingDto;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user