buildCardData($bookingDto, $index); } /** * @param array|null $precomputedPrices */ private function buildCardData( BookingDto $bookingDto, int $index, ?array $precomputedPrices = null, ): ParticipantCardDataDto { $participant = $bookingDto->participants[$index] ?? null; if (null === $participant) { throw new \InvalidArgumentException(sprintf('Participant at index %d does not exist', $index)); } // Extract participant name with fallback $name = $this->getParticipantName($bookingDto, $participant, $index); // Extract email $email = $participant->email ?? ''; // Extract room name $roomName = $this->getRoomName($bookingDto, $participant); // Calculate pricing state for display $priceData = $this->getPriceData($bookingDto, $index, $precomputedPrices); // Check if participant is canceled $isCanceled = $participant->isCanceled(); return new ParticipantCardDataDto( name: $name, email: $email, roomName: $roomName, price: $priceData, isCanceled: $isCanceled, ); } /** * Get card data for all participants. * * @return array */ public function getAllCardsData(BookingDto $bookingDto): array { $cardsData = []; $prices = $this->calculateBulkParticipantPrices($bookingDto); foreach ($bookingDto->participants as $index => $_participant) { $cardsData[$index] = $this->buildCardData($bookingDto, $index, $prices); } return $cardsData; } /** * Get participant name with fallback to generic label. */ private function getParticipantName(BookingDto $bookingDto, object $participant, int $index): string { $firstName = $participant->firstName ?? ''; $lastName = $participant->lastName ?? ''; $name = trim($firstName.' '.$lastName); if ('' === $name) { // For internal agency bookings, first participant is not the applicant if (0 === $index && false === $bookingDto->isInternalAgencyBooking()) { return 'Anmelder:in'; } return 'Teilnehmer:in'; } return $name; } /** * Get room name from travel model. */ private function getRoomName(BookingDto $bookingDto, object $participant): string { $roomId = $participant->assignedRoomId ?? null; if (null === $roomId) { return 'Kein Zimmer zugewiesen'; } $room = $bookingDto->travel->getRoomById($roomId); if (null === $room) { return 'Unbekanntes Zimmer'; } return $room->label ?? 'Unbekanntes Zimmer'; } /** * Calculate individual participant price display state. * * Returns a dash marker when the price is zero and no room is assigned, * indicating incomplete configuration rather than a zero-cost booking. * * @param array|null $precomputedPrices */ private function getPriceData( BookingDto $bookingDto, int $index, ?array $precomputedPrices = null, ): ParticipantCardPriceDto { // Check if canceled (only possible in edit mode when booking property is set) $isCanceled = ($bookingDto->booking?->participantsStatus[$index] ?? null) === 'S'; if (true === $isCanceled) { // Calculate surcharge total for canceled participant if (null === $bookingDto->booking) { return new ParticipantCardPriceDto(null, true); } $surcharges = $bookingDto->booking->getSurchargesForParticipant($index); $surchargeTotal = 0.0; foreach ($surcharges as $surcharge) { $surchargeTotal += $surcharge->individualPrice[$index] ?? 0.0; } // Show nothing if no surcharges, otherwise show "x,xx € Stornokosten" if (0.0 === $surchargeTotal) { return new ParticipantCardPriceDto(null, true); } return new ParticipantCardPriceDto($surchargeTotal, false); } // For active participants: existing price calculation logic $prices = $precomputedPrices ?? $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingDto); $price = $prices[$index] ?? 0.0; // Display dash when price is zero and no room assigned (incomplete configuration) $participant = $bookingDto->participants[$index] ?? null; if (0.0 === $price && (null === $participant || null === $participant->assignedRoomId)) { return new ParticipantCardPriceDto(null, true); } return new ParticipantCardPriceDto($price, false); } /** * Get card data for a single participant with validation state. */ public function getCardDataWithValidation( BookingDto $bookingDto, int $index, bool $forceStrictRequired = false, ): ParticipantCardDataDto { return $this->getCardDataWithValidationInternal( $bookingDto, $index, $this->determineValidationGroups($bookingDto, $forceStrictRequired) ); } /** * Get card data for all participants with validation state. * * @return array */ public function getAllCardsDataWithValidation(BookingDto $bookingDto, bool $forceStrictRequired = false): array { $cardsData = []; $prices = $this->calculateBulkParticipantPrices($bookingDto); $validationGroups = $this->determineValidationGroups($bookingDto, $forceStrictRequired); foreach ($bookingDto->participants as $index => $_participant) { $cardsData[$index] = $this->getCardDataWithValidationInternal( $bookingDto, $index, $validationGroups, $prices ); } return $cardsData; } /** * @return array */ private function calculateBulkParticipantPrices(BookingDto $bookingDto): array { foreach ($bookingDto->participants as $index => $_participant) { if (($bookingDto->booking?->participantsStatus[$index] ?? null) !== 'S') { return $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingDto); } } return []; } /** * Determines validation groups based on booking mode and applicant mutability. * * @return array Validation groups to apply */ private function determineValidationGroups(BookingDto $bookingDto, bool $forceStrictRequired = false): array { $groups = []; // Add mode-specific group if (BookingDto::MODE_CREATE === $bookingDto->getMode()) { $groups[] = 'booking_create'; $groups[] = 'strict_required'; } else { $groups[] = 'booking_edit'; // Strict validation in edit mode except for internal agency bookings, // unless the caller explicitly asks for the full validation set. if (true === $forceStrictRequired || false === $bookingDto->isInternalAgencyBooking()) { $groups[] = 'strict_required'; } } return $groups; } /** * @param array $validationGroups * @param array|null $precomputedPrices */ private function getCardDataWithValidationInternal( BookingDto $bookingDto, int $index, array $validationGroups, ?array $precomputedPrices = null, ): ParticipantCardDataDto { $participant = $bookingDto->participants[$index] ?? null; if (null === $participant) { throw new \InvalidArgumentException(sprintf('Participant at index %d does not exist', $index)); } // Get basic card data $cardData = $this->buildCardData($bookingDto, $index, $precomputedPrices); $wrapper = new ParticipantEditDto( participant: $participant, bookingContext: $bookingDto, ); // Validate the wrapper DTO $violations = $this->validator->validate($wrapper, null, $validationGroups); // Extract validation state $isValid = 0 === count($violations); $errorMessages = []; foreach ($violations as $violation) { $errorMessages[] = $violation->getMessage(); } return $cardData->withValidation($isValid, $errorMessages); } }