From daff70b4fe60f73b098525225703a97e41259b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Wed, 5 Aug 2026 16:53:35 +0200 Subject: [PATCH 1/5] Payment methode now selectable --- .../Event/Actions/SignUp/SignUpCommand.php | 14 ++ .../Event/Actions/SignUp/SignUpRequest.php | 2 +- .../Event/Actions/SignUp/SignUpResponse.php | 1 + .../Event/Controllers/SignupController.php | 32 ++- .../Event/Views/Partials/CommonSettings.vue | 3 - .../Views/Partials/SignUpForm/SignupForm.vue | 16 +- .../components/PaymentMethodInputs.vue | 47 ++++ .../SignUpForm/composables/useSignupForm.js | 80 +++++-- .../SignUpForm/steps/StepPaymentMethod.vue | 223 ++++++++++++++++++ .../Partials/SignUpForm/steps/StepSummary.vue | 41 +++- app/Domains/Event/Views/Signup.vue | 2 - .../AbstractEventPaymentModule.php | 90 +++++-- app/EventPaymentModules/CLAUDE.md | 24 +- .../EventPaymentModule.php | 9 +- .../Modules/AccountTransferPaymentModule.php | 1 + app/Models/EventParticipant.php | 2 + app/Models/PaymentMethod.php | 12 +- .../AvailablePaymentMethodResource.php | 2 + app/Resources/EventResource.php | 2 +- ..._payment_options_to_event_participants.php | 22 ++ tests/Feature/SignUpPaymentOptionsTest.php | 118 +++++++++ tests/Unit/EventPaymentModuleRegistryTest.php | 43 ++++ 22 files changed, 705 insertions(+), 81 deletions(-) create mode 100644 app/Domains/Event/Views/Partials/SignUpForm/components/PaymentMethodInputs.vue create mode 100644 app/Domains/Event/Views/Partials/SignUpForm/steps/StepPaymentMethod.vue create mode 100644 database/migrations/2026_08_05_140010_add_payment_options_to_event_participants.php create mode 100644 tests/Feature/SignUpPaymentOptionsTest.php 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}) +} + + + + + 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 @@ + + + + + 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 @@ + + + + diff --git a/resources/js/constants/paymentMethodIcons.js b/resources/js/constants/paymentMethodIcons.js new file mode 100644 index 0000000..0bffb79 --- /dev/null +++ b/resources/js/constants/paymentMethodIcons.js @@ -0,0 +1,17 @@ +// Kuratierte Liste zahlungsrelevanter Font-Awesome-Solid-Icons für die Symbol-Auswahl einer +// Zahlungsart. Die Namen sind über app/Views/Components/Icon.vue (free-solid) gedeckt und werden +// beim Build gebündelt (kein Netzwerk-Load). value === icon (FA-Solid-Name im kebab-case). +export const PAYMENT_METHOD_ICONS = [ + {value: 'building-columns', label: 'Bank / Überweisung', icon: 'building-columns'}, + {value: 'money-bill-wave', label: 'Bargeld', icon: 'money-bill-wave'}, + {value: 'credit-card', label: 'Kreditkarte', icon: 'credit-card'}, + {value: 'wallet', label: 'Geldbörse', icon: 'wallet'}, + {value: 'coins', label: 'Münzen', icon: 'coins'}, + {value: 'euro-sign', label: 'Euro', icon: 'euro-sign'}, + {value: 'hand-holding-dollar', label: 'Spende', icon: 'hand-holding-dollar'}, + {value: 'sack-dollar', label: 'Geldsack', icon: 'sack-dollar'}, + {value: 'money-check-dollar', label: 'Scheck', icon: 'money-check-dollar'}, + {value: 'receipt', label: 'Beleg', icon: 'receipt'}, + {value: 'piggy-bank', label: 'Sparschwein', icon: 'piggy-bank'}, + {value: 'mobile-screen', label: 'Mobil / App', icon: 'mobile-screen'}, +] diff --git a/tests/Feature/PaymentMethodConfigurationTest.php b/tests/Feature/PaymentMethodConfigurationTest.php index c7127a4..e46f003 100644 --- a/tests/Feature/PaymentMethodConfigurationTest.php +++ b/tests/Feature/PaymentMethodConfigurationTest.php @@ -4,13 +4,12 @@ namespace Tests\Feature; use App\Domains\Admin\Actions\UpdateAvailablePaymentMethod\UpdateAvailablePaymentMethodAction; use App\Domains\Admin\Actions\UpdateAvailablePaymentMethod\UpdateAvailablePaymentMethodRequest; -use App\Domains\Event\Actions\UpdateEvent\UpdateEventCommand; -use App\Domains\Event\Actions\UpdateEvent\UpdateEventRequest; +use App\Domains\Event\Actions\SetPaymentMethods\SetPaymentMethodsCommand; +use App\Domains\Event\Actions\SetPaymentMethods\SetPaymentMethodsRequest; use App\Models\AvailablePaymentMethod; use App\Models\Event; use App\Models\PaymentMethod; use App\Models\Tenant; -use App\ValueObjects\Amount; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -156,26 +155,9 @@ class PaymentMethodConfigurationTest extends TestCase */ private function runUpdateEvent(Event $event, array $slugs, array $configs = []): void { - $request = new UpdateEventRequest( - $event, - $event->name, - $event->location, - $event->postal_code, - $event->email, - new \DateTime('2026-07-01'), - new \DateTime('2026-07-20'), - 16, - false, - false, - Amount::fromString('0'), - Amount::fromString('0'), - [], - [], - $slugs, - $configs, - ); + $request = new SetPaymentMethodsRequest($event, $slugs, $configs); - $response = new UpdateEventCommand($request)->execute(); + $response = new SetPaymentMethodsCommand($request)->execute(); $this->assertTrue($response->success, $response->message); } } From 7f8417e915bd85fc698ef6c496f2f1f977afc3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Wed, 5 Aug 2026 18:08:04 +0200 Subject: [PATCH 3/5] Small improvements --- .../CreateTenant/CreateTenantAction.php | 3 +- .../UpdateTenantPaymentAction.php | 38 ++++++++++++++++++- .../Views/Partials/TenantPaymentMethods.vue | 10 ++++- app/Domains/Admin/Views/TenantData.vue | 4 +- app/Domains/Admin/Views/TenantEdit.vue | 2 +- .../Views/Partials/ParticipationFees.vue | 8 ++++ .../AbstractEventPaymentModule.php | 10 +++++ app/EventPaymentModules/CLAUDE.md | 22 ++++++++--- .../EventPaymentModule.php | 7 ++++ .../Modules/AccountTransferPaymentModule.php | 5 +++ .../Modules/UndefinedPaymentModule.php | 7 +++- app/Models/PaymentMethod.php | 10 +++-- 12 files changed, 110 insertions(+), 16 deletions(-) diff --git a/app/Domains/Admin/Actions/CreateTenant/CreateTenantAction.php b/app/Domains/Admin/Actions/CreateTenant/CreateTenantAction.php index 9d20ed8..177c357 100644 --- a/app/Domains/Admin/Actions/CreateTenant/CreateTenantAction.php +++ b/app/Domains/Admin/Actions/CreateTenant/CreateTenantAction.php @@ -33,7 +33,7 @@ class CreateTenantAction $paymentMethodDefaults = PaymentMethod::defaults(); foreach (PaymentMethod::all() as $paymentMethod) { - $defaults = $paymentMethodDefaults[$paymentMethod->slug] ?? ['name' => $paymentMethod->slug, 'description' => null]; + $defaults = $paymentMethodDefaults[$paymentMethod->slug] ?? ['name' => $paymentMethod->slug, 'description' => null, 'configuration' => []]; AvailablePaymentMethod::create([ 'tenant' => $tenant->slug, @@ -41,6 +41,7 @@ class CreateTenantAction 'name' => $defaults['name'], 'description' => $defaults['description'], 'active' => true, + 'configuration' => PaymentMethod::sanitizeConfiguration($paymentMethod->slug, $defaults['configuration'] ?? []), ]); } diff --git a/app/Domains/Admin/Actions/UpdateTenantPayment/UpdateTenantPaymentAction.php b/app/Domains/Admin/Actions/UpdateTenantPayment/UpdateTenantPaymentAction.php index 7ec581a..343e883 100644 --- a/app/Domains/Admin/Actions/UpdateTenantPayment/UpdateTenantPaymentAction.php +++ b/app/Domains/Admin/Actions/UpdateTenantPayment/UpdateTenantPaymentAction.php @@ -2,6 +2,10 @@ namespace App\Domains\Admin\Actions\UpdateTenantPayment; +use App\Models\AvailablePaymentMethod; +use App\Models\PaymentMethod; +use App\Scopes\SiteScope; + class UpdateTenantPaymentAction { public function __construct(private UpdateTenantPaymentRequest $request) @@ -18,9 +22,41 @@ class UpdateTenantPaymentAction 'account_name' => $this->request->accountName, ]); + $this->syncTransferPaymentConfiguration(); + $response->success = true; - $response->message = 'Bezahldaten wurden gespeichert.'; + $response->message = 'IBAN-Informationen wurden gespeichert.'; return $response; } + + /** + * Übernimmt die eingegebenen IBAN-Daten direkt in die Tenant-Config der Überweisungs-Zahlungsart + * (Kontoinhaber/IBAN/BIC), damit sie nicht doppelt gepflegt werden müssen. Bestehende weitere + * Config-Werte (z.B. Symbol) bleiben erhalten. Der Ziel-Tenant kann ein verwalteter sein, daher + * ohne SiteScope explizit auf den Tenant-Slug gefiltert. + */ + private function syncTransferPaymentConfiguration(): void + { + $slug = PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION; + + $method = AvailablePaymentMethod::withoutGlobalScope(SiteScope::class) + ->where('tenant', $this->request->tenant->slug) + ->where('slug', $slug) + ->first(); + + if ($method === null) { + return; + } + + $method->configuration = PaymentMethod::sanitizeConfiguration($slug, array_merge( + $method->configuration ?? [], + [ + 'account_owner' => $this->request->accountName, + 'iban' => $this->request->accountIban, + 'bic' => $this->request->accountBic, + ], + )); + $method->save(); + } } diff --git a/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue b/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue index d960339..1f60daa 100644 --- a/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue +++ b/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue @@ -92,7 +92,7 @@ async function save() { @@ -416,4 +417,11 @@ onMounted(async () => { color: #ef4444; margin-left: 2px; } + +.option-hint { + display: block; + margin-top: 4px; + font-size: 0.8rem; + color: #6b7280; +} diff --git a/app/EventPaymentModules/AbstractEventPaymentModule.php b/app/EventPaymentModules/AbstractEventPaymentModule.php index 780db01..e77f260 100644 --- a/app/EventPaymentModules/AbstractEventPaymentModule.php +++ b/app/EventPaymentModules/AbstractEventPaymentModule.php @@ -22,6 +22,16 @@ abstract class AbstractEventPaymentModule implements EventPaymentModule return null; } + /** + * Standard-Konfiguration beim Anlegen der Tenant-Instanz. Standard: leer. + * + * @return array + */ + public function defaultConfiguration(): array + { + return []; + } + /** * Payer-seitige Eingaben, die eine Zahlungsart vom Teilnehmer benötigt (z.B. später SEPA-IBAN, * PayPal-Mail). Standard: keine. Gleiche Form wie getOptions(): [{name,label,type,required}]. diff --git a/app/EventPaymentModules/CLAUDE.md b/app/EventPaymentModules/CLAUDE.md index c59f547..f8d3e3a 100644 --- a/app/EventPaymentModules/CLAUDE.md +++ b/app/EventPaymentModules/CLAUDE.md @@ -27,13 +27,20 @@ Rechnung) liegt gekapselt in einem Modul pro Zahlungsart. Aufgelöst wird über Abgesichert über `sanitizeParticipantOptions()` / `participantOptionsComplete()` (Guard im `SignUpCommand`). - `type` ist i.d.R. `'string'`; `'richtext'` wird über `Views/Components/TextEditor.vue` (TinyMCE, HTML) gerendert - und via `v-html`/`{!! !!}` ausgegeben. Teilnehmer-Eingaben rendert die generische - `SignUpForm/components/PaymentMethodInputs.vue` schema-getrieben. + und via `v-html`/`{!! !!}` ausgegeben; `'icon'` rendert die `Views/Components/RichSelectBox.vue` (kuratierte + FA-Symbol-Auswahl, Liste in `resources/js/constants/paymentMethodIcons.js`). Teilnehmer-Eingaben rendert die + generische `SignUpForm/components/PaymentMethodInputs.vue` schema-getrieben. + - Optionaler Schlüssel `'hint'` je Option: erklärender Hilfetext, den die Admin-Render-Stellen unter dem Feld anzeigen + (z.B. bei `payment_information`, dass der Text am Anmeldeende + in der Mail erscheint). + - `defaultConfiguration()` je Modul liefert die Start-Config beim Anlegen der Tenant-Instanz (`CreateTenantAction`), + aktuell das Default-Symbol (`icon`): Überweisung `building-columns`, Sonstiges `coins`. - **Aktivierungs-Guard:** Eine Tenant-Zahlungsmethode darf nur `active` werden, wenn alle `required`-Optionen befüllt sind (`isConfigurationComplete()`), erzwungen in `UpdateAvailablePaymentMethodAction`. - **Config-Speicherung (JSON):** pro Tenant auf `available_payment_methods.configuration`, pro Event auf dem Pivot `event_payment_methods.configuration`. Beim Zuweisen an ein Event wird die Tenant-Config als **Snapshot kopiert** - (Copy-on-Assign, spätere Tenant-Änderungen wirken NICHT nach). Sync erfolgt differenziell in `UpdateEventCommand`. + (Copy-on-Assign, spätere Tenant-Änderungen wirken NICHT nach). Sync erfolgt differenziell in + `SetPaymentMethodsCommand` (Endpoint `/api/v1/event/details/{event}/payment-methods`, ausgelöst aus dem + „Teilnahmegebühren"-Bereich). - **`PaymentMethod` ist nur eine Fassade:** `PaymentMethod::optionsFor/participantOptionsFor/requiredOptionKeys/` `sanitizeConfiguration/isConfigurationComplete/defaults()` delegieren an die Registry. Bestehende Aufrufstellen bleiben stabil. @@ -94,7 +101,8 @@ Beispiel GiroCode (nur Überweisung): ## Neues Zahlungsmodul hinzufügen 1. Klasse unter `Modules/` anlegen, `extends AbstractEventPaymentModule`; `slug()`, `defaultName()`, `getOptions()` - implementieren; `registrationSummary()`/`doPayment()`/`createInvoice()` überschreiben, wo nötig. + implementieren; `defaultConfiguration()` (z.B. Default-`icon`) sowie + `registrationSummary()`/`doPayment()`/`createInvoice()` überschreiben, wo nötig. 2. Zahlart-spezifische Extras als **eigenes Fähigkeits-Interface** (nicht ins Core-Interface). 3. In `EventPaymentModuleRegistry::MODULES` eintragen. `PaymentMethod::create(['slug' => …])` wird dann automatisch über `ProductionDataSeeder` (iteriert `EventPaymentModuleRegistry::slugs()`) geseedet. @@ -102,8 +110,10 @@ Beispiel GiroCode (nur Überweisung): ## Datenübernahme -`storage/app/sync_payment_bank_data.php` überführt einmalig Bestands-Bankdaten (Tenant/Event) in die Modul-Config -(idempotent). Bei Live-Inbetriebnahme einmal ausführen. +`storage/app/2026_08_05_backfill_payment_method_configuration.sql` überführt einmalig Bestands-IBAN-Daten (Tenant/Event) +**und** die Default-Symbole in die Modul-Config (idempotenter JSON-Merge, nichts wird überschrieben). Bei +Live-Inbetriebnahme einmal gegen die Produktionsdatenbank ausführen — ersetzt das ältere PHP-Skript +`sync_payment_bank_data.php`. ## Tests diff --git a/app/EventPaymentModules/EventPaymentModule.php b/app/EventPaymentModules/EventPaymentModule.php index c7b0d2a..503af88 100644 --- a/app/EventPaymentModules/EventPaymentModule.php +++ b/app/EventPaymentModules/EventPaymentModule.php @@ -29,6 +29,13 @@ interface EventPaymentModule /** Standard-Beschreibung beim Anlegen der Tenant-Instanz. */ public function defaultDescription(): ?string; + /** + * Standard-Konfiguration beim Anlegen der Tenant-Instanz (z.B. Default-Symbol). Standard: leer. + * + * @return array + */ + public function defaultConfiguration(): array; + /** * Admin-Options-Schema dieses Moduls (welche Konfigurationsfelder der/die Veranstalter\*in pflegt). * diff --git a/app/EventPaymentModules/Modules/AccountTransferPaymentModule.php b/app/EventPaymentModules/Modules/AccountTransferPaymentModule.php index 9d14c0c..81af531 100644 --- a/app/EventPaymentModules/Modules/AccountTransferPaymentModule.php +++ b/app/EventPaymentModules/Modules/AccountTransferPaymentModule.php @@ -27,6 +27,11 @@ class AccountTransferPaymentModule extends AbstractEventPaymentModule implements return 'Überweisung auf Veranstaltungskonto'; } + public function defaultConfiguration(): array + { + return ['icon' => 'building-columns']; + } + public function getOptions(): array { return [ diff --git a/app/EventPaymentModules/Modules/UndefinedPaymentModule.php b/app/EventPaymentModules/Modules/UndefinedPaymentModule.php index 307bc75..268772f 100644 --- a/app/EventPaymentModules/Modules/UndefinedPaymentModule.php +++ b/app/EventPaymentModules/Modules/UndefinedPaymentModule.php @@ -25,10 +25,15 @@ class UndefinedPaymentModule extends AbstractEventPaymentModule return 'Sonstiges (Barzahlung, Zahlung vor Ort)'; } + public function defaultConfiguration(): array + { + return ['icon' => 'coins']; + } + public function getOptions(): array { return [ - ['name' => 'payment_information', 'label' => 'Zahlungsinformationen', 'type' => 'richtext', 'required' => true], + ['name' => 'payment_information', 'label' => 'Zahlungsinformationen', 'type' => 'richtext', 'required' => true, 'hint' => 'Dieser Text wird der teilnehmenden Person am Ende der Anmeldung und in der Bestätigungs-E-Mail angezeigt.'], ['name' => 'icon', 'label' => 'Symbol', 'type' => 'icon', 'required' => false], ]; } diff --git a/app/Models/PaymentMethod.php b/app/Models/PaymentMethod.php index 416fed1..d2d7023 100644 --- a/app/Models/PaymentMethod.php +++ b/app/Models/PaymentMethod.php @@ -29,15 +29,19 @@ class PaymentMethod extends CommonModel ]; /** - * Standard-Werte (name/description) je Slug -- aus den Zahlungsmodulen aufgebaut. + * Standard-Werte (name/description/configuration) je Slug -- aus den Zahlungsmodulen aufgebaut. * - * @return array + * @return array}> */ public static function defaults(): array { $defaults = []; foreach (EventPaymentModuleRegistry::all() as $slug => $module) { - $defaults[$slug] = ['name' => $module->defaultName(), 'description' => $module->defaultDescription()]; + $defaults[$slug] = [ + 'name' => $module->defaultName(), + 'description' => $module->defaultDescription(), + 'configuration' => $module->defaultConfiguration(), + ]; } return $defaults; From 67c4e69f7b4c29d2eff2f0dcfd14dec8edfb0d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Wed, 5 Aug 2026 19:12:49 +0200 Subject: [PATCH 4/5] Overview of active payment methode --- .../Event/Views/Partials/ParticipantsList.vue | 14 +++++++++++--- .../SignUpForm/components/PaymentMethodInputs.vue | 6 ++++++ version | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/Domains/Event/Views/Partials/ParticipantsList.vue b/app/Domains/Event/Views/Partials/ParticipantsList.vue index 0859993..93ea9be 100644 --- a/app/Domains/Event/Views/Partials/ParticipantsList.vue +++ b/app/Domains/Event/Views/Partials/ParticipantsList.vue @@ -1,11 +1,11 @@