feat: consolidate micro-services and unify draft merge logic

This commit is contained in:
Björn Fromme
2026-04-16 13:34:54 +02:00
parent fdfd5d56c9
commit 0d2cc5b998
16 changed files with 529 additions and 846 deletions
@@ -8,15 +8,13 @@ use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingCreateContext;
use App\Form\Model\BookingDto;
use App\Form\Model\RoomGroupsDto;
use App\Form\Model\ParticipantCardDataDto;
use App\Form\Model\ParticipantCardPriceDto;
use App\Form\Model\BookingSummaryDto;
use App\Service\BookingCreateContextFactory;
use App\Service\BookingRoomSelectionService;
use App\Service\ParticipantCardDataService;
use App\Service\BookingSummaryDataService;
use App\Service\BookingPriceCalculatorService;
use App\Service\ParticipantCardAssembler;
use App\Service\BookingSummaryAssembler;
use App\Service\BookingPriceCalculator;
use App\Service\RoomPricingCalculator;
use PHPUnit\Framework\TestCase;
@@ -47,7 +45,7 @@ class BookingCreateContextFactoryTest extends TestCase
$bookingDto = new BookingDto($travel, 157047);
$summaryData = $this->createMock(BookingSummaryDto::class);
$summaryDataService = $this->createMock(BookingSummaryDataService::class);
$summaryDataService = $this->createMock(BookingSummaryAssembler::class);
$summaryDataService->expects($this->once())
->method('getSummaryData')
->with(
@@ -56,26 +54,15 @@ class BookingCreateContextFactoryTest extends TestCase
)
->willReturn($summaryData);
$roomSelectionService = $this->createMock(BookingRoomSelectionService::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 = $this->createMock(ParticipantCardAssembler::class);
$participantCardDataService->expects($this->never())
->method('getAllCardsDataWithValidation');
$priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
$priceCalculator = $this->createMock(BookingPriceCalculator::class);
$priceCalculator->expects($this->never())
->method('calculateAllParticipantIndividualPrices');
$service = new BookingCreateContextFactory(
$roomSelectionService,
$participantCardDataService,
$summaryDataService,
$priceCalculator,
@@ -86,9 +73,10 @@ class BookingCreateContextFactoryTest extends TestCase
$this->assertInstanceOf(BookingCreateContext::class, $context);
$this->assertSame($bookingDto, $context->bookingDto);
$this->assertSame($summaryData, $context->summaryData);
$this->assertSame(null, $context->cardsData);
$this->assertNull($context->cardsData);
$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
@@ -116,34 +104,23 @@ class BookingCreateContextFactoryTest extends TestCase
isCanceled: false,
);
$summaryDataService = $this->createMock(BookingSummaryDataService::class);
$summaryDataService = $this->createMock(BookingSummaryAssembler::class);
$summaryDataService->expects($this->once())
->method('getSummaryData')
->with($bookingDto, RoomPricingCalculator::PRICING_MODE_SELECTION)
->willReturn($summaryData);
$roomSelectionService = $this->createMock(BookingRoomSelectionService::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 = $this->createMock(ParticipantCardAssembler::class);
$participantCardDataService->expects($this->once())
->method('getAllCardsDataWithValidation')
->with($bookingDto)
->willReturn([$cardData]);
$priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
$priceCalculator = $this->createMock(BookingPriceCalculator::class);
$priceCalculator->expects($this->never())
->method('calculateAllParticipantIndividualPrices');
$service = new BookingCreateContextFactory(
$roomSelectionService,
$participantCardDataService,
$summaryDataService,
$priceCalculator,
@@ -154,6 +131,8 @@ class BookingCreateContextFactoryTest extends TestCase
$this->assertInstanceOf(BookingCreateContext::class, $context);
$this->assertSame([$cardData], $context->cardsData);
$this->assertTrue($context->isSubmitted);
$this->assertSame([], $context->groupedRooms->byPax);
$this->assertSame([10 => $room], $context->groupedRooms->byRoom);
}
public function testCreateWithParticipantPricesBuildsStep4Context(): void
@@ -174,34 +153,23 @@ class BookingCreateContextFactoryTest extends TestCase
$bookingDto = new BookingDto($travel, 157047);
$summaryData = $this->createMock(BookingSummaryDto::class);
$summaryDataService = $this->createMock(BookingSummaryDataService::class);
$summaryDataService = $this->createMock(BookingSummaryAssembler::class);
$summaryDataService->expects($this->once())
->method('getSummaryData')
->with($bookingDto, RoomPricingCalculator::PRICING_MODE_ASSIGNMENT)
->willReturn($summaryData);
$roomSelectionService = $this->createMock(BookingRoomSelectionService::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 = $this->createMock(ParticipantCardAssembler::class);
$participantCardDataService->expects($this->never())
->method('getAllCardsDataWithValidation');
$priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
$priceCalculator = $this->createMock(BookingPriceCalculator::class);
$priceCalculator->expects($this->once())
->method('calculateAllParticipantIndividualPrices')
->with($bookingDto)
->willReturn([123.45]);
$service = new BookingCreateContextFactory(
$roomSelectionService,
$participantCardDataService,
$summaryDataService,
$priceCalculator,
@@ -212,5 +180,6 @@ class BookingCreateContextFactoryTest extends TestCase
$this->assertInstanceOf(BookingCreateContext::class, $context);
$this->assertSame([123.45], $context->participantPrices);
$this->assertNull($context->cardsData);
$this->assertSame([10 => $room], $context->groupedRooms->byRoom);
}
}
@@ -13,9 +13,9 @@ use App\Entity\User;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Repository\BookingEditDraftRepository;
use App\Service\BookingEditDraftService;
use App\Service\BookingEditDraftParticipantApplier;
use App\Service\BookingFingerprintService;
use App\Service\BookingEditDraftManager;
use App\Service\BookingEditDraftMerger;
use App\Service\BookingChangeTracker;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
@@ -27,17 +27,17 @@ use Psr\Log\NullLogger;
* service selections on the DTO. This prevents stale draft data from causing
* 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
{
$this->service = new BookingEditDraftService(
$this->service = new BookingEditDraftManager(
$this->createMock(BookingEditDraftRepository::class),
$this->createMock(EntityManagerInterface::class),
$this->createMock(BookingFingerprintService::class),
new BookingEditDraftParticipantApplier(),
$this->createMock(BookingChangeTracker::class),
new BookingEditDraftMerger(),
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;
}
}
@@ -4,19 +4,27 @@ declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingDto;
use App\Form\Model\BookingSummaryDto;
use App\Form\Model\ParticipantDto;
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 Psr\Log\NullLogger;
use Symfony\Contracts\Cache\CacheInterface;
class BookingSummaryParticipantCountServiceTest extends TestCase
class BookingSummaryAssemblerTest extends TestCase
{
public function testCreateStep1UsesExpectedParticipantCountFromSelectedRooms(): void
{
$service = new BookingSummaryParticipantCountService();
$service = $this->createService();
$bookingDto = $this->createBookingDto();
$bookingDto->currentStep = 1;
$bookingDto->roomSelections = [
@@ -24,12 +32,15 @@ class BookingSummaryParticipantCountServiceTest extends TestCase
$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
{
$service = new BookingSummaryParticipantCountService();
$service = $this->createService();
$bookingDto = $this->createBookingDto();
$bookingDto->currentStep = 2;
$bookingDto->participants = [
@@ -38,20 +49,45 @@ class BookingSummaryParticipantCountServiceTest extends TestCase
new ParticipantDto(),
];
self::assertSame(3, $service->calculate($bookingDto));
$summary = $service->getSummaryData($bookingDto);
$this->assertSame(3, $summary->participantCount);
}
public function testEditModeUsesActualParticipantCount(): void
{
$service = new BookingSummaryParticipantCountService();
$service = $this->createService();
$bookingDto = $this->createBookingDto();
$bookingDto->booking = new \App\BusProNet\Model\Booking();
$bookingDto->booking = new Booking();
$bookingDto->participants = [
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
@@ -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;
}
}