Payment methode now selectable

This commit is contained in:
2026-08-05 16:53:35 +02:00
parent 7919d04dc3
commit daff70b4fe
22 changed files with 705 additions and 81 deletions
@@ -22,36 +22,83 @@ abstract class AbstractEventPaymentModule implements EventPaymentModule
return null;
}
/**
* 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}].
*
* @return array<int, array{name: string, label: string, type: string, required: bool}>
*/
public function getParticipantOptions(): array
{
return [];
}
// -----------------------------------------------------------------------------------------
// Aus getOptions() abgeleitete Helfer (zuvor statisch auf PaymentMethod).
// Aus dem jeweiligen Schema abgeleitete Helfer (Admin-Config = getOptions(),
// Teilnehmer-Eingaben = getParticipantOptions()).
// -----------------------------------------------------------------------------------------
/**
* Namen aller Pflicht-Optionen dieses Moduls.
*
* @return array<int, string>
*/
/** @return array<int, string> */
public function requiredOptionKeys(): array
{
return array_values(array_map(
static fn (array $option) => $option['name'],
array_filter($this->getOptions(), static fn (array $option) => $option['required'] ?? false)
));
return $this->requiredKeys($this->getOptions());
}
/**
* Reduziert eine Konfiguration auf die im Schema definierten Options-Keys.
* Unbekannte Keys werden verworfen -- getOptions() ist die Autorität.
*
* @param array<string, mixed> $config
* @return array<string, mixed>
*/
public function sanitizeConfiguration(array $config): array
{
return $this->sanitizeAgainst($this->getOptions(), $config);
}
/** @param array<string, mixed> $config */
public function isConfigurationComplete(array $config): bool
{
return $this->allRequiredFilled($this->getOptions(), $config);
}
/**
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
public function sanitizeParticipantOptions(array $input): array
{
return $this->sanitizeAgainst($this->getParticipantOptions(), $input);
}
/** @param array<string, mixed> $input */
public function participantOptionsComplete(array $input): bool
{
return $this->allRequiredFilled($this->getParticipantOptions(), $input);
}
/**
* @param array<int, array{name: string, required?: bool}> $schema
* @return array<int, string>
*/
private function requiredKeys(array $schema): array
{
return array_values(array_map(
static fn (array $option) => $option['name'],
array_filter($schema, static fn(array $option) => $option['required'] ?? false)
));
}
/**
* Reduziert Eingaben auf die im Schema definierten Keys (unbekannte werden verworfen).
*
* @param array<int, array{name: string}> $schema
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
private function sanitizeAgainst(array $schema, array $input): array
{
$result = [];
foreach ($this->getOptions() as $option) {
if (array_key_exists($option['name'], $config)) {
$result[$option['name']] = $config[$option['name']];
foreach ($schema as $option) {
if (array_key_exists($option['name'], $input)) {
$result[$option['name']] = $input[$option['name']];
}
}
@@ -59,14 +106,15 @@ abstract class AbstractEventPaymentModule implements EventPaymentModule
}
/**
* Prüft, ob alle Pflicht-Optionen befüllt (non-empty) sind.
* Sind alle Pflichtfelder des Schemas befüllt (non-empty)?
*
* @param array<string, mixed> $config
* @param array<int, array{name: string, required?: bool}> $schema
* @param array<string, mixed> $input
*/
public function isConfigurationComplete(array $config): bool
private function allRequiredFilled(array $schema, array $input): bool
{
foreach ($this->requiredOptionKeys() as $key) {
$value = $config[$key] ?? null;
foreach ($this->requiredKeys($schema) as $key) {
$value = $input[$key] ?? null;
if ($value === null || $value === '' || $value === []) {
return false;
}
+19 -5
View File
@@ -19,16 +19,30 @@ Rechnung) liegt gekapselt in einem Modul pro Zahlungsart. Aufgelöst wird über
## 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']`. `type` ist i.d.R. `'string'`; `'richtext'` wird im Frontend über
`Views/Components/TextEditor.vue` (TinyMCE, HTML) gerendert und via `v-html`/`{!! !!}` ausgegeben.
- **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. Teilnehmer-Eingaben rendert die generische
`SignUpForm/components/PaymentMethodInputs.vue` schema-getrieben.
- **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`.
- **`PaymentMethod` ist nur eine Fassade:** `PaymentMethod::optionsFor/requiredOptionKeys/sanitizeConfiguration/`
`isConfigurationComplete/defaults()` delegieren an die Registry. Bestehende Aufrufstellen bleiben so stabil.
- **`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.
@@ -30,12 +30,19 @@ interface EventPaymentModule
public function defaultDescription(): ?string;
/**
* Options-Schema dieses Moduls (welche Konfigurationsfelder es benötigt).
* Admin-Options-Schema dieses Moduls (welche Konfigurationsfelder der/die Veranstalter\*in pflegt).
*
* @return array<int, array{name: string, label: string, type: string, required: bool}>
*/
public function getOptions(): array;
/**
* Teilnehmer-Eingaben, die diese Zahlungsart beim Anmelden abfragt (payer-seitig; Standard: keine).
*
* @return array<int, array{name: string, label: string, type: string, required: bool}>
*/
public function getParticipantOptions(): array;
/**
* Liefert den zahlungsspezifischen Teil der Anmelde-Zusammenfassung.
* (In dieser Iteration nur als Stub vorhanden.)
@@ -33,6 +33,7 @@ class AccountTransferPaymentModule extends AbstractEventPaymentModule implements
['name' => 'account_owner', 'label' => 'Kontoinhaber', 'type' => 'string', 'required' => true],
['name' => 'iban', 'label' => 'IBAN', 'type' => 'string', 'required' => true],
['name' => 'bic', 'label' => 'BIC', 'type' => 'string', 'required' => false],
['name' => 'summary_confirmation_text', 'label' => 'Bestätigungstext (Zusammenfassung, {amount} als Platzhalter)', 'type' => 'string', 'required' => false],
];
}