diff --git a/docs/service-simplification-plan.md b/docs/service-simplification-plan.md index a897fe5..3114782 100644 --- a/docs/service-simplification-plan.md +++ b/docs/service-simplification-plan.md @@ -18,7 +18,7 @@ The emphasis is not on deleting services for its own sake. The emphasis is on: The codebase is already in a better place than it was at the start of the refactor, but a few services still carry more than one responsibility: - `BookingService` no longer owns session lifecycle, baseline snapshot handling, return URL management, or room grouping. That work now lives in `BookingSessionService` and `BookingRoomSelectionService`, which keeps the booking orchestration boundary narrower. -- `BookingService` still covers hydration, booking bootstrap, participant counting for participant auto-fill, service preselection, and booking status rules. +- `BookingService` still covers hydration, booking bootstrap, service preselection, and booking status rules. - `BookingPriceCalculatorService` is focused on pricing, but it still sits close to display-oriented behavior in adjacent code paths. - `TravelDataService` remains broad and is likely the next larger boundary after booking orchestration is reduced. @@ -104,7 +104,7 @@ Likely directions, only if justified later: |------|--------|-------| | Participant card DTO cleanup | Done | Card data now uses typed DTOs instead of nested array payloads | | Room label formatting cleanup | Done | Pricing labels now have a dedicated presentation helper | -| Booking service split | In progress | Session lifecycle, baseline snapshot, return URL handling, and room grouping moved out of `BookingService` | +| Booking service split | In progress | Session lifecycle, baseline snapshot, return URL handling, room grouping, and participant count shaping moved out of `BookingService` | | Pricing service review | Pending | Keep focused on calculation, not rendering | | Travel data service review | Pending | Broad boundary, likely later pass | | Participant field registry review | Deferred | Real orchestration boundary, intentionally left alone for now | diff --git a/src/Controller/Booking/Create/Step2Controller.php b/src/Controller/Booking/Create/Step2Controller.php index bea6155..2b1d0c1 100644 --- a/src/Controller/Booking/Create/Step2Controller.php +++ b/src/Controller/Booking/Create/Step2Controller.php @@ -9,6 +9,7 @@ use App\Controller\Booking\Traits\BookingExceptionHandlerTrait; use App\Form\BookingCreateStep2Type; use App\Form\Model\BookingDto; use App\Service\BookingService; +use App\Service\BookingParticipantCountService; use App\Service\BookingSessionService; use App\Service\BookingSummaryDataService; use App\Service\ParticipantCardDataService; @@ -33,6 +34,7 @@ class Step2Controller extends AbstractController public function __construct( private readonly BookingService $bookingService, + private readonly BookingParticipantCountService $participantCountService, private readonly BookingSessionService $bookingSessionService, private readonly BookingSummaryDataService $summaryDataService, private readonly TravelDataService $travelDataService, @@ -64,7 +66,7 @@ class Step2Controller extends AbstractController $this->travelDataService->enrichWithFreshAvailabilities($bookingCreateDto->travel); // Ensure correct number of participants with prepopulation callback - $this->bookingService->ensureCorrectNumberOfParticipants( + $this->participantCountService->ensureCorrectNumberOfParticipants( $bookingCreateDto, $this->getUser(), fn ($user, $participant) => $this->prepopulationService->prepopulateApplicantFromUser($user, $participant) diff --git a/src/Service/BookingParticipantCountService.php b/src/Service/BookingParticipantCountService.php new file mode 100644 index 0000000..30ad222 --- /dev/null +++ b/src/Service/BookingParticipantCountService.php @@ -0,0 +1,74 @@ +calculateParticipantsCount($bookingDto->roomSelections, $bookingDto->travel); + + $existingParticipants = $bookingDto->participants; + $bookingDto->participants = []; + + for ($i = 0; $i < $participantsCount; ++$i) { + $participant = $existingParticipants[$i] ?? new ParticipantDto(); + $participant->index = $i; + + // Prepopulate applicant from authenticated user (index 0 only) + if (0 === $i && null !== $user && null !== $prepopulateCallback && $this->shouldPrepopulate($participant)) { + $participant = $prepopulateCallback($user, $participant); + } + + $bookingDto->participants[$i] = $participant; + } + } + + /** + * Calculates the total number of participants based on room selections. + * + * Multiplies each room's minimum occupancy (minPax) by the selected quantity + * to determine the total number of participants required for the booking. + * + * @param array $roomSelections Array of room selection DTOs + * @param Travel $travelData Travel data containing room information + */ + private function calculateParticipantsCount(array $roomSelections, Travel $travelData): int + { + $participantsCount = 0; + $rooms = $travelData->getAvailableRooms(); + + foreach ($roomSelections as $roomSelection) { + $room = $rooms[$roomSelection->id]; + $participantsCount += $room->minPax * $roomSelection->quantity; + } + + return $participantsCount; + } + + /** + * Only prepopulate if the participant is fresh. + */ + private function shouldPrepopulate(ParticipantDto $participant): bool + { + return null === $participant->firstName || '' === $participant->firstName; + } +} diff --git a/src/Service/BookingService.php b/src/Service/BookingService.php index 18095e1..c232755 100644 --- a/src/Service/BookingService.php +++ b/src/Service/BookingService.php @@ -660,60 +660,4 @@ class BookingService * @param \Symfony\Component\Security\Core\User\UserInterface|null $user Optional authenticated user for prepopulation * @param callable|null $prepopulateCallback Callback to prepopulate applicant: fn(UserInterface, ParticipantDto): ParticipantDto */ - public function ensureCorrectNumberOfParticipants( - BookingDto $bookingDto, - ?\Symfony\Component\Security\Core\User\UserInterface $user = null, - ?callable $prepopulateCallback = null, - ): void { - $participantsCount = $this->calculateParticipantsCount($bookingDto->roomSelections, $bookingDto->travel); - - $existingParticipants = $bookingDto->participants; - $bookingDto->participants = []; - - for ($i = 0; $i < $participantsCount; ++$i) { - $participant = $existingParticipants[$i] ?? new ParticipantDto(); - $participant->index = $i; - - // Prepopulate applicant from authenticated user (index 0 only) - if (0 === $i && null !== $user && null !== $prepopulateCallback && $this->shouldPrepopulate($participant)) { - $participant = $prepopulateCallback($user, $participant); - } - - $bookingDto->participants[$i] = $participant; - } - } - - /** - * Determines if a participant should be prepopulated. - * - * Only prepopulates if the participant is "fresh" (no name set yet). - */ - private function shouldPrepopulate(ParticipantDto $participant): bool - { - return null === $participant->firstName || '' === $participant->firstName; - } - - /** - * Calculates the total number of participants based on room selections. - * - * Multiplies each room's minimum occupancy (minPax) by the selected quantity - * to determine the total number of participants required for the booking. - * - * @param array $roomSelections Array of RoomSelectionDto objects - * @param Travel $travelData Travel data containing room information - * - * @return int Total number of participants required - */ - private function calculateParticipantsCount(array $roomSelections, Travel $travelData): int - { - $participantsCount = 0; - $rooms = $travelData->getAvailableRooms(); - - foreach ($roomSelections as $roomSelection) { - $room = $rooms[$roomSelection->id]; - $participantsCount += $room->minPax * $roomSelection->quantity; - } - - return $participantsCount; - } } diff --git a/tests/Service/BookingParticipantCountServiceTest.php b/tests/Service/BookingParticipantCountServiceTest.php new file mode 100644 index 0000000..c7c693f --- /dev/null +++ b/tests/Service/BookingParticipantCountServiceTest.php @@ -0,0 +1,103 @@ +service = new BookingParticipantCountService(); + } + + public function testEnsureCorrectNumberOfParticipantsExpandsToCalculatedCount(): void + { + $travel = $this->createTravel([ + 1 => 2, + 2 => 3, + ]); + + $bookingDto = new BookingDto($travel, 123); + $bookingDto->roomSelections = [ + $this->createRoomSelection(1, 1), + $this->createRoomSelection(2, 2), + ]; + $bookingDto->participants = [new ParticipantDto()]; + + $this->service->ensureCorrectNumberOfParticipants($bookingDto); + + $this->assertCount(8, $bookingDto->participants); + $this->assertSame(0, $bookingDto->participants[0]->index); + $this->assertSame(7, $bookingDto->participants[7]->index); + } + + public function testEnsureCorrectNumberOfParticipantsPrepopulatesFreshApplicant(): void + { + $travel = $this->createTravel([1 => 2]); + $bookingDto = new BookingDto($travel, 123); + $bookingDto->roomSelections = [$this->createRoomSelection(1, 1)]; + $bookingDto->participants = [new ParticipantDto()]; + + $user = $this->createMock(UserInterface::class); + $callbackCalled = false; + + $this->service->ensureCorrectNumberOfParticipants( + $bookingDto, + $user, + function (UserInterface $userArg, ParticipantDto $participant) use (&$callbackCalled): ParticipantDto { + $callbackCalled = true; + $participant->firstName = 'Alex'; + + return $participant; + } + ); + + $this->assertTrue($callbackCalled); + $this->assertSame('Alex', $bookingDto->participants[0]->firstName); + } + + private function createTravel(array $roomCapacities): Travel + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $rooms = []; + foreach ($roomCapacities as $id => $minPax) { + $room = new Room(); + $room->id = $id; + $room->label = 'Room '.$id; + $room->minPax = $minPax; + $room->maxPax = $minPax + 1; + $room->available = 5; + $room->status = Constants::STATUS_AVAILABLE; + $rooms[$id] = $room; + } + + $travel->rooms = $rooms; + + return $travel; + } + + private function createRoomSelection(int $id, int $quantity): RoomSelectionDto + { + $selection = new RoomSelectionDto(); + $selection->id = $id; + $selection->quantity = $quantity; + + return $selection; + } +}