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
EventPaymentModule— Core-Interface. Hält nur das, was jede Zahlungsart hat:slug(),defaultName()/defaultDescription(),getOptions(),registrationSummary(),doPayment(),createInvoice().AbstractEventPaymentModule— Basisklasse (Template-Method). Liefert die ausgetOptions()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 Mapslug → Modul-Instanz(forSlug(),all(),slugs()). Neue Module hier eintragen. Kein Container-Binding.ProvidesGiroCode— Fä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 inevent_participants.payment_options(JSON). Abgesichert übersanitizeParticipantOptions()/participantOptionsComplete()(Guard imSignUpCommand).typeist i.d.R.'string';'richtext'wird überViews/Components/TextEditor.vue(TinyMCE, HTML) gerendert und viav-html/{!! !!}ausgegeben;'icon'rendert dieViews/Components/RichSelectBox.vue(kuratierte FA-Symbol-Auswahl, Liste inresources/js/constants/paymentMethodIcons.js). Teilnehmer-Eingaben rendert die generischeSignUpForm/components/PaymentMethodInputs.vueschema-getrieben.- Optionaler Schlüssel
'hint'je Option: erklärender Hilfetext, den die Admin-Render-Stellen unter dem Feld anzeigen (z.B. beipayment_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): Überweisungbuilding-columns, Sonstigescoins.
- Aktivierungs-Guard: Eine Tenant-Zahlungsmethode darf nur
activewerden, wenn allerequired-Optionen befüllt sind (isConfigurationComplete()), erzwungen inUpdateAvailablePaymentMethodAction. - Config-Speicherung (JSON): pro Tenant auf
available_payment_methods.configuration, pro Event auf dem Pivotevent_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 inSetPaymentMethodsCommand(Endpoint/api/v1/event/details/{event}/payment-methods, ausgelöst aus dem „Teilnahmegebühren"-Bereich). PaymentMethodist 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 inevent_participants.payment_method, die Eingaben inpayment_options. Der Überweisungs-Bestätigungstext in der Zusammenfassung ist eine Admin-Config-Optionsummary_confirmation_text({amount}-Platzhalter) und erscheint nur beiPAYMENT_ACCOUNT_TRANSACTION. PaymentStatusist ein DB-gestütztes Enumerations-Model (app/Enumerations/PaymentStatus.php, Tabellepayment_status, geseedet inProductionDataSeeder), analog zuInvoiceStatus. Reine Steuersignale (z.B. Redirect) gehören NICHT ins Status-Vokabular, sondern als transiente Felder aufs Response-DTO.doPayment()undcreateInvoice()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 vonAccountTransferPaymentModule.- 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.
ProvidesRedirectfür PayPal,ProvidesMandatefür SEPA-Lastschrift) — erst modellieren, wenn tatsächlich gebraucht.
Anmelde-Zusammenfassung / Mail-Anzeige
registrationSummary(RegistrationSummaryRequest): RegistrationSummaryResponseliefert den zahlungsspezifischen Anzeige-Block fertig als HTML (hasPaymentInformation+html). Jedes Modul rendert seinen Block über eigene Blade-Templates unterresources/views/payment-modules/{modul}/{web,mail}.blade.php(Überweisung:account-transfer/, Barzahlung:undefined/— Rahmen um den konfigurierten richtext). Die Render-Stellen geben nur nochv-html/{!! !!}aus — keine zahlart-spezifische Logik im Frontend/Blade.- Render-Kontext:
RegistrationSummaryRequestträgt einenRegistrationRenderContext(Web/Mail). Das Modul wählt darüber (a) ein medien-gerechtes Template —resources/views/payment-modules/{modul}/{web,mail}.blade.php(Web: SPA-Klassenform-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 Wrapperemails/subparts/payment.blade.php). Hinweis: Das Web-Template darf globale (un-scoped) SPA-Klassen nutzen, da Vue-scoped-Styles nicht aufv-htmlgreifen. - 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/ParticipantPaymentMissingPaymentMailrufenpaymentSummary(Mail)), sowiesignup_complete/missing_amount(nur umrahmende Prosa, gegated aufhasPaymentInformation).
Neues Zahlungsmodul hinzufügen
- Klasse unter
Modules/anlegen,extends AbstractEventPaymentModule;slug(),defaultName(),getOptions()implementieren;defaultConfiguration()(z.B. Default-icon) sowieregistrationSummary()/doPayment()/createInvoice()überschreiben, wo nötig. - Zahlart-spezifische Extras als eigenes Fähigkeits-Interface (nicht ins Core-Interface).
- In
EventPaymentModuleRegistry::MODULESeintragen.PaymentMethod::create(['slug' => …])wird dann automatisch überProductionDataSeeder(iteriertEventPaymentModuleRegistry::slugs()) geseedet. - Slug-Konstante bei Bedarf auf
PaymentMethodergä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.