Notification messages for user feedback */ public array $notifications = []; public function __construct() { $this->address = new Address(); } public static function fromPersonalData(PersonalData $personalData): static { $instance = new static(); $instance->status = $personalData->status; $instance->addressId = $personalData->addressId; $instance->personId = $personalData->personId; $instance->mutable = $personalData->mutable; $instance->firstName = $personalData->firstName; $instance->lastName = $personalData->name; $instance->title = $personalData->title; $instance->gender = $personalData->gender; $instance->nationality = $personalData->nationality; $instance->email = $personalData->communication?->email; $instance->mobile = $personalData->communication?->mobile; $instance->dateOfBirth = $personalData->dateOfBirth; $instance->height = $personalData->height; $instance->weight = $personalData->weight; $instance->shoeSize = $personalData->shoeSize; // Clone address to prevent shared object references that could cause mutations $instance->address = null !== $personalData->address ? clone $personalData->address : null; $instance->remarksRoom = $personalData->remarksRoom; $instance->licensePlate = $personalData->licensePlate; return $instance; } public function isApplicant(): bool { return 0 === $this->index; } public function isCanceled(): bool { return 'S' === $this->status; } public function isOption(): bool { return 'O' === $this->status; } /** * Calculates the participant's age in complete years. * * Uses the same logic as existing age evaluators in the system * for consistency across age-related calculations. * * @param \DateTimeImmutable|null $referenceDate The date to calculate age at (defaults to current date) * * @return int|null The calculated age in complete years, or null if no birth date */ public function getAge(?\DateTimeImmutable $referenceDate = null): ?int { if (null === $this->dateOfBirth) { return null; } $referenceDate = $referenceDate ?? new \DateTimeImmutable(); return $this->dateOfBirth->diff($referenceDate)->y; } /** * Checks if the participant has selected an insurance. * * @return bool True if an insurance is selected */ public function hasInsuranceSelected(): bool { return null !== $this->insurance; } /** * Gets the insurance label for display purposes. * * @return string|null The insurance label or null if no insurance selected */ public function getInsuranceLabel(): ?string { return $this->insurance?->label; } /** * Gets the insurance price for pricing calculations. * * @return float The insurance price (0.0 if no insurance selected) */ public function getInsurancePrice(): float { return $this->insurance?->price ?? 0.0; } /** * Adds a notification message for user feedback. * * Uses an optional ID to prevent duplicate notifications. If no ID is provided, * generates one from the type and message combination. Duplicate IDs will * overwrite previous notifications, ensuring each unique notification appears once. * * @param string $type The notification type (info, warning, success) * @param string $message The notification message * @param string|null $id Optional unique identifier (auto-generated if null) */ public function addNotification(string $type, string $message, ?string $id = null): void { // Generate ID from type and message if not provided $notificationId = $id ?? md5($type.'_'.$message); // Use ID as key to automatically prevent duplicates $this->notifications[$notificationId] = [ 'type' => $type, 'message' => $message, ]; } /** * Determines if the participant is a child based on age at current date. * * A child is defined as someone under the specified age threshold (default: 16 years). * This classification is used for email uniqueness validation (children can share emails with adults). * * @param int $ageThreshold The age threshold for child classification (default: 16) * * @return bool True if participant is under the age threshold, false otherwise or if age unknown */ public function isChild(int $ageThreshold = 16): bool { $age = $this->getAge(); // Treat unknown age as adult for safety (requires email uniqueness) if (null === $age) { return false; } return $ageThreshold > $age; } /** * Validates that the applicant (index 0) has a complete address. * * This callback only applies to the applicant participant. Address validation includes: * - Address object must exist * - All required address fields must be filled (street, postCode, city, country) */ #[Assert\Callback(groups: ['strict_required'])] public function validateApplicantAddress(ExecutionContextInterface $context): void { // Only validate applicant's address if (0 !== $this->index) { return; } // Address object is required for applicant if (null === $this->address) { $context->buildViolation('Bitte angeben') ->atPath('address') ->addViolation(); return; } // Validate address subfields if (null === $this->address->street || '' === trim($this->address->street)) { $context->buildViolation('Bitte angeben') ->atPath('address.street') ->addViolation(); } if (null === $this->address->postCode || '' === trim($this->address->postCode)) { $context->buildViolation('Bitte angeben') ->atPath('address.postCode') ->addViolation(); } if (null === $this->address->city || '' === trim($this->address->city)) { $context->buildViolation('Bitte angeben') ->atPath('address.city') ->addViolation(); } if (null === $this->address->country || '' === trim($this->address->country)) { $context->buildViolation('Bitte angeben') ->atPath('address.country') ->addViolation(); } } }