5.8 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
- Options-Schema lebt im Code, nicht in der DB (
getOptions()je Modul). In der DB stehen nur die Werte. Optionsform:['name', 'label', 'type', 'required'].typeist i.d.R.'string';'richtext'wird im Frontend überViews/Components/TextEditor.vue(TinyMCE, HTML) gerendert und viav-html/{!! !!}ausgegeben. - 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 inUpdateEventCommand. PaymentMethodist nur eine Fassade:PaymentMethod::optionsFor/requiredOptionKeys/sanitizeConfiguration/isConfigurationComplete/defaults()delegieren an die Registry. Bestehende Aufrufstellen bleiben so stabil.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 render-agnostisch: entwederlines(label/value, z.B. Überweisung) oderhtml(Freitext, z.B. Barzahlung), plushasPaymentInformationundshowGiroCode.- Zugang über das Teilnehmer-Model:
EventParticipant::paymentModule(),paymentConfiguration()(eager-load-fähig über->with('event.paymentMethods'), kein statischer Cache),paymentSummary(). - Konsumiert von
EventParticipantResource(paymentSummary),SubmitSuccess.vue,emails/subparts/payment.blade.php(+signup_complete,missing_amount). GiroCode in Mails: inline per CID ($message->embedData(...)), im Web über/print-girocode/{token}(sessionlos, lazy).
Neues Zahlungsmodul hinzufügen
- Klasse unter
Modules/anlegen,extends AbstractEventPaymentModule;slug(),defaultName(),getOptions()implementieren;registrationSummary()/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/sync_payment_bank_data.php überführt einmalig Bestands-Bankdaten (Tenant/Event) in die Modul-Config
(idempotent). Bei Live-Inbetriebnahme einmal ausführen.
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.