Files
2026-08-05 18:08:04 +02:00

8.7 KiB

Zahlungsmodule (app/EventPaymentModules)

Interface-gesteuerte Strategie-Schicht: Das Verhalten je Zahlungsart (Optionen, Anmelde-Zusammenfassung, Zahlung, Rechnung) liegt gekapselt in einem Modul pro Zahlungsart. Aufgelöst wird über den Slug.

Aufbau

  • EventPaymentModuleCore-Interface. Hält nur das, was jede Zahlungsart hat: slug(), defaultName()/defaultDescription(), getOptions(), registrationSummary(), doPayment(), createInvoice().
  • AbstractEventPaymentModule — Basisklasse (Template-Method). Liefert die aus getOptions() abgeleiteten Helfer (requiredOptionKeys(), sanitizeConfiguration(), isConfigurationComplete()) und sinnvolle Default-/Stub-Bodies.
  • Modules/ — konkrete Module (flach, eine Klasse je Zahlungsart): AccountTransferPaymentModule (Überweisung), UndefinedPaymentModule (Barzahlung/Sonstiges).
  • DTO/ — geteilte Request/Response-DTOs je Operation (DoPayment*, CreateInvoice*, RegistrationSummary*).
  • EventPaymentModuleRegistry — statische Map slug → Modul-Instanz (forSlug(), all(), slugs()). Neue Module hier eintragen. Kein Container-Binding.
  • ProvidesGiroCodeFähigkeits-Interface (siehe unten).

Kernregeln

  • Zwei getrennte Options-Schemata (beide im Code, gleiche Form ['name','label','type','required']):
    • getOptions() = Admin-Config (was der/die Veranstalter*in pflegt, z.B. Empfänger-Konto). Werte in der DB (configuration, s.u.).
    • getParticipantOptions() = Teilnehmer-Eingaben beim Anmelden (payer-seitig, z.B. künftig SEPA-IBAN/PayPal-Mail; beide Bestandsmodule: []). Werte in event_participants.payment_options (JSON). 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; '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 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.
  • Zahlungsauswahl im Anmeldeprozess: Nach „Allergien" wählt der/die Teilnehmer*in aus den aktiven Event-Methoden (StepPaymentMethod.vue); bei Beitrag 0 € wird der Schritt übersprungen. Die Wahl landet in event_participants.payment_method, die Eingaben in payment_options. Der Überweisungs-Bestätigungstext in der Zusammenfassung ist eine Admin-Config-Option summary_confirmation_text ({amount}-Platzhalter) und erscheint nur bei PAYMENT_ACCOUNT_TRANSACTION.
  • PaymentStatus ist ein DB-gestütztes Enumerations-Model (app/Enumerations/PaymentStatus.php, Tabelle payment_status, geseedet in ProductionDataSeeder), analog zu InvoiceStatus. Reine Steuersignale (z.B. Redirect) gehören NICHT ins Status-Vokabular, sondern als transiente Felder aufs Response-DTO.
  • doPayment() und createInvoice() sind aktuell Stubs (nur Struktur/DTOs vorhanden). createInvoice() ist als Template-Method angelegt: gemeinsamer Rumpf in der Basis, invoiceClosingStatement() je Modul.

Zahlart-spezifisches Verhalten → Fähigkeits-Interfaces (Interface Segregation)

Verhalten, das nur eine Zahlungsart hat, gehört nicht auf EventPaymentModule und nicht auf das generische EventParticipant-Model, sondern in ein schmales Fähigkeits-Interface, das nur das betreffende Modul implementiert. Aufrufer prüfen per instanceof.

Beispiel GiroCode (nur Überweisung):

  • ProvidesGiroCode::giroCode(EventParticipant, array $configuration): ?string — implementiert nur von AccountTransferPaymentModule.
  • Aufrufer (GiroCodeGetController, EventSignUpSuccessfullMail, ParticipantPaymentMissingPaymentMail) lösen inline auf:
    $module = $participant->paymentModule();
    $binary = $module instanceof ProvidesGiroCode
        ? $module->giroCode($participant, $participant->paymentConfiguration())
        : null;
    
  • Künftige Verfahren würden analog eigene Fähigkeiten mitbringen (z.B. ProvidesRedirect für PayPal, ProvidesMandate für SEPA-Lastschrift) — erst modellieren, wenn tatsächlich gebraucht.

Anmelde-Zusammenfassung / Mail-Anzeige

  • registrationSummary(RegistrationSummaryRequest): RegistrationSummaryResponse liefert den zahlungsspezifischen Anzeige-Block fertig als HTML (hasPaymentInformation + html). Jedes Modul rendert seinen Block über eigene Blade-Templates unter resources/views/payment-modules/{modul}/{web,mail}.blade.php (Überweisung: account-transfer/, Barzahlung: undefined/ — Rahmen um den konfigurierten richtext). Die Render-Stellen geben nur noch v-html / {!! !!} aus — keine zahlart-spezifische Logik im Frontend/Blade.
  • Render-Kontext: RegistrationSummaryRequest trägt einen RegistrationRenderContext (Web/Mail). Das Modul wählt darüber (a) ein medien-gerechtes Templateresources/views/payment-modules/{modul}/{web,mail}.blade.php (Web: SPA-Klassen form-table/link; Mail: E-Mail-taugliche Inline-Styles) und (b) die GiroCode-Bildquelle — Web: /print-girocode/{token} (sessionlos, lazy), Mail: cid:girocode.png (inline via $message->embedData(...), bereitgestellt im dünnen Wrapper emails/subparts/payment.blade.php). Hinweis: Das Web-Template darf globale (un-scoped) SPA-Klassen nutzen, da Vue-scoped-Styles nicht auf v-html greifen.
  • Zugang über das Teilnehmer-Model: EventParticipant::paymentModule(), paymentConfiguration() (eager-load-fähig über ->with('event.paymentMethods'), kein statischer Cache), paymentSummary(RegistrationRenderContext).
  • Konsumiert von EventParticipantResource (paymentSummary = {hasPaymentInformation, html}, Web-Kontext), SubmitSuccess.vue, den Mails (EventSignUpSuccessfullMail/ParticipantPaymentMissingPaymentMail rufen paymentSummary(Mail)), sowie signup_complete/missing_amount (nur umrahmende Prosa, gegated auf hasPaymentInformation).

Neues Zahlungsmodul hinzufügen

  1. Klasse unter Modules/ anlegen, extends AbstractEventPaymentModule; slug(), defaultName(), getOptions() 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.
  4. Slug-Konstante bei Bedarf auf PaymentMethod ergänzen.

Datenübernahme

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

tests/Unit/PaymentMethodOptionsTest, tests/Unit/EventPaymentModuleRegistryTest, tests/Unit/RegistrationSummaryTest, tests/Feature/PaymentMethodConfigurationTest, tests/Feature/EventParticipantPaymentSummaryTest. Ausführung im Container (PHP 8.5): docker exec mareike-mareike-app-1 php artisan test.