Dev 4.6.1 #13
@@ -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'] ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ async function save() {
|
||||
|
||||
<FullScreenModal :show="editing !== null" @close="close">
|
||||
<template v-if="editing">
|
||||
<h2>Zahlungsweg bearbeiten</h2>
|
||||
<h2>Zahlungsmodul bearbeiten</h2>
|
||||
<table class="data-table">
|
||||
<tr><th>Name</th><td><input type="text" v-model="form.name" class="form-input" /></td></tr>
|
||||
<tr><th>Beschreibung</th><td><textarea v-model="form.description" class="form-input"></textarea></td></tr>
|
||||
@@ -103,6 +103,7 @@ async function save() {
|
||||
:options="PAYMENT_METHOD_ICONS" placeholder="Symbol wählen…"/>
|
||||
<TextEditor v-else-if="option.type === 'richtext'" v-model="form.configuration[option.name]"/>
|
||||
<input v-else type="text" v-model="form.configuration[option.name]" class="form-input"/>
|
||||
<span v-if="option.hint" class="option-hint">{{ option.hint }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -221,6 +222,13 @@ async function save() {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.option-hint {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
@@ -19,12 +19,12 @@ const tabs = [
|
||||
endpoint: '/api/v1/admin/tenant/contact',
|
||||
},
|
||||
{
|
||||
title: 'Bezahldaten',
|
||||
title: 'IBAN-Informationen',
|
||||
component: TenantPayment,
|
||||
endpoint: '/api/v1/admin/tenant/payment',
|
||||
},
|
||||
{
|
||||
title: 'Zahlungswege',
|
||||
title: 'Zahlungsmodule',
|
||||
component: TenantPaymentMethods,
|
||||
endpoint: '/api/v1/admin/tenant/payment-methods',
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ const tabs = [
|
||||
endpoint: '/api/v1/admin/tenants/' + slug + '/contact',
|
||||
},
|
||||
{
|
||||
title: 'Bezahldaten',
|
||||
title: 'IBAN-Informationen',
|
||||
component: TenantPayment,
|
||||
endpoint: '/api/v1/admin/tenants/' + slug + '/payment',
|
||||
},
|
||||
|
||||
@@ -392,6 +392,7 @@ onMounted(async () => {
|
||||
<input v-else type="text"
|
||||
v-model="paymentMethodConfigurations[optionsMethod.slug][option.name]"
|
||||
class="width-full"/>
|
||||
<span v-if="option.hint" class="option-hint">{{ option.hint }}</span>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -416,4 +417,11 @@ onMounted(async () => {
|
||||
color: #ef4444;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.option-hint {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,6 +22,16 @@ abstract class AbstractEventPaymentModule implements EventPaymentModule
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard-Konfiguration beim Anlegen der Tenant-Instanz. Standard: leer.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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}].
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<string, mixed>
|
||||
*/
|
||||
public function defaultConfiguration(): array;
|
||||
|
||||
/**
|
||||
* Admin-Options-Schema dieses Moduls (welche Konfigurationsfelder der/die Veranstalter\*in pflegt).
|
||||
*
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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<string, array{name: string, description: ?string}>
|
||||
* @return array<string, array{name: string, description: ?string, configuration: array<string, mixed>}>
|
||||
*/
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user