feat: exclusion groups for additional services

This commit is contained in:
Björn Fromme
2026-08-04 10:24:29 +02:00
parent c31d086356
commit 331c88c38e
11 changed files with 457 additions and 17 deletions
+96 -16
View File
@@ -4,20 +4,21 @@ declare(strict_types=1);
namespace App\Tests\Form;
use App\Entity\Groups\AdditionalService;
use App\Form\AccommodationStep2Type;
use App\Form\Model\AccommodationBookingDto;
use App\Service\AdditionalServiceExclusionResolver;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\Forms;
class AccommodationStep2TypeTest extends TestCase
{
public function testChildrenLabelIsBuiltFromMaxAdolescentAgeOption(): void
{
$form = Forms::createFormFactoryBuilder()
->getFormFactory()
->create(AccommodationStep2Type::class, new AccommodationBookingDto(), [
'max_adolescent_age' => 15,
]);
$form = $this->createForm(new AccommodationBookingDto(), [
'max_adolescent_age' => 15,
]);
self::assertSame(
'davon Kinder (415 Jahre)',
@@ -28,9 +29,7 @@ class AccommodationStep2TypeTest extends TestCase
public function testBlankOptionalChildCountersAreNormalizedToZero(): void
{
$dto = new AccommodationBookingDto();
$form = Forms::createFormFactoryBuilder()
->getFormFactory()
->create(AccommodationStep2Type::class, $dto);
$form = $this->createForm($dto);
$form->submit([
'paxCount' => '2',
@@ -48,14 +47,12 @@ class AccommodationStep2TypeTest extends TestCase
public function testGroupedAdditionalServiceValuesCanSubmitUnderSelectionGroupKeys(): void
{
$dto = new AccommodationBookingDto();
$form = Forms::createFormFactoryBuilder()
->getFormFactory()
->create(AccommodationStep2Type::class, $dto, [
'additional_service_choices' => [
'Ski service' => 101,
'Board service' => 202,
],
]);
$form = $this->createForm($dto, [
'additional_service_choices' => [
'Ski service' => 101,
'Board service' => 202,
],
]);
$form->submit([
'paxCount' => '2',
@@ -71,4 +68,87 @@ class AccommodationStep2TypeTest extends TestCase
self::assertTrue($form->isSynchronized());
self::assertSame([101, 202], array_values($dto->selectedAdditionalServiceIds));
}
public function testNewlySelectedExclusiveServiceClearsItsConflicts(): void
{
$dto = new AccommodationBookingDto();
$dto->selectedAdditionalServiceIds = [11, 12];
$form = $this->createForm($dto, [
'additional_service_choices' => [
'Keller' => 11,
'1. OG' => 12,
'Komplett' => 13,
],
'additional_services' => [
$this->service(11, 'Reinigung', false),
$this->service(12, 'Reinigung', false),
$this->service(13, 'Reinigung', true),
],
]);
$form->submit([
'paxCount' => '2',
'minorsCount' => '0',
'childrenCount' => '0',
'selectedBoardServiceId' => '',
'selectedAdditionalServiceIds' => ['11', '12', '13'],
]);
self::assertTrue($form->isSynchronized());
self::assertSame([13], array_values($dto->selectedAdditionalServiceIds));
}
public function testNewlySelectedRegularServiceClearsTheExclusiveOne(): void
{
$dto = new AccommodationBookingDto();
$dto->selectedAdditionalServiceIds = [13];
$form = $this->createForm($dto, [
'additional_service_choices' => [
'Keller' => 11,
'Komplett' => 13,
],
'additional_services' => [
$this->service(11, 'Reinigung', false),
$this->service(13, 'Reinigung', true),
],
]);
$form->submit([
'paxCount' => '2',
'minorsCount' => '0',
'childrenCount' => '0',
'selectedBoardServiceId' => '',
'selectedAdditionalServiceIds' => ['13', '11'],
]);
self::assertTrue($form->isSynchronized());
self::assertSame([11], array_values($dto->selectedAdditionalServiceIds));
}
/**
* @param array<string, mixed> $options
*
* @return FormInterface<AccommodationBookingDto>
*/
private function createForm(AccommodationBookingDto $dto, array $options = []): FormInterface
{
return Forms::createFormFactoryBuilder()
->addType(new AccommodationStep2Type(new AdditionalServiceExclusionResolver()))
->getFormFactory()
->create(AccommodationStep2Type::class, $dto, $options);
}
private function service(int $id, ?string $exclusionGroup, bool $isExclusive): AdditionalService
{
$service = new AdditionalService();
$service->setExclusionGroup($exclusionGroup);
$service->setIsExclusive($isExclusive);
$property = new \ReflectionProperty(AdditionalService::class, 'id');
$property->setValue($service, $id);
return $service;
}
}
@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\Entity\Groups\AdditionalService;
use App\Service\AdditionalServiceExclusionResolver;
use PHPUnit\Framework\TestCase;
class AdditionalServiceExclusionResolverTest extends TestCase
{
private const int BASEMENT = 1;
private const int FIRST_FLOOR = 2;
private const int SECOND_FLOOR = 3;
private const int FULL = 4;
private const int BREAKFAST = 5;
public function testSelectingTheExclusiveServiceClearsTheOtherGroupMembers(): void
{
$resolved = $this->resolve(
[self::BASEMENT, self::FIRST_FLOOR, self::FULL],
[self::BASEMENT, self::FIRST_FLOOR],
);
self::assertSame([self::FULL], array_values($resolved));
}
public function testSelectingANonExclusiveServiceClearsOnlyTheExclusiveOne(): void
{
$resolved = $this->resolve(
[self::FULL, self::BASEMENT],
[self::FULL],
);
self::assertSame([self::BASEMENT], array_values($resolved));
}
public function testNonExclusiveGroupMembersCanBeCombined(): void
{
$resolved = $this->resolve(
[self::BASEMENT, self::FIRST_FLOOR, self::SECOND_FLOOR],
[self::BASEMENT],
);
self::assertSame(
[self::BASEMENT, self::FIRST_FLOOR, self::SECOND_FLOOR],
array_values($resolved),
);
}
public function testServicesWithoutAnExclusionGroupAreNeverCleared(): void
{
$resolved = $this->resolve(
[self::BREAKFAST, self::BASEMENT, self::FULL],
[self::BREAKFAST, self::BASEMENT],
);
self::assertSame([self::BREAKFAST, self::FULL], array_values($resolved));
}
public function testUnchangedSelectionIsLeftAlone(): void
{
$resolved = $this->resolve(
[self::BASEMENT, self::FULL],
[self::BASEMENT, self::FULL],
);
self::assertSame([self::BASEMENT, self::FULL], array_values($resolved));
}
public function testSubmittedArrayKeysArePreserved(): void
{
$resolver = new AdditionalServiceExclusionResolver();
$resolved = $resolver->resolve(
$this->services(),
['Reinigung' => self::FULL, 'Verpflegung' => self::BREAKFAST, 0 => self::BASEMENT],
[self::BASEMENT],
);
self::assertSame(
['Reinigung' => self::FULL, 'Verpflegung' => self::BREAKFAST],
$resolved,
);
}
/**
* @param list<int> $submittedIds
* @param list<int> $previousIds
*
* @return array<array-key, int>
*/
private function resolve(array $submittedIds, array $previousIds): array
{
return (new AdditionalServiceExclusionResolver())->resolve(
$this->services(),
$submittedIds,
$previousIds,
);
}
/**
* @return AdditionalService[]
*/
private function services(): array
{
return [
$this->service(self::BASEMENT, 'Reinigung', false),
$this->service(self::FIRST_FLOOR, 'Reinigung', false),
$this->service(self::SECOND_FLOOR, 'Reinigung', false),
$this->service(self::FULL, 'Reinigung', true),
$this->service(self::BREAKFAST, null, false),
];
}
private function service(int $id, ?string $exclusionGroup, bool $isExclusive): AdditionalService
{
$service = new AdditionalService();
$service->setExclusionGroup($exclusionGroup);
$service->setIsExclusive($isExclusive);
$property = new \ReflectionProperty(AdditionalService::class, 'id');
$property->setValue($service, $id);
return $service;
}
}