request->paymentMethod !== null ? EventPaymentModuleRegistry::forSlug($this->request->paymentMethod) : null; $paymentOptions = $module?->sanitizeParticipantOptions($this->request->paymentOptions) ?? []; if ($module !== null && !$module->participantOptionsComplete($paymentOptions)) { $response->success = false; $response->message = 'Bitte alle erforderlichen Zahlungsangaben ausfüllen.'; return $response; } // Teilnahmeoptionen (event-spezifische Pflicht-Auswahl) gegen die Event-Definition absichern. $participationOptionRows = $this->buildParticipationOptionRows(); if ($participationOptionRows === null) { $response->success = false; $response->message = 'Bitte alle Auswahlfelder ausfüllen.'; return $response; } $eatingHabit = match ($this->request->eating_habit) { 'vegan' => EatingHabit::EATING_HABIT_VEGAN, 'vegetarian' => EatingHabit::EATING_HABIT_VEGETARIAN, default => EatingHabit::EATING_HABIT_OMNIVOR, }; $participantAge = new Age($this->request->birthday); $response->participant = $this->request->event->participants()->create( [ 'tenant' => $this->request->event->tenant, 'user_id' => $this->request->user_id, 'identifier' => Str::random(10), 'firstname' => $this->request->firstname, 'lastname' => $this->request->lastname, 'nickname' => $this->request->nickname, 'participation_type' => $this->request->participationType, 'local_group' => $this->request->localGroup->slug, 'birthday' => $this->request->birthday, 'address_1' => $this->request->address_1, 'address_2' => $this->request->address_2, 'postcode' => $this->request->postcode, 'city' => $this->request->city, 'email_1' => $this->request->email_1, 'email_2' => $this->request->email_2, 'phone_1' => $this->request->phone_1, 'phone_2' => $this->request->phone_2, 'contact_person' => $this->request->contact_person, 'allergies' => $this->request->allergies, 'intolerances' => $this->request->intolerances, 'medications' => $this->request->medications, 'tetanus_vaccination' => $this->request->tetanus_vaccination, 'eating_habit' => $eatingHabit, 'swimming_permission' => $participantAge->isfullAged() ? 'SWIMMING_PERMISSION_ALLOWED' : $this->request->swimming_permission, 'first_aid_permission' => $participantAge->isfullAged() ? 'FIRST_AID_PERMISSION_ALLOWED' : $this->request->first_aid_permission, 'foto_socialmedia' => $this->request->foto_socialmedia, 'foto_print' => $this->request->foto_print, 'foto_webseite' => $this->request->foto_webseite, 'foto_partner' => $this->request->foto_partner, 'foto_intern' => $this->request->foto_intern, 'arrival_date' => $this->request->arrival, 'departure_date' => $this->request->departure, 'arrival_eating' => $this->request->arrival_eating, 'departure_eating' => $this->request->departure_eating, 'notes' => $this->request->notes, 'amount' => $this->request->amount, 'payment_purpose' => $this->request->event->name . ' - Beitrag ' . $this->request->firstname . ' ' . $this->request->lastname, 'payment_method' => $this->request->paymentMethod, 'payment_options' => $paymentOptions, 'efz_status' => $participantAge->isfullAged() ? EfzStatus::EFZ_STATUS_NOT_CHECKED : EfzStatus::EFZ_STATUS_NOT_REQUIRED, ] ); $this->persistParticipationOptions($response->participant, $participationOptionRows); $this->persistAddons($response->participant); $response->success = true; return $response; } /** * Speichert die gewählten Zusätze (Event-Addons) als Snapshot-Zeilen (Titel/Preis/Betrag) für die spätere * Rechnung. Die Beträge sind bereits in der Controller-Auflösung (`resolveAddons`) berechnet, inkl. USt. */ private function persistAddons(\App\Models\EventParticipant $participant): void { foreach ($this->request->addonItems as $item) { $participant->selectedAddons()->create([ 'tenant' => $this->request->event->tenant, 'event_id' => $this->request->event->id, 'addon_key' => $item['key'], 'title' => $item['title'], 'price' => $item['price'] ?? 0, 'flat' => (bool)($item['flat'] ?? false), 'days' => $item['days'] ?? 0, 'amount' => $item['amount'] ?? 0, ]); } } /** * Validiert die Antworten gegen die am Event definierten Teilnahmeoptionen und erzeugt die zu speichernden * Zeilen mit Label-Snapshots. Gibt null zurück, wenn eine Pflicht-Frage unbeantwortet oder eine Antwort * ungültig ist (Absicherung gegen manipulierte Payloads). * * @return array|null */ private function buildParticipationOptionRows(): ?array { $rows = []; foreach ($this->request->event->participation_options ?? [] as $question) { $key = $question['key'] ?? null; if ($key === null) { continue; } $value = $this->request->participationOptions[$key] ?? null; if ($value === null || $value === '') { return null; } $option = null; foreach ($question['options'] ?? [] as $candidate) { if (($candidate['value'] ?? null) === $value) { $option = $candidate; break; } } if ($option === null) { return null; } $rows[] = [ 'question_key' => $key, 'question_label' => $question['label'] ?? $key, 'value' => $value, 'value_label' => $option['label'] ?? $value, ]; } return $rows; } /** * @param array $rows */ private function persistParticipationOptions(\App\Models\EventParticipant $participant, array $rows): void { foreach ($rows as $row) { $participant->selectedOptions()->create(array_merge($row, [ 'tenant' => $this->request->event->tenant, 'event_id' => $this->request->event->id, ])); } } }