feat: extract booking participant count service
This commit is contained in:
@@ -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:
|
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` 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.
|
- `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.
|
- `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 |
|
| 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 |
|
| 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 |
|
| Pricing service review | Pending | Keep focused on calculation, not rendering |
|
||||||
| Travel data service review | Pending | Broad boundary, likely later pass |
|
| Travel data service review | Pending | Broad boundary, likely later pass |
|
||||||
| Participant field registry review | Deferred | Real orchestration boundary, intentionally left alone for now |
|
| Participant field registry review | Deferred | Real orchestration boundary, intentionally left alone for now |
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
|
|||||||
use App\Form\BookingCreateStep2Type;
|
use App\Form\BookingCreateStep2Type;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Service\BookingService;
|
use App\Service\BookingService;
|
||||||
|
use App\Service\BookingParticipantCountService;
|
||||||
use App\Service\BookingSessionService;
|
use App\Service\BookingSessionService;
|
||||||
use App\Service\BookingSummaryDataService;
|
use App\Service\BookingSummaryDataService;
|
||||||
use App\Service\ParticipantCardDataService;
|
use App\Service\ParticipantCardDataService;
|
||||||
@@ -33,6 +34,7 @@ class Step2Controller extends AbstractController
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly BookingService $bookingService,
|
private readonly BookingService $bookingService,
|
||||||
|
private readonly BookingParticipantCountService $participantCountService,
|
||||||
private readonly BookingSessionService $bookingSessionService,
|
private readonly BookingSessionService $bookingSessionService,
|
||||||
private readonly BookingSummaryDataService $summaryDataService,
|
private readonly BookingSummaryDataService $summaryDataService,
|
||||||
private readonly TravelDataService $travelDataService,
|
private readonly TravelDataService $travelDataService,
|
||||||
@@ -64,7 +66,7 @@ class Step2Controller extends AbstractController
|
|||||||
$this->travelDataService->enrichWithFreshAvailabilities($bookingCreateDto->travel);
|
$this->travelDataService->enrichWithFreshAvailabilities($bookingCreateDto->travel);
|
||||||
|
|
||||||
// Ensure correct number of participants with prepopulation callback
|
// Ensure correct number of participants with prepopulation callback
|
||||||
$this->bookingService->ensureCorrectNumberOfParticipants(
|
$this->participantCountService->ensureCorrectNumberOfParticipants(
|
||||||
$bookingCreateDto,
|
$bookingCreateDto,
|
||||||
$this->getUser(),
|
$this->getUser(),
|
||||||
fn ($user, $participant) => $this->prepopulationService->prepopulateApplicantFromUser($user, $participant)
|
fn ($user, $participant) => $this->prepopulationService->prepopulateApplicantFromUser($user, $participant)
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Model\ParticipantDto;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps participant-count shaping separate from booking orchestration.
|
||||||
|
*/
|
||||||
|
class BookingParticipantCountService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Ensures the booking DTO has the expected number of participant objects.
|
||||||
|
*
|
||||||
|
* @param callable|null $prepopulateCallback fn(UserInterface, ParticipantDto): ParticipantDto
|
||||||
|
*/
|
||||||
|
public function ensureCorrectNumberOfParticipants(
|
||||||
|
BookingDto $bookingDto,
|
||||||
|
?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -660,60 +660,4 @@ class BookingService
|
|||||||
* @param \Symfony\Component\Security\Core\User\UserInterface|null $user Optional authenticated user for prepopulation
|
* @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
|
* @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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Room;
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\BusProNet\Constants;
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Model\ParticipantDto;
|
||||||
|
use App\Form\Model\RoomSelectionDto;
|
||||||
|
use App\Service\BookingParticipantCountService;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
|
class BookingParticipantCountServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private BookingParticipantCountService $service;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user