diff --git a/app/Domains/Event/Actions/SignUp/SignUpCommand.php b/app/Domains/Event/Actions/SignUp/SignUpCommand.php index 48667a4..f4ad093 100644 --- a/app/Domains/Event/Actions/SignUp/SignUpCommand.php +++ b/app/Domains/Event/Actions/SignUp/SignUpCommand.php @@ -4,6 +4,7 @@ namespace App\Domains\Event\Actions\SignUp; use App\Enumerations\EatingHabit; use App\Enumerations\EfzStatus; +use App\EventPaymentModules\EventPaymentModuleRegistry; use App\ValueObjects\Age; use Illuminate\Support\Str; @@ -14,6 +15,18 @@ class SignUpCommand { public function execute() : SignUpResponse { $response = new SignUpResponse(); + // Teilnehmer-Eingaben der gewählten Zahlungsart gegen das Modul-Schema absichern. + $module = $this->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; + } + $eatingHabit = match ($this->request->eating_habit) { 'vegan' => EatingHabit::EATING_HABIT_VEGAN, 'vegetarian' => EatingHabit::EATING_HABIT_VEGETARIAN, @@ -62,6 +75,7 @@ class SignUpCommand { '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, ] ); diff --git a/app/Domains/Event/Actions/SignUp/SignUpRequest.php b/app/Domains/Event/Actions/SignUp/SignUpRequest.php index fc6d648..c64979b 100644 --- a/app/Domains/Event/Actions/SignUp/SignUpRequest.php +++ b/app/Domains/Event/Actions/SignUp/SignUpRequest.php @@ -2,7 +2,6 @@ namespace App\Domains\Event\Actions\SignUp; -use App\Enumerations\ParticipationType; use App\Models\Event; use App\Models\Tenant; use App\ValueObjects\Amount; @@ -46,6 +45,7 @@ class SignUpRequest { public ?string $notes, public Amount $amount, public ?string $paymentMethod, + public array $paymentOptions = [], ) { } } diff --git a/app/Domains/Event/Actions/SignUp/SignUpResponse.php b/app/Domains/Event/Actions/SignUp/SignUpResponse.php index cc5e94a..2fa875f 100644 --- a/app/Domains/Event/Actions/SignUp/SignUpResponse.php +++ b/app/Domains/Event/Actions/SignUp/SignUpResponse.php @@ -7,6 +7,7 @@ use App\Models\EventParticipant; class SignUpResponse { public bool $success; public ?EventParticipant $participant; + public ?string $message = null; public function __construct() { $this->success = false; diff --git a/app/Domains/Event/Controllers/SignupController.php b/app/Domains/Event/Controllers/SignupController.php index 01f82d1..643d0d6 100644 --- a/app/Domains/Event/Controllers/SignupController.php +++ b/app/Domains/Event/Controllers/SignupController.php @@ -26,10 +26,8 @@ class SignupController extends CommonController { $eventModel = $this->events->getByIdentifier($eventId, false); $event = $eventModel?->toResource()->toArray($request); - // Aktuell ist genau eine aktive Zahlungsmethode je Event vorgesehen; wird bereits beim Start - // der Anmeldung ermittelt und ans Frontend durchgereicht, statt erst bei der Erstellung des - // Teilnehmenden (siehe SignUpCommand). - $paymentMethod = $eventModel?->paymentMethods()->where('active', true)->first(); + // Die (aktiven) Zahlungsmethoden stehen dem Frontend über $event['paymentMethods'] zur Verfügung; + // die Auswahl trifft der/die Teilnehmer\*in im Anmeldeschritt "Zahlungsart". $participantData = [ 'firstname' => '', @@ -67,7 +65,6 @@ class SignupController extends CommonController { 'availableEvents' => $availableEvents, 'localGroups' => $event['contributingLocalGroups'], 'participantData' => $participantData, - 'paymentMethod' => $paymentMethod?->slug, ]); return $inertiaProvider->render(); } @@ -144,11 +141,16 @@ class SignupController extends CommonController { $registrationData['anmerkungen'], $amount, $registrationData['paymentMethod'] ?? null, + (array)($registrationData['paymentOptions'] ?? []), ); $signupCommand = new SignUpCommand($signupRequest); $signupResponse = $signupCommand->execute(); + if (!$signupResponse->success) { + return response()->json(['status' => 'error', 'message' => $signupResponse->message], 422); + } + // 4. Addons registrieren @@ -182,14 +184,18 @@ class SignupController extends CommonController { $siblingReduction = $request->input('sibling') === 'true'; - return response()->json(['amount' => - $event->calculateAmount( - $request->input('participationType'), - $request->input('beitrag'), - \DateTime::createFromFormat('Y-m-d', $request->input('arrival')), - \DateTime::createFromFormat('Y-m-d', $request->input('departure')), - $siblingReduction - )->toString() + $amount = $event->calculateAmount( + $request->input('participationType'), + $request->input('beitrag'), + \DateTime::createFromFormat('Y-m-d', $request->input('arrival')), + \DateTime::createFromFormat('Y-m-d', $request->input('departure')), + $siblingReduction + ); + + // amountValue (numerisch) steuert im Frontend das Überspringen des Zahlungsart-Schritts bei 0 €. + return response()->json([ + 'amount' => $amount->toString(), + 'amountValue' => $amount->getAmount(), ]); } } diff --git a/app/Domains/Event/Views/Partials/CommonSettings.vue b/app/Domains/Event/Views/Partials/CommonSettings.vue index c5ac008..78e7aec 100644 --- a/app/Domains/Event/Views/Partials/CommonSettings.vue +++ b/app/Domains/Event/Views/Partials/CommonSettings.vue @@ -73,9 +73,6 @@ const emit = defineEmits(['close']) if (paymentMethods.value.length === 0) { toast.error('Mindestens eine Zahlungsmöglichkeit muss ausgewählt sein.') return - } else if(paymentMethods.value.length > 1) { - toast.error('Aktuell wird nur exakt eine Zahlungseinstellung unterstützt.') - return } const response = await request('/api/v1/event/details/' + props.event.id + '/common-settings', { diff --git a/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue b/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue index af4b0f3..3d5e9ab 100644 --- a/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue +++ b/app/Domains/Event/Views/Partials/SignUpForm/SignupForm.vue @@ -1,5 +1,5 @@ @@ -94,11 +95,14 @@ const steps = [ + +import TextEditor from "../../../../../../Views/Components/TextEditor.vue"; + +// Generische, schema-getriebene Eingabefelder eines Zahlungsmoduls. Wiederverwendbar für künftige +// Verfahren (SEPA, PayPal): das jeweilige Modul liefert das Schema, hier wird nur gerendert. +const props = defineProps({ + // [{ name, label, type, required }] -- z.B. aus participantOptionsSchema einer Zahlungsmethode. + schema: {type: Array, default: () => []}, + // v-model: Objekt { optionName: value } + modelValue: {type: Object, default: () => ({})}, +}) +const emit = defineEmits(['update:modelValue']) + +function setValue(name, value) { + emit('update:modelValue', {...props.modelValue, [name]: value}) +} + + + + + + + {{ option.label }}*: + + + setValue(option.name, value)" + /> + setValue(option.name, event.target.value)" + /> + + + + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/composables/useSignupForm.js b/app/Domains/Event/Views/Partials/SignUpForm/composables/useSignupForm.js index 3f7cd32..5485f66 100644 --- a/app/Domains/Event/Views/Partials/SignUpForm/composables/useSignupForm.js +++ b/app/Domains/Event/Views/Partials/SignUpForm/composables/useSignupForm.js @@ -1,7 +1,7 @@ -import { ref, reactive, computed } from 'vue' +import {reactive, ref} from 'vue' import axios from 'axios' -export function useSignupForm(event, participantData, paymentMethod) { +export function useSignupForm(event, participantData) { const currentStep = ref(1) const submitting = ref(false) const summaryLoading = ref(false) @@ -9,9 +9,13 @@ export function useSignupForm(event, participantData, paymentMethod) { const selectedAddons = reactive({}) + // Für die Aktion freigeschaltete Zahlungsmethoden; ist genau eine aktiv, wird sie vorausgewählt. + const activeMethods = (event.paymentMethods ?? []).filter(m => m.active) + const formData = reactive({ eatingHabit: 'EATING_HABIT_VEGAN', - paymentMethod: paymentMethod ?? null, + paymentMethod: activeMethods.length === 1 ? activeMethods[0].slug : null, + paymentOptions: {}, userId: participantData.id, eventId: event.id, vorname: participantData.firstname ?? '', @@ -52,33 +56,52 @@ export function useSignupForm(event, participantData, paymentMethod) { }) const summaryAmount = ref('') + const summaryAmountValue = ref(0) + + const fetchAmount = async () => { + summaryLoading.value = true + try { + const res = await axios.post('/api/v1/event/' + event.id + '/calculate-amount', { + arrival: formData.arrival, + departure: formData.departure, + event_id: event.id, + participation_group: formData.participant_group, + selected_amount: formData.beitrag, + addons: selectedAddons, + participationType: formData.participationType, + beitrag: formData.beitrag, + sibling: formData.sibling, + }) + summaryAmount.value = res.data.amount + summaryAmountValue.value = Number(res.data.amountValue ?? 0) + } finally { + summaryLoading.value = false + } + } + + // Steps: ... 8 Allergien, 9 Zahlungsart, 10 Zusammenfassung. + const STEP_PAYMENT = 9 + const STEP_SUMMARY = 10 const goToStep = async (step) => { - if (step === 9) { - summaryLoading.value = true - summaryAmount.value = '' - try { - const res = await axios.post('/api/v1/event/' + event.id + '/calculate-amount', { - arrival: formData.arrival, - departure: formData.departure, - event_id: event.id, - participation_group: formData.participant_group, - selected_amount: formData.beitrag, - addons: selectedAddons, - participationType: formData.participationType, - beitrag: formData.beitrag, - sibling: formData.sibling, - }) - summaryAmount.value = res.data.amount - } finally { - summaryLoading.value = false + if (step === STEP_PAYMENT) { + // Betrag ermitteln: Bei 0 € wird die Zahlungsart-Auswahl übersprungen. + await fetchAmount() + if (summaryAmountValue.value <= 0) { + formData.paymentMethod = null + formData.paymentOptions = {} + currentStep.value = STEP_SUMMARY + return } + } else if (step === STEP_SUMMARY) { + await fetchAmount() } currentStep.value = step } const submit = async () => { - if (!formData.summary_information_correct || !formData.summary_accept_terms || !formData.legal_accepted || !formData.payment) { + // Zahlungs-Bestätigung wird im Summary-Schritt kontextabhängig erzwungen (nur bei Überweisung). + if (!formData.summary_information_correct || !formData.summary_accept_terms || !formData.legal_accepted) { return } submitting.value = true @@ -96,5 +119,16 @@ export function useSignupForm(event, participantData, paymentMethod) { } } - return { currentStep, goToStep, formData, selectedAddons, submit, submitting, submitResult, summaryLoading, summaryAmount } + return { + currentStep, + goToStep, + formData, + selectedAddons, + submit, + submitting, + submitResult, + summaryLoading, + summaryAmount, + summaryAmountValue + } } diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepPaymentMethod.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepPaymentMethod.vue new file mode 100644 index 0000000..dedc920 --- /dev/null +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepPaymentMethod.vue @@ -0,0 +1,223 @@ + + + + + Zahlungsweise + Bitte wähle, wie du deinen Teilnahmebeitrag + begleichen möchtest. + + + + + + + + {{ method.name }} + {{ method.description }} + + ✓ + + + + + + + + + ← Zurück + Weiter → + + + + + + diff --git a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue index a7da2a2..89eeb5f 100644 --- a/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue +++ b/app/Domains/Event/Views/Partials/SignUpForm/steps/StepSummary.vue @@ -1,15 +1,48 @@
Bitte wähle, wie du deinen Teilnahmebeitrag + begleichen möchtest.
{{ method.description }}