fix: validate mandatory services selection

This commit is contained in:
Björn Fromme
2026-03-16 12:03:00 +01:00
parent 474d171779
commit b752256bda
4 changed files with 291 additions and 0 deletions
+1
View File
@@ -19,6 +19,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
*/
#[AppAssert\PurchaseVoucher(groups: ['booking_create', 'booking_edit'])]
#[AppAssert\PromoVoucher(groups: ['booking_create', 'booking_edit'])]
#[AppAssert\MandatoryAdditionalServicesSelected(groups: ['booking_create', 'booking_edit'])]
class ParticipantEditDto
{
public function __construct(
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* Validates that all mandatory additional services are selected.
*
* Applied on ParticipantEditDto to enforce mandatory additional services based
* on participant age and booking eligibility in both create and edit flows.
*/
#[\Attribute]
class MandatoryAdditionalServicesSelected extends Constraint
{
public string $message = 'Bitte wähle alle Pflichtleistungen aus.';
public function getTargets(): array|string
{
return static::CLASS_CONSTRAINT;
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Validator\Constraints;
use App\BusProNet\Constants;
use App\BusProNet\Model\Service;
use App\Form\Model\ParticipantEditDto;
use App\Form\Service\ServiceAgeEvaluator;
use App\Service\ParticipantEligibilityService;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Enforces selection of age-eligible mandatory additional services.
*/
class MandatoryAdditionalServicesSelectedValidator extends ConstraintValidator
{
public function __construct(
private readonly ParticipantEligibilityService $participantEligibilityService,
private readonly ServiceAgeEvaluator $serviceAgeEvaluator,
) {
}
public function validate(mixed $value, Constraint $constraint): void
{
if (false === $constraint instanceof MandatoryAdditionalServicesSelected) {
throw new UnexpectedTypeException($constraint, MandatoryAdditionalServicesSelected::class);
}
if (false === $value instanceof ParticipantEditDto) {
throw new UnexpectedTypeException($value, ParticipantEditDto::class);
}
$participant = $value->participant;
if (true === $participant->isCanceled()) {
return;
}
$participantIndex = $participant->index ?? 0;
if (false === $this->participantEligibilityService->isParticipantEligible($value->bookingContext, $participantIndex)) {
return;
}
$mandatoryAdditionalServices = array_filter(
$value->bookingContext->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
function (Service $service) use ($value, $participantIndex): bool {
if (true !== $service->mandatory) {
return false;
}
if (true === $this->serviceAgeEvaluator->canEvaluate($service)) {
return $this->serviceAgeEvaluator->isServiceAvailableForParticipant(
$service,
$value->bookingContext,
$participantIndex
);
}
return true;
}
);
if (true === empty($mandatoryAdditionalServices)) {
return;
}
$selectedAdditionalServiceIds = array_map(
static fn (Service $service): int => $service->id,
$participant->additionalServices
);
foreach ($mandatoryAdditionalServices as $mandatoryService) {
if (false === in_array($mandatoryService->id, $selectedAdditionalServiceIds, true)) {
$this->context->buildViolation($constraint->message)
->atPath('participant.additionalServices')
->addViolation();
return;
}
}
}
}