feat: consolidate micro-services and unify draft merge logic
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Service Simplification Plan
|
# Service Simplification Plan
|
||||||
|
|
||||||
Status: draft
|
Status: active
|
||||||
Last updated: 2026-04-13
|
Last updated: 2026-04-13
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
@@ -17,9 +17,10 @@ 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` now mostly covers booking bootstrap, service preselection, and booking status rules.
|
||||||
- `BookingService` still covers hydration, booking bootstrap, service preselection, and booking status rules.
|
- `BookingParticipantCountService` was removed — its logic lives as a private method in the one controller that needed it (`Step2Controller`).
|
||||||
- `BookingParticipantCountService` now handles only participant count shaping.
|
- `BookingRoomSelectionService` was removed — its logic lives as a private method in `BookingCreateContextFactory`.
|
||||||
|
- `BookingSummaryParticipantCountService` was removed — its logic lives as a private method in `BookingSummaryDataService`.
|
||||||
- `ParticipantPrepopulationService` now owns applicant prefill plus the create-mode dummy-data shortcut.
|
- `ParticipantPrepopulationService` now owns applicant prefill plus the create-mode dummy-data shortcut.
|
||||||
- `BookingEditParticipantContextFactory` now prepares the edit participant page context directly, replacing the older pass-through participant form service.
|
- `BookingEditParticipantContextFactory` now prepares the edit participant page context directly, replacing the older pass-through participant form service.
|
||||||
- `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.
|
||||||
@@ -32,22 +33,7 @@ It is not just a lookup table. It owns execution order, edit-mode mutability gat
|
|||||||
|
|
||||||
## Next Pass
|
## Next Pass
|
||||||
|
|
||||||
### 1. Reduce `BookingService`
|
### 1. Keep pricing calculation focused
|
||||||
|
|
||||||
Primary goal: make the booking create/edit flow easier to read by splitting unrelated concerns.
|
|
||||||
|
|
||||||
Concrete next steps:
|
|
||||||
- keep booking session lifecycle in one place
|
|
||||||
- extract baseline room snapshot handling into a narrower helper or dedicated service
|
|
||||||
- separate return URL handling if it stays conceptually unrelated
|
|
||||||
- keep `startFreshBooking()` focused on booking bootstrap rather than general session utilities
|
|
||||||
- keep hydration behavior obvious and local to the booking session path
|
|
||||||
|
|
||||||
Decision rule:
|
|
||||||
- if a method only forwards to DTO/session behavior, prefer removing the wrapper
|
|
||||||
- if a method is a genuine workflow owner, keep it and narrow the surrounding API instead of splitting it into generic helpers
|
|
||||||
|
|
||||||
### 2. Keep pricing calculation focused
|
|
||||||
|
|
||||||
Primary goal: keep pricing code about pricing, not rendering.
|
Primary goal: keep pricing code about pricing, not rendering.
|
||||||
|
|
||||||
@@ -60,63 +46,7 @@ Concrete next steps:
|
|||||||
Decision rule:
|
Decision rule:
|
||||||
- if a value is only needed for display, prefer exposing the raw numeric/domain value and formatting it as close to the UI as possible
|
- if a value is only needed for display, prefer exposing the raw numeric/domain value and formatting it as close to the UI as possible
|
||||||
|
|
||||||
### 3. Leave the field-handler registry in place
|
### 2. Leave the field-handler registry in place
|
||||||
|
|
||||||
Primary goal: avoid unnecessary churn in a class that is already a meaningful orchestration layer.
|
|
||||||
|
|
||||||
Concrete next steps:
|
|
||||||
- do not refactor `ParticipantFieldHandlerRegistry` in this pass
|
|
||||||
- revisit only if a later change can split ordering, mutability, and synchronization into clear collaborators without making the flow harder to trace
|
|
||||||
|
|
||||||
Decision rule:
|
|
||||||
- if a registry owns actual workflow behavior, treat it as a boundary rather than a smell
|
|
||||||
|
|
||||||
## Implementation Backlog
|
|
||||||
|
|
||||||
### 1. Keep the summary count contract explicit
|
|
||||||
|
|
||||||
Goal: make the booking summary read clearly without duplicating equivalent count fields.
|
|
||||||
|
|
||||||
Tasks:
|
|
||||||
- use a single summary-facing count field for the sidebar and step summary views
|
|
||||||
- keep the room-capacity-derived meaning explicit in the field name and docblock
|
|
||||||
- keep the participant-shaping count logic separate if the code still needs it internally
|
|
||||||
- remove template branching that compares two equivalent summary counts
|
|
||||||
|
|
||||||
Acceptance criteria:
|
|
||||||
- the summary template reads one count field, not two equivalent ones
|
|
||||||
- the field name makes the room-capacity meaning obvious to a new developer
|
|
||||||
- participant-shaping logic can still use its own internal count without leaking that distinction into the view layer
|
|
||||||
|
|
||||||
### 2. Reduce `BookingService`
|
|
||||||
|
|
||||||
Primary goal: make the booking create/edit flow easier to read by splitting unrelated concerns.
|
|
||||||
|
|
||||||
Concrete next steps:
|
|
||||||
- keep booking session lifecycle in one place
|
|
||||||
- extract baseline room snapshot handling into a narrower helper or dedicated service
|
|
||||||
- separate return URL handling if it stays conceptually unrelated
|
|
||||||
- keep `startFreshBooking()` focused on booking bootstrap rather than general session utilities
|
|
||||||
- keep hydration behavior obvious and local to the booking session path
|
|
||||||
|
|
||||||
Decision rule:
|
|
||||||
- if a method only forwards to DTO/session behavior, prefer removing the wrapper
|
|
||||||
- if a method is a genuine workflow owner, keep it and narrow the surrounding API instead of splitting it into generic helpers
|
|
||||||
|
|
||||||
### 3. Keep pricing calculation focused
|
|
||||||
|
|
||||||
Primary goal: keep pricing code about pricing, not rendering.
|
|
||||||
|
|
||||||
Concrete next steps:
|
|
||||||
- keep `BookingPriceCalculatorService` as the pricing boundary
|
|
||||||
- continue removing display formatting from pricing code paths
|
|
||||||
- keep any remaining view-specific formatting in the presentation layer or a dedicated UI helper
|
|
||||||
- avoid introducing another service that only formats values already known to the view
|
|
||||||
|
|
||||||
Decision rule:
|
|
||||||
- if a value is only needed for display, prefer exposing the raw numeric/domain value and formatting it as close to the UI as possible
|
|
||||||
|
|
||||||
### 4. Leave the field-handler registry in place
|
|
||||||
|
|
||||||
Primary goal: avoid unnecessary churn in a class that is already a meaningful orchestration layer.
|
Primary goal: avoid unnecessary churn in a class that is already a meaningful orchestration layer.
|
||||||
|
|
||||||
@@ -157,13 +87,21 @@ Likely directions, only if justified later:
|
|||||||
- split synchronization code if a clearer DTO/form boundary emerges
|
- split synchronization code if a clearer DTO/form boundary emerges
|
||||||
- otherwise leave it as the central orchestration point for participant field processing
|
- otherwise leave it as the central orchestration point for participant field processing
|
||||||
|
|
||||||
|
## Current Notes
|
||||||
|
|
||||||
|
- `templates/booking/_summary_travel_info.html.twig` already renders the summary participant count directly, so the remaining work here is naming and contract clarity rather than Twig branching.
|
||||||
|
- The summary count still needs a clear name if the code should distinguish the display-oriented count from the canonical participant total in the DTO/service layer.
|
||||||
|
- `ParticipantFormSupportService` was reviewed and kept: it is shared between two controllers (`Step2ParticipantController` and `Edit/ParticipantController`) with real shared logic, not just delegation.
|
||||||
|
|
||||||
## Progress Tracker
|
## Progress Tracker
|
||||||
|
|
||||||
| Item | Status | Notes |
|
| Item | Status | Notes |
|
||||||
|------|--------|-------|
|
|------|--------|-------|
|
||||||
| 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, room grouping, and participant count shaping moved out of `BookingService`; dummy prefill moved into `ParticipantPrepopulationService` |
|
| Booking service split | Done | Session lifecycle, hydration, baseline snapshot, return URL handling, room grouping, and participant count shaping moved out of `BookingService` |
|
||||||
|
| Dummy prefill extraction | Done | Moved into `ParticipantPrepopulationService` |
|
||||||
|
| Micro-service consolidation | Done | `BookingRoomSelectionService`, `BookingParticipantCountService`, `BookingSummaryParticipantCountService` inlined into their single callers |
|
||||||
| 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 |
|
||||||
@@ -174,7 +112,7 @@ The next booking-service pass is only worth keeping if it:
|
|||||||
- reduces the number of unrelated responsibilities in `BookingService`
|
- reduces the number of unrelated responsibilities in `BookingService`
|
||||||
- makes the booking flow easier to trace from controller to session/DTO state
|
- makes the booking flow easier to trace from controller to session/DTO state
|
||||||
- preserves existing booking behavior and test coverage
|
- preserves existing booking behavior and test coverage
|
||||||
- does not replace one large service with several generic “manager” classes
|
- does not replace one large service with several generic "manager" classes
|
||||||
|
|
||||||
## Working Agreement
|
## Working Agreement
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Booking\Create;
|
namespace App\Controller\Booking\Create;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Controller\Booking\Traits\BookingCreateTrait;
|
use App\Controller\Booking\Traits\BookingCreateTrait;
|
||||||
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
|
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\Form\Model\ParticipantDto;
|
||||||
use App\Service\BookingCreateContextFactory;
|
use App\Service\BookingCreateContextFactory;
|
||||||
use App\Service\BookingService;
|
use App\Service\BookingConfigurator;
|
||||||
use App\Service\BookingParticipantCountService;
|
use App\Service\BookingSessionStore;
|
||||||
use App\Service\BookingSessionService;
|
use App\Service\ParticipantDataPrefiller;
|
||||||
use App\Service\ParticipantPrepopulationService;
|
use App\Service\RoomAssigner;
|
||||||
use App\Service\RoomAssignmentService;
|
use App\Service\TravelDataProvider;
|
||||||
use App\Service\TravelDataService;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -33,13 +34,12 @@ class Step2Controller extends AbstractController
|
|||||||
use BookingExceptionHandlerTrait;
|
use BookingExceptionHandlerTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly BookingService $bookingService,
|
private readonly BookingConfigurator $bookingService,
|
||||||
private readonly BookingParticipantCountService $participantCountService,
|
private readonly BookingSessionStore $bookingSessionService,
|
||||||
private readonly BookingSessionService $bookingSessionService,
|
|
||||||
private readonly BookingCreateContextFactory $createContextFactory,
|
private readonly BookingCreateContextFactory $createContextFactory,
|
||||||
private readonly TravelDataService $travelDataService,
|
private readonly TravelDataProvider $travelDataService,
|
||||||
private readonly RoomAssignmentService $roomAssignmentService,
|
private readonly RoomAssigner $roomAssignmentService,
|
||||||
private readonly ParticipantPrepopulationService $prepopulationService,
|
private readonly ParticipantDataPrefiller $prepopulationService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,13 +65,13 @@ class Step2Controller extends AbstractController
|
|||||||
$this->travelDataService->enrichWithFreshAvailabilities($bookingCreateDto->travel);
|
$this->travelDataService->enrichWithFreshAvailabilities($bookingCreateDto->travel);
|
||||||
|
|
||||||
// Ensure correct number of participants first, then prepopulate the applicant if needed.
|
// Ensure correct number of participants first, then prepopulate the applicant if needed.
|
||||||
$this->participantCountService->ensureCorrectNumberOfParticipants($bookingCreateDto);
|
$this->ensureCorrectNumberOfParticipants($bookingCreateDto);
|
||||||
|
|
||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
if ($user instanceof User
|
if ($user instanceof User
|
||||||
&& isset($bookingCreateDto->participants[0])
|
&& isset($bookingCreateDto->participants[0])
|
||||||
&& $this->prepopulationService->shouldPrepopulateApplicant($bookingCreateDto->participants[0])) {
|
&& $this->prepopulationService->shouldPrefillApplicant($bookingCreateDto->participants[0])) {
|
||||||
$bookingCreateDto->participants[0] = $this->prepopulationService->prepopulateApplicantFromUser(
|
$bookingCreateDto->participants[0] = $this->prepopulationService->prefillApplicantFromUser(
|
||||||
$user,
|
$user,
|
||||||
$bookingCreateDto->participants[0]
|
$bookingCreateDto->participants[0]
|
||||||
);
|
);
|
||||||
@@ -114,4 +114,34 @@ class Step2Controller extends AbstractController
|
|||||||
return $this->render('booking/create/step_2.html.twig', $templateData);
|
return $this->render('booking/create/step_2.html.twig', $templateData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the booking DTO has the expected number of participant objects based on room selections.
|
||||||
|
*/
|
||||||
|
private function ensureCorrectNumberOfParticipants(BookingDto $bookingDto): 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;
|
||||||
|
|
||||||
|
$bookingDto->participants[$i] = $participant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Room;
|
||||||
use App\Form\Model\BookingCreateContext;
|
use App\Form\Model\BookingCreateContext;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\BookingSummaryDto;
|
use App\Form\Model\BookingSummaryDto;
|
||||||
@@ -15,10 +16,9 @@ use App\Form\Model\RoomGroupsDto;
|
|||||||
class BookingCreateContextFactory
|
class BookingCreateContextFactory
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly BookingRoomSelectionService $roomSelectionService,
|
private readonly ParticipantCardAssembler $participantCardDataService,
|
||||||
private readonly ParticipantCardDataService $participantCardDataService,
|
private readonly BookingSummaryAssembler $summaryDataService,
|
||||||
private readonly BookingSummaryDataService $summaryDataService,
|
private readonly BookingPriceCalculator $priceCalculator,
|
||||||
private readonly BookingPriceCalculatorService $priceCalculator,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +68,35 @@ class BookingCreateContextFactory
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'summaryData' => $this->summaryDataService->getSummaryData($bookingDto, $pricingMode),
|
'summaryData' => $this->summaryDataService->getSummaryData($bookingDto, $pricingMode),
|
||||||
'groupedRooms' => $this->roomSelectionService->groupRoomsBySelectionType($bookingDto->travel->getAvailableRooms()),
|
'groupedRooms' => $this->groupRoomsBySelectionType($bookingDto->travel->getAvailableRooms()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, Room> $rooms
|
||||||
|
*/
|
||||||
|
private function groupRoomsBySelectionType(array $rooms): RoomGroupsDto
|
||||||
|
{
|
||||||
|
$groups = [
|
||||||
|
Room::SELECTION_TYPE_BY_PAX => [],
|
||||||
|
Room::SELECTION_TYPE_BY_ROOM => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($rooms as $room) {
|
||||||
|
if (false !== stripos($room->label, 'bett')) {
|
||||||
|
$groups[Room::SELECTION_TYPE_BY_PAX][$room->id] = $room;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups[Room::SELECTION_TYPE_BY_ROOM][$room->id] = $room;
|
||||||
|
}
|
||||||
|
|
||||||
|
uasort($groups[Room::SELECTION_TYPE_BY_PAX], static fn (Room $a, Room $b): int => $a->maxPax <=> $b->maxPax);
|
||||||
|
uasort($groups[Room::SELECTION_TYPE_BY_ROOM], static fn (Room $a, Room $b): int => $a->maxPax <=> $b->maxPax);
|
||||||
|
|
||||||
|
return new RoomGroupsDto(
|
||||||
|
byPax: $groups[Room::SELECTION_TYPE_BY_PAX],
|
||||||
|
byRoom: $groups[Room::SELECTION_TYPE_BY_ROOM],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Entity\BookingEditDraft;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Form\Model\BankAccountDto;
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Repository\BookingEditDraftRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages draft persistence for the booking edit flow.
|
||||||
|
*
|
||||||
|
* This service handles saving, loading, and applying draft data to prevent
|
||||||
|
* data loss when BusProNet API rejects booking update submissions. Drafts
|
||||||
|
* are automatically applied on top of fresh API data when a user returns
|
||||||
|
* to edit a booking.
|
||||||
|
*/
|
||||||
|
class BookingEditDraftManager
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly BookingEditDraftRepository $draftRepository,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly BookingChangeTracker $fingerprintService,
|
||||||
|
private readonly BookingEditDraftMerger $participantApplier,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an existing draft for a user and booking combination.
|
||||||
|
*
|
||||||
|
* @param User $user The user who owns the draft
|
||||||
|
* @param int $bookingId The BusProNet booking ID
|
||||||
|
*
|
||||||
|
* @return BookingEditDraft|null The draft if found, null otherwise
|
||||||
|
*/
|
||||||
|
public function findDraft(User $user, int $bookingId): ?BookingEditDraft
|
||||||
|
{
|
||||||
|
return $this->draftRepository->findByUserAndBooking($user, $bookingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves or updates a draft for the given booking.
|
||||||
|
*
|
||||||
|
* Uses an upsert pattern: creates a new draft if none exists,
|
||||||
|
* or updates the existing draft with new form data.
|
||||||
|
*
|
||||||
|
* @param User $user The user who owns the draft
|
||||||
|
* @param int $bookingId The BusProNet booking ID
|
||||||
|
* @param BookingDto $bookingDto The booking DTO containing user edits
|
||||||
|
*/
|
||||||
|
public function saveDraft(User $user, int $bookingId, BookingDto $bookingDto): void
|
||||||
|
{
|
||||||
|
$formData = $this->fingerprintService->extractUserData($bookingDto);
|
||||||
|
$travelDate = $bookingDto->travel->dateFrom;
|
||||||
|
$existingDraft = $this->findDraft($user, $bookingId);
|
||||||
|
|
||||||
|
$bookingNumber = $bookingDto->booking?->bookingNumber;
|
||||||
|
$dateId = $bookingDto->travel->id;
|
||||||
|
$hotelId = $bookingDto->travel->hotelId;
|
||||||
|
|
||||||
|
if (null !== $existingDraft) {
|
||||||
|
$existingDraft->setFormData($formData);
|
||||||
|
|
||||||
|
// Only set once — the draft must stay bound to the original departure date and
|
||||||
|
// hotel, so these identifiers are never overwritten once assigned.
|
||||||
|
if (null === $existingDraft->getBookingNumber() && null !== $bookingNumber) {
|
||||||
|
$existingDraft->setBookingNumber($bookingNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $existingDraft->getDateId() && null !== $dateId) {
|
||||||
|
$existingDraft->setDateId($dateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $existingDraft->getHotelId() && null !== $hotelId) {
|
||||||
|
$existingDraft->setHotelId($hotelId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$draft = new BookingEditDraft($user, $bookingId, $travelDate, $formData);
|
||||||
|
$draft->setBookingNumber($bookingNumber);
|
||||||
|
$draft->setDateId($dateId);
|
||||||
|
$draft->setHotelId($hotelId);
|
||||||
|
$this->entityManager->persist($draft);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
$this->logger->debug('Saved booking edit draft', [
|
||||||
|
'user_id' => $user->getId(),
|
||||||
|
'booking_id' => $bookingId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a draft after successful booking submission.
|
||||||
|
*
|
||||||
|
* @param User $user The user who owns the draft
|
||||||
|
* @param int $bookingId The BusProNet booking ID
|
||||||
|
*/
|
||||||
|
public function deleteDraft(User $user, int $bookingId): void
|
||||||
|
{
|
||||||
|
$this->draftRepository->deleteByUserAndBooking($user, $bookingId);
|
||||||
|
|
||||||
|
$this->logger->debug('Deleted booking edit draft', [
|
||||||
|
'user_id' => $user->getId(),
|
||||||
|
'booking_id' => $bookingId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies draft data to a fresh BookingDto loaded from the API.
|
||||||
|
*
|
||||||
|
* This method merges user input from the draft onto fresh API data.
|
||||||
|
* The draft contains personal data, service selections, and payment info
|
||||||
|
* that the user previously entered. Services are resolved by ID against
|
||||||
|
* the current Travel data to ensure prices and availability are current.
|
||||||
|
*
|
||||||
|
* @param BookingEditDraft $draft The draft containing saved user data
|
||||||
|
* @param BookingDto $dto The fresh BookingDto from API (modified in place)
|
||||||
|
* @param Travel $travel The current travel data for service resolution
|
||||||
|
*
|
||||||
|
* @return bool True if draft was applied successfully, false if draft data was invalid
|
||||||
|
*/
|
||||||
|
public function applyDraftToDto(BookingEditDraft $draft, BookingDto $dto, Travel $travel): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$formData = $draft->getFormData();
|
||||||
|
|
||||||
|
// Apply payment data
|
||||||
|
if (isset($formData['paymentMethod'])) {
|
||||||
|
$dto->paymentMethod = $formData['paymentMethod'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($formData['bankAccount']) && true === is_array($formData['bankAccount'])) {
|
||||||
|
$this->applyBankAccountData($dto, $formData['bankAccount']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply participant data (personal fields, body dimensions, room assignment,
|
||||||
|
// vouchers, and service selections — all gated by per-participant and travel mutability)
|
||||||
|
if (isset($formData['participants']) && true === is_array($formData['participants'])) {
|
||||||
|
foreach ($formData['participants'] as $index => $participantData) {
|
||||||
|
if (false === isset($dto->participants[$index])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->participantApplier->apply(
|
||||||
|
$dto,
|
||||||
|
$index,
|
||||||
|
$dto->participants[$index],
|
||||||
|
$participantData,
|
||||||
|
$travel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->info('Applied draft to booking DTO', [
|
||||||
|
'booking_id' => $draft->getBookingId(),
|
||||||
|
'draft_created_at' => $draft->getCreatedAt()->format('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logger->error('Failed to apply draft to booking DTO', [
|
||||||
|
'booking_id' => $draft->getBookingId(),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies bank account data from draft to BookingDto.
|
||||||
|
*/
|
||||||
|
private function applyBankAccountData(BookingDto $dto, array $bankAccountData): void
|
||||||
|
{
|
||||||
|
$iban = $bankAccountData['iban'] ?? null;
|
||||||
|
$accountHolder = $bankAccountData['accountHolder'] ?? null;
|
||||||
|
|
||||||
|
if (null === $iban && null === $accountHolder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $dto->bankAccount) {
|
||||||
|
$dto->bankAccount = new BankAccountDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
$dto->bankAccount->iban = $iban;
|
||||||
|
$dto->bankAccount->accountHolder = $accountHolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
+138
-175
@@ -5,196 +5,165 @@ declare(strict_types=1);
|
|||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
use App\Entity\BookingEditDraft;
|
|
||||||
use App\Entity\User;
|
|
||||||
use App\Form\Model\BankAccountDto;
|
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Repository\BookingEditDraftRepository;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages draft persistence for the booking edit flow.
|
* Applies participant-level draft payloads onto booking DTO participants.
|
||||||
*
|
*
|
||||||
* This service handles saving, loading, and applying draft data to prevent
|
* Handles all per-participant draft fields: personal data, address, body dimensions,
|
||||||
* data loss when BusProNet API rejects booking update submissions. Drafts
|
* room assignment, license plate, vouchers, and service selections.
|
||||||
* are automatically applied on top of fresh API data when a user returns
|
*
|
||||||
* to edit a booking.
|
* 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.
|
||||||
*/
|
*/
|
||||||
class BookingEditDraftService
|
class BookingEditDraftMerger
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function apply(
|
||||||
private readonly BookingEditDraftRepository $draftRepository,
|
BookingDto $bookingDto,
|
||||||
private readonly EntityManagerInterface $entityManager,
|
int $participantIndex,
|
||||||
private readonly BookingFingerprintService $fingerprintService,
|
ParticipantDto $participant,
|
||||||
private readonly BookingEditDraftParticipantApplier $participantApplier,
|
array $data,
|
||||||
private readonly LoggerInterface $logger,
|
Travel $travel,
|
||||||
) {
|
): void {
|
||||||
}
|
$canApplyPersonalDataDraft = $this->canApplyPersonalDataDraft($bookingDto, $participantIndex, $participant);
|
||||||
|
|
||||||
/**
|
// Personal data
|
||||||
* Finds an existing draft for a user and booking combination.
|
if (true === $canApplyPersonalDataDraft && isset($data['personalData']) && true === is_array($data['personalData'])) {
|
||||||
*
|
$this->applyPersonalData($participant, $data['personalData']);
|
||||||
* @param User $user The user who owns the draft
|
|
||||||
* @param int $bookingId The BusProNet booking ID
|
|
||||||
*
|
|
||||||
* @return BookingEditDraft|null The draft if found, null otherwise
|
|
||||||
*/
|
|
||||||
public function findDraft(User $user, int $bookingId): ?BookingEditDraft
|
|
||||||
{
|
|
||||||
return $this->draftRepository->findByUserAndBooking($user, $bookingId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves or updates a draft for the given booking.
|
|
||||||
*
|
|
||||||
* Uses an upsert pattern: creates a new draft if none exists,
|
|
||||||
* or updates the existing draft with new form data.
|
|
||||||
*
|
|
||||||
* @param User $user The user who owns the draft
|
|
||||||
* @param int $bookingId The BusProNet booking ID
|
|
||||||
* @param BookingDto $bookingDto The booking DTO containing user edits
|
|
||||||
*/
|
|
||||||
public function saveDraft(User $user, int $bookingId, BookingDto $bookingDto): void
|
|
||||||
{
|
|
||||||
$formData = $this->fingerprintService->extractUserData($bookingDto);
|
|
||||||
$travelDate = $bookingDto->travel->dateFrom;
|
|
||||||
$existingDraft = $this->findDraft($user, $bookingId);
|
|
||||||
|
|
||||||
$bookingNumber = $bookingDto->booking?->bookingNumber;
|
|
||||||
$dateId = $bookingDto->travel->id;
|
|
||||||
$hotelId = $bookingDto->travel->hotelId;
|
|
||||||
|
|
||||||
if (null !== $existingDraft) {
|
|
||||||
$existingDraft->setFormData($formData);
|
|
||||||
|
|
||||||
if (null === $existingDraft->getBookingNumber() && null !== $bookingNumber) {
|
|
||||||
$existingDraft->setBookingNumber($bookingNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $existingDraft->getDateId() && null !== $dateId) {
|
|
||||||
$existingDraft->setDateId($dateId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $existingDraft->getHotelId() && null !== $hotelId) {
|
|
||||||
$existingDraft->setHotelId($hotelId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$draft = new BookingEditDraft($user, $bookingId, $travelDate, $formData);
|
|
||||||
$draft->setBookingNumber($bookingNumber);
|
|
||||||
$draft->setDateId($dateId);
|
|
||||||
$draft->setHotelId($hotelId);
|
|
||||||
$this->entityManager->persist($draft);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->entityManager->flush();
|
// Address
|
||||||
|
if (true === $canApplyPersonalDataDraft && isset($data['address']) && true === is_array($data['address'])) {
|
||||||
|
$this->applyAddressData($participant, $data['address']);
|
||||||
|
}
|
||||||
|
|
||||||
$this->logger->debug('Saved booking edit draft', [
|
// Body dimensions
|
||||||
'user_id' => $user->getId(),
|
if (true === isset($data['bodyDimensions']) && true === is_array($data['bodyDimensions'])) {
|
||||||
'booking_id' => $bookingId,
|
$this->applyBodyDimensions($participant, $data['bodyDimensions']);
|
||||||
]);
|
}
|
||||||
|
|
||||||
|
// Room assignment
|
||||||
|
if (true === isset($data['roomAssignment']) && true === is_array($data['roomAssignment'])) {
|
||||||
|
$this->applyRoomAssignment($participant, $data['roomAssignment']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// License plate
|
||||||
|
if (true === array_key_exists('licensePlate', $data)) {
|
||||||
|
$participant->licensePlate = $data['licensePlate'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vouchers
|
||||||
|
if (true === isset($data['vouchers']) && true === is_array($data['vouchers'])) {
|
||||||
|
$this->applyVoucherCodes($participant, $data['vouchers']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service selections (gated per-category by travel mutability flags)
|
||||||
|
if (isset($data['services']) && true === is_array($data['services'])) {
|
||||||
|
$this->applyServiceSelections($participant, $data['services'], $travel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function canApplyPersonalDataDraft(BookingDto $bookingDto, int $participantIndex, ParticipantDto $participant): bool
|
||||||
* Deletes a draft after successful booking submission.
|
|
||||||
*
|
|
||||||
* @param User $user The user who owns the draft
|
|
||||||
* @param int $bookingId The BusProNet booking ID
|
|
||||||
*/
|
|
||||||
public function deleteDraft(User $user, int $bookingId): void
|
|
||||||
{
|
{
|
||||||
$this->draftRepository->deleteByUserAndBooking($user, $bookingId);
|
if (true === $bookingDto->isInternalAgencyBooking()) {
|
||||||
|
|
||||||
$this->logger->debug('Deleted booking edit draft', [
|
|
||||||
'user_id' => $user->getId(),
|
|
||||||
'booking_id' => $bookingId,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies draft data to a fresh BookingDto loaded from the API.
|
|
||||||
*
|
|
||||||
* This method merges user input from the draft onto fresh API data.
|
|
||||||
* The draft contains personal data, service selections, and payment info
|
|
||||||
* that the user previously entered. Services are resolved by ID against
|
|
||||||
* the current Travel data to ensure prices and availability are current.
|
|
||||||
*
|
|
||||||
* @param BookingEditDraft $draft The draft containing saved user data
|
|
||||||
* @param BookingDto $dto The fresh BookingDto from API (modified in place)
|
|
||||||
* @param Travel $travel The current travel data for service resolution
|
|
||||||
*
|
|
||||||
* @return bool True if draft was applied successfully, false if draft data was invalid
|
|
||||||
*/
|
|
||||||
public function applyDraftToDto(BookingEditDraft $draft, BookingDto $dto, Travel $travel): bool
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$formData = $draft->getFormData();
|
|
||||||
|
|
||||||
// Apply payment data
|
|
||||||
if (isset($formData['paymentMethod'])) {
|
|
||||||
$dto->paymentMethod = $formData['paymentMethod'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($formData['bankAccount']) && true === is_array($formData['bankAccount'])) {
|
|
||||||
$this->applyBankAccountData($dto, $formData['bankAccount']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply participant data
|
|
||||||
if (isset($formData['participants']) && true === is_array($formData['participants'])) {
|
|
||||||
foreach ($formData['participants'] as $index => $participantData) {
|
|
||||||
// Only apply to participants that exist in fresh DTO
|
|
||||||
if (false === isset($dto->participants[$index])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->participantApplier->apply(
|
|
||||||
$dto,
|
|
||||||
$index,
|
|
||||||
$dto->participants[$index],
|
|
||||||
$participantData,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isset($participantData['services']) && true === is_array($participantData['services'])) {
|
|
||||||
$this->applyServiceSelections($dto->participants[$index], $participantData['services'], $travel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->info('Applied draft to booking DTO', [
|
|
||||||
'booking_id' => $draft->getBookingId(),
|
|
||||||
'draft_created_at' => $draft->getCreatedAt()->format('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (\Throwable $e) {
|
}
|
||||||
$this->logger->error('Failed to apply draft to booking DTO', [
|
|
||||||
'booking_id' => $draft->getBookingId(),
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
if (0 === $participantIndex) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $participant->mutable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function applyPersonalData(ParticipantDto $participant, array $data): void
|
||||||
* Applies bank account data from draft to BookingDto.
|
|
||||||
*/
|
|
||||||
private function applyBankAccountData(BookingDto $dto, array $bankAccountData): void
|
|
||||||
{
|
{
|
||||||
$iban = $bankAccountData['iban'] ?? null;
|
if (true === array_key_exists('firstName', $data)) {
|
||||||
$accountHolder = $bankAccountData['accountHolder'] ?? null;
|
$participant->firstName = $data['firstName'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('lastName', $data)) {
|
||||||
|
$participant->lastName = $data['lastName'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('dateOfBirth', $data) && null !== $data['dateOfBirth']) {
|
||||||
|
$participant->dateOfBirth = new \DateTimeImmutable($data['dateOfBirth']);
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('email', $data)) {
|
||||||
|
$participant->email = $data['email'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('mobile', $data)) {
|
||||||
|
$participant->mobile = $data['mobile'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('gender', $data)) {
|
||||||
|
$participant->gender = $data['gender'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('nationality', $data) && '' !== $data['nationality'] && null !== $data['nationality']) {
|
||||||
|
$participant->nationality = $data['nationality'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (null === $iban && null === $accountHolder) {
|
private function applyAddressData(ParticipantDto $participant, array $data): void
|
||||||
|
{
|
||||||
|
$hasAddressData = null !== ($data['street'] ?? null)
|
||||||
|
|| null !== ($data['postCode'] ?? null)
|
||||||
|
|| null !== ($data['city'] ?? null)
|
||||||
|
|| null !== ($data['country'] ?? null);
|
||||||
|
|
||||||
|
if (false === $hasAddressData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $dto->bankAccount) {
|
if (null === $participant->address) {
|
||||||
$dto->bankAccount = new BankAccountDto();
|
$participant->address = new \App\BusProNet\Model\Address();
|
||||||
}
|
}
|
||||||
|
|
||||||
$dto->bankAccount->iban = $iban;
|
if (true === array_key_exists('street', $data)) {
|
||||||
$dto->bankAccount->accountHolder = $accountHolder;
|
$participant->address->street = $data['street'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('postCode', $data)) {
|
||||||
|
$participant->address->postCode = $data['postCode'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('city', $data)) {
|
||||||
|
$participant->address->city = $data['city'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('country', $data)) {
|
||||||
|
$participant->address->country = $data['country'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyBodyDimensions(ParticipantDto $participant, array $data): void
|
||||||
|
{
|
||||||
|
if (true === array_key_exists('height', $data)) {
|
||||||
|
$participant->height = $data['height'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('weight', $data)) {
|
||||||
|
$participant->weight = $data['weight'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('shoeSize', $data)) {
|
||||||
|
$participant->shoeSize = $data['shoeSize'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyRoomAssignment(ParticipantDto $participant, array $data): void
|
||||||
|
{
|
||||||
|
if (true === array_key_exists('assignedRoomId', $data)) {
|
||||||
|
$participant->assignedRoomId = $data['assignedRoomId'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('remarksRoom', $data)) {
|
||||||
|
$participant->remarksRoom = $data['remarksRoom'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyVoucherCodes(ParticipantDto $participant, array $data): void
|
||||||
|
{
|
||||||
|
if (true === array_key_exists('purchaseVoucherCode', $data)) {
|
||||||
|
$participant->purchaseVoucherCode = $data['purchaseVoucherCode'];
|
||||||
|
}
|
||||||
|
if (true === array_key_exists('promoVoucherCode', $data)) {
|
||||||
|
$participant->promoVoucherCode = $data['promoVoucherCode'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,9 +185,7 @@ class BookingEditDraftService
|
|||||||
if (true === $travel->additionalServicesMutable) {
|
if (true === $travel->additionalServicesMutable) {
|
||||||
// Ski pass (single service) - merge strategy: only apply if resolves to valid service
|
// Ski pass (single service) - merge strategy: only apply if resolves to valid service
|
||||||
if (true === array_key_exists('skiPass', $data) && null !== $data['skiPass']) {
|
if (true === array_key_exists('skiPass', $data) && null !== $data['skiPass']) {
|
||||||
$draftSkiPassId = $data['skiPass'];
|
$resolved = $this->resolveService($data['skiPass'], $travel->additionalServices);
|
||||||
$resolved = $this->resolveService($draftSkiPassId, $travel->additionalServices);
|
|
||||||
|
|
||||||
if (null !== $resolved) {
|
if (null !== $resolved) {
|
||||||
$participant->skiPass = $resolved;
|
$participant->skiPass = $resolved;
|
||||||
}
|
}
|
||||||
@@ -345,21 +312,16 @@ class BookingEditDraftService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add mandatory services from original API data that aren't in draft
|
// Add mandatory services from original API data that aren't in draft.
|
||||||
// Check mandatory status from travel data (pflicht attribute)
|
// Check mandatory status from travel data (pflicht attribute), since the
|
||||||
|
// booking entity itself does not carry the mandatory flag.
|
||||||
foreach ($originalServices as $service) {
|
foreach ($originalServices as $service) {
|
||||||
if (false === isset($draftServiceIds[$service->id])) {
|
if (false === isset($draftServiceIds[$service->id])) {
|
||||||
// Look up mandatory status from travel data
|
|
||||||
$travelService = $travel->additionalServices[$service->id] ?? null;
|
$travelService = $travel->additionalServices[$service->id] ?? null;
|
||||||
$isMandatory = null !== $travelService && true === $travelService->mandatory;
|
$isMandatory = null !== $travelService && true === $travelService->mandatory;
|
||||||
|
|
||||||
if ($isMandatory) {
|
if ($isMandatory) {
|
||||||
$draftServices[] = $service;
|
$draftServices[] = $service;
|
||||||
|
|
||||||
$this->logger->debug('Preserved mandatory service from API during draft application', [
|
|
||||||
'service_id' => $service->id,
|
|
||||||
'service_label' => $service->label,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +342,7 @@ class BookingEditDraftService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, int> $serviceIds
|
* @param array<int, int> $serviceIds
|
||||||
* @param array<int, object> $services
|
* @param array<int, object> $services
|
||||||
*
|
*
|
||||||
* @return array<int, object>
|
* @return array<int, object>
|
||||||
@@ -406,6 +368,7 @@ class BookingEditDraftService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pickups and drop-offs share the same ID space; a pickup ID may appear in either map
|
||||||
return $travel->pickups[$pickupId] ?? $travel->dropOffs[$pickupId] ?? null;
|
return $travel->pickups[$pickupId] ?? $travel->dropOffs[$pickupId] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Service;
|
|
||||||
|
|
||||||
use App\Form\Model\BookingDto;
|
|
||||||
use App\Form\Model\ParticipantDto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies participant-level draft payloads onto booking DTO participants.
|
|
||||||
*/
|
|
||||||
class BookingEditDraftParticipantApplier
|
|
||||||
{
|
|
||||||
public function apply(
|
|
||||||
BookingDto $bookingDto,
|
|
||||||
int $participantIndex,
|
|
||||||
ParticipantDto $participant,
|
|
||||||
array $data,
|
|
||||||
): void {
|
|
||||||
$canApplyPersonalDataDraft = $this->canApplyPersonalDataDraft($bookingDto, $participantIndex, $participant);
|
|
||||||
|
|
||||||
// Personal data
|
|
||||||
if (true === $canApplyPersonalDataDraft && isset($data['personalData']) && true === is_array($data['personalData'])) {
|
|
||||||
$this->applyPersonalData($participant, $data['personalData']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address
|
|
||||||
if (true === $canApplyPersonalDataDraft && isset($data['address']) && true === is_array($data['address'])) {
|
|
||||||
$this->applyAddressData($participant, $data['address']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Body dimensions
|
|
||||||
if (true === isset($data['bodyDimensions']) && true === is_array($data['bodyDimensions'])) {
|
|
||||||
$this->applyBodyDimensions($participant, $data['bodyDimensions']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Room assignment
|
|
||||||
if (true === isset($data['roomAssignment']) && true === is_array($data['roomAssignment'])) {
|
|
||||||
$this->applyRoomAssignment($participant, $data['roomAssignment']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// License plate
|
|
||||||
if (true === array_key_exists('licensePlate', $data)) {
|
|
||||||
$participant->licensePlate = $data['licensePlate'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vouchers
|
|
||||||
if (true === isset($data['vouchers']) && true === is_array($data['vouchers'])) {
|
|
||||||
$this->applyVoucherCodes($participant, $data['vouchers']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function canApplyPersonalDataDraft(BookingDto $bookingDto, int $participantIndex, ParticipantDto $participant): bool
|
|
||||||
{
|
|
||||||
if (true === $bookingDto->isInternalAgencyBooking()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (0 === $participantIndex) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $participant->mutable;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function applyPersonalData(ParticipantDto $participant, array $data): void
|
|
||||||
{
|
|
||||||
if (true === array_key_exists('firstName', $data)) {
|
|
||||||
$participant->firstName = $data['firstName'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('lastName', $data)) {
|
|
||||||
$participant->lastName = $data['lastName'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('dateOfBirth', $data) && null !== $data['dateOfBirth']) {
|
|
||||||
$participant->dateOfBirth = new \DateTimeImmutable($data['dateOfBirth']);
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('email', $data)) {
|
|
||||||
$participant->email = $data['email'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('mobile', $data)) {
|
|
||||||
$participant->mobile = $data['mobile'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('gender', $data)) {
|
|
||||||
$participant->gender = $data['gender'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('nationality', $data) && '' !== $data['nationality'] && null !== $data['nationality']) {
|
|
||||||
$participant->nationality = $data['nationality'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function applyAddressData(ParticipantDto $participant, array $data): void
|
|
||||||
{
|
|
||||||
$hasAddressData = null !== ($data['street'] ?? null)
|
|
||||||
|| null !== ($data['postCode'] ?? null)
|
|
||||||
|| null !== ($data['city'] ?? null)
|
|
||||||
|| null !== ($data['country'] ?? null);
|
|
||||||
|
|
||||||
if (false === $hasAddressData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $participant->address) {
|
|
||||||
$participant->address = new \App\BusProNet\Model\Address();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (true === array_key_exists('street', $data)) {
|
|
||||||
$participant->address->street = $data['street'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('postCode', $data)) {
|
|
||||||
$participant->address->postCode = $data['postCode'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('city', $data)) {
|
|
||||||
$participant->address->city = $data['city'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('country', $data)) {
|
|
||||||
$participant->address->country = $data['country'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function applyBodyDimensions(ParticipantDto $participant, array $data): void
|
|
||||||
{
|
|
||||||
if (true === array_key_exists('height', $data)) {
|
|
||||||
$participant->height = $data['height'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('weight', $data)) {
|
|
||||||
$participant->weight = $data['weight'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('shoeSize', $data)) {
|
|
||||||
$participant->shoeSize = $data['shoeSize'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function applyRoomAssignment(ParticipantDto $participant, array $data): void
|
|
||||||
{
|
|
||||||
if (true === array_key_exists('assignedRoomId', $data)) {
|
|
||||||
$participant->assignedRoomId = $data['assignedRoomId'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('remarksRoom', $data)) {
|
|
||||||
$participant->remarksRoom = $data['remarksRoom'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function applyVoucherCodes(ParticipantDto $participant, array $data): void
|
|
||||||
{
|
|
||||||
if (true === array_key_exists('purchaseVoucherCode', $data)) {
|
|
||||||
$participant->purchaseVoucherCode = $data['purchaseVoucherCode'];
|
|
||||||
}
|
|
||||||
if (true === array_key_exists('promoVoucherCode', $data)) {
|
|
||||||
$participant->promoVoucherCode = $data['promoVoucherCode'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Service;
|
|
||||||
|
|
||||||
use App\BusProNet\Model\Travel;
|
|
||||||
use App\Form\Model\BookingDto;
|
|
||||||
use App\Form\Model\ParticipantDto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keeps participant-count shaping separate from booking orchestration.
|
|
||||||
*/
|
|
||||||
class BookingParticipantCountService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Ensures the booking DTO has the expected number of participant objects.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public function ensureCorrectNumberOfParticipants(BookingDto $bookingDto): 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;
|
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Service;
|
|
||||||
|
|
||||||
use App\BusProNet\Model\Room;
|
|
||||||
use App\Form\Model\RoomGroupsDto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Groups room data for booking steps.
|
|
||||||
*/
|
|
||||||
class BookingRoomSelectionService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param array<int, Room> $rooms
|
|
||||||
*/
|
|
||||||
public function groupRoomsBySelectionType(array $rooms): RoomGroupsDto
|
|
||||||
{
|
|
||||||
$groups = [
|
|
||||||
Room::SELECTION_TYPE_BY_PAX => [],
|
|
||||||
Room::SELECTION_TYPE_BY_ROOM => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($rooms as $room) {
|
|
||||||
if (false !== stripos($room->label, 'bett')) {
|
|
||||||
$groups[Room::SELECTION_TYPE_BY_PAX][$room->id] = $room;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$groups[Room::SELECTION_TYPE_BY_ROOM][$room->id] = $room;
|
|
||||||
}
|
|
||||||
|
|
||||||
uasort($groups[Room::SELECTION_TYPE_BY_PAX], static fn (Room $a, Room $b): int => $a->maxPax <=> $b->maxPax);
|
|
||||||
uasort($groups[Room::SELECTION_TYPE_BY_ROOM], static fn (Room $a, Room $b): int => $a->maxPax <=> $b->maxPax);
|
|
||||||
|
|
||||||
return new RoomGroupsDto(
|
|
||||||
byPax: $groups[Room::SELECTION_TYPE_BY_PAX],
|
|
||||||
byRoom: $groups[Room::SELECTION_TYPE_BY_ROOM],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+28
-5
@@ -23,12 +23,11 @@ use Symfony\Contracts\Cache\ItemInterface;
|
|||||||
* Provides pricing breakdowns, room assignments, participant counts,
|
* Provides pricing breakdowns, room assignments, participant counts,
|
||||||
* and CMS product information in a single service.
|
* and CMS product information in a single service.
|
||||||
*/
|
*/
|
||||||
class BookingSummaryDataService
|
class BookingSummaryAssembler
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly BookingPriceCalculatorService $priceCalculator,
|
private readonly BookingPriceCalculator $priceCalculator,
|
||||||
private readonly BookingSummaryParticipantCountService $participantCountService,
|
private readonly CmsDataProvider $cmsDataService,
|
||||||
private readonly CmsDataService $cmsDataService,
|
|
||||||
private readonly HotelLoader $hotelLoader,
|
private readonly HotelLoader $hotelLoader,
|
||||||
private readonly CountryDataProvider $countryDataProvider,
|
private readonly CountryDataProvider $countryDataProvider,
|
||||||
private readonly CacheInterface $cache,
|
private readonly CacheInterface $cache,
|
||||||
@@ -75,7 +74,7 @@ class BookingSummaryDataService
|
|||||||
$cmsData = $this->getCmsDataForProduct($productCode, $hotelCode);
|
$cmsData = $this->getCmsDataForProduct($productCode, $hotelCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
$participantCount = $this->participantCountService->calculate($bookingDto);
|
$participantCount = $this->calculateParticipantCount($bookingDto);
|
||||||
|
|
||||||
$acceptedVouchers = $bookingDto->getAcceptedVouchers($participantPrices);
|
$acceptedVouchers = $bookingDto->getAcceptedVouchers($participantPrices);
|
||||||
|
|
||||||
@@ -177,4 +176,28 @@ class BookingSummaryDataService
|
|||||||
return [] === $parts ? null : implode("\n", $parts);
|
return [] === $parts ? null : implode("\n", $parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the participant count for the summary sidebar.
|
||||||
|
*
|
||||||
|
* In create step 1, derives the expected count from room selections.
|
||||||
|
* In later create steps and edit mode, uses the actual participant list.
|
||||||
|
*/
|
||||||
|
private function calculateParticipantCount(BookingDto $bookingDto): int
|
||||||
|
{
|
||||||
|
if (BookingDto::MODE_CREATE === $bookingDto->getMode() && 1 === $bookingDto->currentStep) {
|
||||||
|
$totalCapacity = 0;
|
||||||
|
$availableRooms = $bookingDto->travel->getAvailableRooms();
|
||||||
|
|
||||||
|
foreach ($bookingDto->roomSelections as $selection) {
|
||||||
|
if ($selection->quantity > 0 && isset($availableRooms[$selection->id])) {
|
||||||
|
$room = $availableRooms[$selection->id];
|
||||||
|
$totalCapacity += $selection->quantity * ($room->maxPax ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $totalCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count($bookingDto->participants);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Service;
|
|
||||||
|
|
||||||
use App\Form\Model\BookingDto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the participant count shown in the summary sidebar.
|
|
||||||
*
|
|
||||||
* In create step 1, derive the expected participant count from room selections.
|
|
||||||
* In later create steps and edit mode, use the actual participant list.
|
|
||||||
*/
|
|
||||||
class BookingSummaryParticipantCountService
|
|
||||||
{
|
|
||||||
public function calculate(BookingDto $bookingDto): int
|
|
||||||
{
|
|
||||||
if (BookingDto::MODE_CREATE === $bookingDto->getMode() && 1 === $bookingDto->currentStep) {
|
|
||||||
return $this->calculateExpectedParticipantCountFromRooms($bookingDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
return count($bookingDto->participants);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function calculateExpectedParticipantCountFromRooms(BookingDto $bookingDto): int
|
|
||||||
{
|
|
||||||
$totalCapacity = 0;
|
|
||||||
$availableRooms = $bookingDto->travel->getAvailableRooms();
|
|
||||||
|
|
||||||
foreach ($bookingDto->roomSelections as $selection) {
|
|
||||||
if ($selection->quantity > 0 && isset($availableRooms[$selection->id])) {
|
|
||||||
$room = $availableRooms[$selection->id];
|
|
||||||
$totalCapacity += $selection->quantity * ($room->maxPax ?? 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $totalCapacity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,15 +8,13 @@ use App\BusProNet\Model\Room;
|
|||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
use App\Form\Model\BookingCreateContext;
|
use App\Form\Model\BookingCreateContext;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\RoomGroupsDto;
|
|
||||||
use App\Form\Model\ParticipantCardDataDto;
|
use App\Form\Model\ParticipantCardDataDto;
|
||||||
use App\Form\Model\ParticipantCardPriceDto;
|
use App\Form\Model\ParticipantCardPriceDto;
|
||||||
use App\Form\Model\BookingSummaryDto;
|
use App\Form\Model\BookingSummaryDto;
|
||||||
use App\Service\BookingCreateContextFactory;
|
use App\Service\BookingCreateContextFactory;
|
||||||
use App\Service\BookingRoomSelectionService;
|
use App\Service\ParticipantCardAssembler;
|
||||||
use App\Service\ParticipantCardDataService;
|
use App\Service\BookingSummaryAssembler;
|
||||||
use App\Service\BookingSummaryDataService;
|
use App\Service\BookingPriceCalculator;
|
||||||
use App\Service\BookingPriceCalculatorService;
|
|
||||||
use App\Service\RoomPricingCalculator;
|
use App\Service\RoomPricingCalculator;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
@@ -47,7 +45,7 @@ class BookingCreateContextFactoryTest extends TestCase
|
|||||||
$bookingDto = new BookingDto($travel, 157047);
|
$bookingDto = new BookingDto($travel, 157047);
|
||||||
$summaryData = $this->createMock(BookingSummaryDto::class);
|
$summaryData = $this->createMock(BookingSummaryDto::class);
|
||||||
|
|
||||||
$summaryDataService = $this->createMock(BookingSummaryDataService::class);
|
$summaryDataService = $this->createMock(BookingSummaryAssembler::class);
|
||||||
$summaryDataService->expects($this->once())
|
$summaryDataService->expects($this->once())
|
||||||
->method('getSummaryData')
|
->method('getSummaryData')
|
||||||
->with(
|
->with(
|
||||||
@@ -56,26 +54,15 @@ class BookingCreateContextFactoryTest extends TestCase
|
|||||||
)
|
)
|
||||||
->willReturn($summaryData);
|
->willReturn($summaryData);
|
||||||
|
|
||||||
$roomSelectionService = $this->createMock(BookingRoomSelectionService::class);
|
$participantCardDataService = $this->createMock(ParticipantCardAssembler::class);
|
||||||
$groupedRooms = new RoomGroupsDto(
|
|
||||||
byPax: [$roomByPax->id => $roomByPax],
|
|
||||||
byRoom: [$roomByRoom->id => $roomByRoom],
|
|
||||||
);
|
|
||||||
$roomSelectionService->expects($this->once())
|
|
||||||
->method('groupRoomsBySelectionType')
|
|
||||||
->with([$roomByRoom->id => $roomByRoom, $roomByPax->id => $roomByPax])
|
|
||||||
->willReturn($groupedRooms);
|
|
||||||
|
|
||||||
$participantCardDataService = $this->createMock(ParticipantCardDataService::class);
|
|
||||||
$participantCardDataService->expects($this->never())
|
$participantCardDataService->expects($this->never())
|
||||||
->method('getAllCardsDataWithValidation');
|
->method('getAllCardsDataWithValidation');
|
||||||
|
|
||||||
$priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
|
$priceCalculator = $this->createMock(BookingPriceCalculator::class);
|
||||||
$priceCalculator->expects($this->never())
|
$priceCalculator->expects($this->never())
|
||||||
->method('calculateAllParticipantIndividualPrices');
|
->method('calculateAllParticipantIndividualPrices');
|
||||||
|
|
||||||
$service = new BookingCreateContextFactory(
|
$service = new BookingCreateContextFactory(
|
||||||
$roomSelectionService,
|
|
||||||
$participantCardDataService,
|
$participantCardDataService,
|
||||||
$summaryDataService,
|
$summaryDataService,
|
||||||
$priceCalculator,
|
$priceCalculator,
|
||||||
@@ -86,9 +73,10 @@ class BookingCreateContextFactoryTest extends TestCase
|
|||||||
$this->assertInstanceOf(BookingCreateContext::class, $context);
|
$this->assertInstanceOf(BookingCreateContext::class, $context);
|
||||||
$this->assertSame($bookingDto, $context->bookingDto);
|
$this->assertSame($bookingDto, $context->bookingDto);
|
||||||
$this->assertSame($summaryData, $context->summaryData);
|
$this->assertSame($summaryData, $context->summaryData);
|
||||||
$this->assertSame(null, $context->cardsData);
|
$this->assertNull($context->cardsData);
|
||||||
$this->assertFalse($context->isSubmitted);
|
$this->assertFalse($context->isSubmitted);
|
||||||
$this->assertSame($groupedRooms, $context->groupedRooms);
|
$this->assertSame([11 => $roomByPax], $context->groupedRooms->byPax);
|
||||||
|
$this->assertSame([10 => $roomByRoom], $context->groupedRooms->byRoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCreateWithParticipantCardsBuildsStep2Context(): void
|
public function testCreateWithParticipantCardsBuildsStep2Context(): void
|
||||||
@@ -116,34 +104,23 @@ class BookingCreateContextFactoryTest extends TestCase
|
|||||||
isCanceled: false,
|
isCanceled: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
$summaryDataService = $this->createMock(BookingSummaryDataService::class);
|
$summaryDataService = $this->createMock(BookingSummaryAssembler::class);
|
||||||
$summaryDataService->expects($this->once())
|
$summaryDataService->expects($this->once())
|
||||||
->method('getSummaryData')
|
->method('getSummaryData')
|
||||||
->with($bookingDto, RoomPricingCalculator::PRICING_MODE_SELECTION)
|
->with($bookingDto, RoomPricingCalculator::PRICING_MODE_SELECTION)
|
||||||
->willReturn($summaryData);
|
->willReturn($summaryData);
|
||||||
|
|
||||||
$roomSelectionService = $this->createMock(BookingRoomSelectionService::class);
|
$participantCardDataService = $this->createMock(ParticipantCardAssembler::class);
|
||||||
$groupedRooms = new RoomGroupsDto(
|
|
||||||
byPax: [],
|
|
||||||
byRoom: [$room->id => $room],
|
|
||||||
);
|
|
||||||
$roomSelectionService->expects($this->once())
|
|
||||||
->method('groupRoomsBySelectionType')
|
|
||||||
->with([$room->id => $room])
|
|
||||||
->willReturn($groupedRooms);
|
|
||||||
|
|
||||||
$participantCardDataService = $this->createMock(ParticipantCardDataService::class);
|
|
||||||
$participantCardDataService->expects($this->once())
|
$participantCardDataService->expects($this->once())
|
||||||
->method('getAllCardsDataWithValidation')
|
->method('getAllCardsDataWithValidation')
|
||||||
->with($bookingDto)
|
->with($bookingDto)
|
||||||
->willReturn([$cardData]);
|
->willReturn([$cardData]);
|
||||||
|
|
||||||
$priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
|
$priceCalculator = $this->createMock(BookingPriceCalculator::class);
|
||||||
$priceCalculator->expects($this->never())
|
$priceCalculator->expects($this->never())
|
||||||
->method('calculateAllParticipantIndividualPrices');
|
->method('calculateAllParticipantIndividualPrices');
|
||||||
|
|
||||||
$service = new BookingCreateContextFactory(
|
$service = new BookingCreateContextFactory(
|
||||||
$roomSelectionService,
|
|
||||||
$participantCardDataService,
|
$participantCardDataService,
|
||||||
$summaryDataService,
|
$summaryDataService,
|
||||||
$priceCalculator,
|
$priceCalculator,
|
||||||
@@ -154,6 +131,8 @@ class BookingCreateContextFactoryTest extends TestCase
|
|||||||
$this->assertInstanceOf(BookingCreateContext::class, $context);
|
$this->assertInstanceOf(BookingCreateContext::class, $context);
|
||||||
$this->assertSame([$cardData], $context->cardsData);
|
$this->assertSame([$cardData], $context->cardsData);
|
||||||
$this->assertTrue($context->isSubmitted);
|
$this->assertTrue($context->isSubmitted);
|
||||||
|
$this->assertSame([], $context->groupedRooms->byPax);
|
||||||
|
$this->assertSame([10 => $room], $context->groupedRooms->byRoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCreateWithParticipantPricesBuildsStep4Context(): void
|
public function testCreateWithParticipantPricesBuildsStep4Context(): void
|
||||||
@@ -174,34 +153,23 @@ class BookingCreateContextFactoryTest extends TestCase
|
|||||||
$bookingDto = new BookingDto($travel, 157047);
|
$bookingDto = new BookingDto($travel, 157047);
|
||||||
$summaryData = $this->createMock(BookingSummaryDto::class);
|
$summaryData = $this->createMock(BookingSummaryDto::class);
|
||||||
|
|
||||||
$summaryDataService = $this->createMock(BookingSummaryDataService::class);
|
$summaryDataService = $this->createMock(BookingSummaryAssembler::class);
|
||||||
$summaryDataService->expects($this->once())
|
$summaryDataService->expects($this->once())
|
||||||
->method('getSummaryData')
|
->method('getSummaryData')
|
||||||
->with($bookingDto, RoomPricingCalculator::PRICING_MODE_ASSIGNMENT)
|
->with($bookingDto, RoomPricingCalculator::PRICING_MODE_ASSIGNMENT)
|
||||||
->willReturn($summaryData);
|
->willReturn($summaryData);
|
||||||
|
|
||||||
$roomSelectionService = $this->createMock(BookingRoomSelectionService::class);
|
$participantCardDataService = $this->createMock(ParticipantCardAssembler::class);
|
||||||
$groupedRooms = new RoomGroupsDto(
|
|
||||||
byPax: [],
|
|
||||||
byRoom: [$room->id => $room],
|
|
||||||
);
|
|
||||||
$roomSelectionService->expects($this->once())
|
|
||||||
->method('groupRoomsBySelectionType')
|
|
||||||
->with([$room->id => $room])
|
|
||||||
->willReturn($groupedRooms);
|
|
||||||
|
|
||||||
$participantCardDataService = $this->createMock(ParticipantCardDataService::class);
|
|
||||||
$participantCardDataService->expects($this->never())
|
$participantCardDataService->expects($this->never())
|
||||||
->method('getAllCardsDataWithValidation');
|
->method('getAllCardsDataWithValidation');
|
||||||
|
|
||||||
$priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
|
$priceCalculator = $this->createMock(BookingPriceCalculator::class);
|
||||||
$priceCalculator->expects($this->once())
|
$priceCalculator->expects($this->once())
|
||||||
->method('calculateAllParticipantIndividualPrices')
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
->with($bookingDto)
|
->with($bookingDto)
|
||||||
->willReturn([123.45]);
|
->willReturn([123.45]);
|
||||||
|
|
||||||
$service = new BookingCreateContextFactory(
|
$service = new BookingCreateContextFactory(
|
||||||
$roomSelectionService,
|
|
||||||
$participantCardDataService,
|
$participantCardDataService,
|
||||||
$summaryDataService,
|
$summaryDataService,
|
||||||
$priceCalculator,
|
$priceCalculator,
|
||||||
@@ -212,5 +180,6 @@ class BookingCreateContextFactoryTest extends TestCase
|
|||||||
$this->assertInstanceOf(BookingCreateContext::class, $context);
|
$this->assertInstanceOf(BookingCreateContext::class, $context);
|
||||||
$this->assertSame([123.45], $context->participantPrices);
|
$this->assertSame([123.45], $context->participantPrices);
|
||||||
$this->assertNull($context->cardsData);
|
$this->assertNull($context->cardsData);
|
||||||
|
$this->assertSame([10 => $room], $context->groupedRooms->byRoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -13,9 +13,9 @@ use App\Entity\User;
|
|||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Repository\BookingEditDraftRepository;
|
use App\Repository\BookingEditDraftRepository;
|
||||||
use App\Service\BookingEditDraftService;
|
use App\Service\BookingEditDraftManager;
|
||||||
use App\Service\BookingEditDraftParticipantApplier;
|
use App\Service\BookingEditDraftMerger;
|
||||||
use App\Service\BookingFingerprintService;
|
use App\Service\BookingChangeTracker;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Psr\Log\NullLogger;
|
use Psr\Log\NullLogger;
|
||||||
@@ -27,17 +27,17 @@ use Psr\Log\NullLogger;
|
|||||||
* service selections on the DTO. This prevents stale draft data from causing
|
* service selections on the DTO. This prevents stale draft data from causing
|
||||||
* BusPro API rejections when submitting booking updates.
|
* BusPro API rejections when submitting booking updates.
|
||||||
*/
|
*/
|
||||||
class BookingEditDraftServiceMutabilityTest extends TestCase
|
class BookingEditDraftManagerMutabilityTest extends TestCase
|
||||||
{
|
{
|
||||||
private BookingEditDraftService $service;
|
private BookingEditDraftManager $service;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->service = new BookingEditDraftService(
|
$this->service = new BookingEditDraftManager(
|
||||||
$this->createMock(BookingEditDraftRepository::class),
|
$this->createMock(BookingEditDraftRepository::class),
|
||||||
$this->createMock(EntityManagerInterface::class),
|
$this->createMock(EntityManagerInterface::class),
|
||||||
$this->createMock(BookingFingerprintService::class),
|
$this->createMock(BookingChangeTracker::class),
|
||||||
new BookingEditDraftParticipantApplier(),
|
new BookingEditDraftMerger(),
|
||||||
new NullLogger(),
|
new NullLogger(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
<?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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Tests\Service;
|
|
||||||
|
|
||||||
use App\BusProNet\Model\Room;
|
|
||||||
use App\Form\Model\RoomGroupsDto;
|
|
||||||
use App\Service\BookingRoomSelectionService;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
class BookingRoomSelectionServiceTest extends TestCase
|
|
||||||
{
|
|
||||||
private BookingRoomSelectionService $service;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
$this->service = new BookingRoomSelectionService();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testGroupRoomsBySelectionTypeSplitsAndSortsRooms(): void
|
|
||||||
{
|
|
||||||
$rooms = [
|
|
||||||
10 => $this->createRoom(10, '2 Bett Zimmer', 4),
|
|
||||||
11 => $this->createRoom(11, 'Suite', 2),
|
|
||||||
12 => $this->createRoom(12, '3 Bett Zimmer', 6),
|
|
||||||
];
|
|
||||||
|
|
||||||
$groups = $this->service->groupRoomsBySelectionType($rooms);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(RoomGroupsDto::class, $groups);
|
|
||||||
$this->assertSame([10, 12], array_keys($groups->byPax));
|
|
||||||
$this->assertSame([11], array_keys($groups->byRoom));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createRoom(int $id, string $label, int $maxPax): Room
|
|
||||||
{
|
|
||||||
$room = new Room();
|
|
||||||
$room->id = $id;
|
|
||||||
$room->label = $label;
|
|
||||||
$room->maxPax = $maxPax;
|
|
||||||
|
|
||||||
return $room;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+45
-9
@@ -4,19 +4,27 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Tests\Service;
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Booking;
|
||||||
use App\BusProNet\Model\Room;
|
use App\BusProNet\Model\Room;
|
||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Model\BookingSummaryDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Form\Model\RoomSelectionDto;
|
use App\Form\Model\RoomSelectionDto;
|
||||||
use App\Service\BookingSummaryParticipantCountService;
|
use App\Service\BookingPriceCalculator;
|
||||||
|
use App\Service\BookingSummaryAssembler;
|
||||||
|
use App\Service\CmsDataProvider;
|
||||||
|
use App\BusProNet\DataProvider\CountryDataProvider;
|
||||||
|
use App\BusProNet\XmlLoader\HotelLoader;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Contracts\Cache\CacheInterface;
|
||||||
|
|
||||||
class BookingSummaryParticipantCountServiceTest extends TestCase
|
class BookingSummaryAssemblerTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testCreateStep1UsesExpectedParticipantCountFromSelectedRooms(): void
|
public function testCreateStep1UsesExpectedParticipantCountFromSelectedRooms(): void
|
||||||
{
|
{
|
||||||
$service = new BookingSummaryParticipantCountService();
|
$service = $this->createService();
|
||||||
$bookingDto = $this->createBookingDto();
|
$bookingDto = $this->createBookingDto();
|
||||||
$bookingDto->currentStep = 1;
|
$bookingDto->currentStep = 1;
|
||||||
$bookingDto->roomSelections = [
|
$bookingDto->roomSelections = [
|
||||||
@@ -24,12 +32,15 @@ class BookingSummaryParticipantCountServiceTest extends TestCase
|
|||||||
$this->createRoomSelection(11, 2),
|
$this->createRoomSelection(11, 2),
|
||||||
];
|
];
|
||||||
|
|
||||||
self::assertSame(8, $service->calculate($bookingDto));
|
$summary = $service->getSummaryData($bookingDto);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(BookingSummaryDto::class, $summary);
|
||||||
|
$this->assertSame(8, $summary->participantCount); // 1×2 + 2×3 = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testLaterCreateStepsUseActualParticipantCount(): void
|
public function testLaterCreateStepsUseActualParticipantCount(): void
|
||||||
{
|
{
|
||||||
$service = new BookingSummaryParticipantCountService();
|
$service = $this->createService();
|
||||||
$bookingDto = $this->createBookingDto();
|
$bookingDto = $this->createBookingDto();
|
||||||
$bookingDto->currentStep = 2;
|
$bookingDto->currentStep = 2;
|
||||||
$bookingDto->participants = [
|
$bookingDto->participants = [
|
||||||
@@ -38,20 +49,45 @@ class BookingSummaryParticipantCountServiceTest extends TestCase
|
|||||||
new ParticipantDto(),
|
new ParticipantDto(),
|
||||||
];
|
];
|
||||||
|
|
||||||
self::assertSame(3, $service->calculate($bookingDto));
|
$summary = $service->getSummaryData($bookingDto);
|
||||||
|
|
||||||
|
$this->assertSame(3, $summary->participantCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testEditModeUsesActualParticipantCount(): void
|
public function testEditModeUsesActualParticipantCount(): void
|
||||||
{
|
{
|
||||||
$service = new BookingSummaryParticipantCountService();
|
$service = $this->createService();
|
||||||
$bookingDto = $this->createBookingDto();
|
$bookingDto = $this->createBookingDto();
|
||||||
$bookingDto->booking = new \App\BusProNet\Model\Booking();
|
$bookingDto->booking = new Booking();
|
||||||
$bookingDto->participants = [
|
$bookingDto->participants = [
|
||||||
new ParticipantDto(),
|
new ParticipantDto(),
|
||||||
new ParticipantDto(),
|
new ParticipantDto(),
|
||||||
];
|
];
|
||||||
|
|
||||||
self::assertSame(2, $service->calculate($bookingDto));
|
$summary = $service->getSummaryData($bookingDto);
|
||||||
|
|
||||||
|
$this->assertSame(2, $summary->participantCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createService(): BookingSummaryAssembler
|
||||||
|
{
|
||||||
|
$priceCalculator = $this->createMock(BookingPriceCalculator::class);
|
||||||
|
$priceCalculator->method('calculateAllParticipantIndividualPrices')->willReturn([]);
|
||||||
|
$priceCalculator->method('getPricingBreakdown')->willReturn([
|
||||||
|
'rooms' => [],
|
||||||
|
'services' => [],
|
||||||
|
'surcharges' => null,
|
||||||
|
'grandTotal' => 0.0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new BookingSummaryAssembler(
|
||||||
|
$priceCalculator,
|
||||||
|
$this->createMock(CmsDataProvider::class),
|
||||||
|
$this->createMock(HotelLoader::class),
|
||||||
|
$this->createMock(CountryDataProvider::class),
|
||||||
|
$this->createMock(CacheInterface::class),
|
||||||
|
new NullLogger(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createBookingDto(): BookingDto
|
private function createBookingDto(): BookingDto
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Tests\Service;
|
|
||||||
|
|
||||||
use App\BusProNet\Model\Booking;
|
|
||||||
use App\Form\Model\BookingDto;
|
|
||||||
use App\Form\Model\BookingSummaryDto;
|
|
||||||
use App\Service\BookingPriceCalculatorService;
|
|
||||||
use App\Service\BookingSummaryDataService;
|
|
||||||
use App\Service\BookingSummaryParticipantCountService;
|
|
||||||
use App\Service\CmsDataService;
|
|
||||||
use App\BusProNet\DataProvider\CountryDataProvider;
|
|
||||||
use App\BusProNet\XmlLoader\HotelLoader;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
use Symfony\Contracts\Cache\CacheInterface;
|
|
||||||
|
|
||||||
class BookingSummaryDataServiceTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testCreateStep1UsesExpectedParticipantCountFromSelectedRooms(): void
|
|
||||||
{
|
|
||||||
$service = $this->createService(8);
|
|
||||||
$bookingDto = $this->createBookingDto();
|
|
||||||
|
|
||||||
$summary = $service->getSummaryData($bookingDto);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(BookingSummaryDto::class, $summary);
|
|
||||||
$this->assertSame(8, $summary->participantCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testLaterCreateStepsUseActualParticipantCount(): void
|
|
||||||
{
|
|
||||||
$service = $this->createService(3);
|
|
||||||
$bookingDto = $this->createBookingDto();
|
|
||||||
$bookingDto->currentStep = 2;
|
|
||||||
|
|
||||||
$summary = $service->getSummaryData($bookingDto);
|
|
||||||
|
|
||||||
$this->assertSame(3, $summary->participantCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testEditModeUsesActualParticipantCount(): void
|
|
||||||
{
|
|
||||||
$service = $this->createService(2);
|
|
||||||
$bookingDto = $this->createBookingDto();
|
|
||||||
$bookingDto->booking = new Booking();
|
|
||||||
|
|
||||||
$summary = $service->getSummaryData($bookingDto);
|
|
||||||
|
|
||||||
$this->assertSame(2, $summary->participantCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createService(int $participantCount): BookingSummaryDataService
|
|
||||||
{
|
|
||||||
$priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
|
|
||||||
$priceCalculator->method('calculateAllParticipantIndividualPrices')->willReturn([]);
|
|
||||||
$priceCalculator->method('getPricingBreakdown')->willReturn([
|
|
||||||
'rooms' => [],
|
|
||||||
'services' => [],
|
|
||||||
'surcharges' => null,
|
|
||||||
'grandTotal' => 0.0,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$participantCountService = $this->createMock(BookingSummaryParticipantCountService::class);
|
|
||||||
$participantCountService->method('calculate')
|
|
||||||
->willReturn($participantCount);
|
|
||||||
|
|
||||||
return new BookingSummaryDataService(
|
|
||||||
$priceCalculator,
|
|
||||||
$participantCountService,
|
|
||||||
$this->createMock(CmsDataService::class),
|
|
||||||
$this->createMock(HotelLoader::class),
|
|
||||||
$this->createMock(CountryDataProvider::class),
|
|
||||||
$this->createMock(CacheInterface::class),
|
|
||||||
new NullLogger(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function createBookingDto(): BookingDto
|
|
||||||
{
|
|
||||||
$bookingDto = new BookingDto(new \App\BusProNet\Model\Travel(), 157047);
|
|
||||||
|
|
||||||
return $bookingDto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user