Configurations for payment modules

This commit is contained in:
2026-07-29 12:37:48 +02:00
parent ab3d526217
commit 29db141b84
19 changed files with 436 additions and 19 deletions
@@ -3,6 +3,7 @@
namespace App\Domains\Admin\Actions\UpdateAvailablePaymentMethod;
use App\Domains\Admin\Actions\RemovePaymentMethodUsage\RemovePaymentMethodUsageAction;
use App\Models\PaymentMethod;
class UpdateAvailablePaymentMethodAction
{
@@ -13,16 +14,29 @@ class UpdateAvailablePaymentMethodAction
public function execute(): UpdateAvailablePaymentMethodResponse
{
$response = new UpdateAvailablePaymentMethodResponse();
$wasActive = $this->request->availablePaymentMethod->active;
$paymentMethod = $this->request->availablePaymentMethod;
$wasActive = $paymentMethod->active;
$this->request->availablePaymentMethod->update([
// Nur bekannte Options-Keys übernehmen -- das Schema (PaymentMethod::OPTIONS) ist die Autorität.
$configuration = PaymentMethod::sanitizeConfiguration($paymentMethod->slug, $this->request->configuration);
// Guard: Aktivierung nur erlaubt, wenn alle Pflicht-Optionen befüllt sind.
if ($this->request->active && !PaymentMethod::isConfigurationComplete($paymentMethod->slug, $configuration)) {
$response->success = false;
$response->message = 'Bitte zuerst alle Pflichtfelder ausfüllen, bevor die Zahlungsmethode aktiviert wird.';
return $response;
}
$paymentMethod->update([
'name' => $this->request->name,
'description' => $this->request->description,
'active' => $this->request->active,
'configuration' => $configuration,
]);
if ($wasActive && !$this->request->active) {
(new RemovePaymentMethodUsageAction())->execute($this->request->availablePaymentMethod);
(new RemovePaymentMethodUsageAction())->execute($paymentMethod);
}
$response->success = true;
@@ -6,11 +6,15 @@ use App\Models\AvailablePaymentMethod;
class UpdateAvailablePaymentMethodRequest
{
/**
* @param array<string, mixed> $configuration
*/
public function __construct(
public AvailablePaymentMethod $availablePaymentMethod,
public string $name,
public ?string $description,
public bool $active,
public array $configuration = [],
) {
}
}
@@ -3,6 +3,7 @@
namespace App\Domains\Admin\Controllers;
use App\Models\AvailablePaymentMethod;
use App\Resources\AvailablePaymentMethodResource;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -12,7 +13,8 @@ class TenantPaymentMethodsGetController extends CommonController
public function __invoke(Request $request): JsonResponse
{
return response()->json([
'paymentMethods' => AvailablePaymentMethod::all(),
'paymentMethods' => AvailablePaymentMethod::all()
->map(fn (AvailablePaymentMethod $method) => new AvailablePaymentMethodResource($method)->toArray($request)),
'saveEndpoint' => '/api/v1/admin/tenant/payment-methods',
]);
}
@@ -20,6 +20,7 @@ class TenantPaymentMethodsUpdateController extends CommonController
name: $request->input('name'),
description: $request->input('description'),
active: (bool) $request->input('active'),
configuration: (array) $request->input('configuration', []),
));
$response = $action->execute();
@@ -1,5 +1,5 @@
<script setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue";
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
@@ -15,14 +15,31 @@ const { request } = useAjax()
const paymentMethods = ref(props.data.paymentMethods ?? [])
const editing = ref(null)
const form = ref({ name: '', description: '', active: true })
const form = ref({ name: '', description: '', active: true, configuration: {} })
// Sind alle Pflichtfelder des aktuell bearbeiteten Moduls befüllt? Der Server ist die
// eigentliche Autorität (Guard), das hier dient nur der Bedienführung.
const requiredComplete = computed(() => {
if (!editing.value) return true
return (editing.value.optionsSchema ?? [])
.filter(option => option.required)
.every(option => {
const value = form.value.configuration[option.name]
return value !== undefined && value !== null && value !== ''
})
})
function edit(paymentMethod) {
editing.value = paymentMethod
const configuration = {}
for (const option of paymentMethod.optionsSchema ?? []) {
configuration[option.name] = paymentMethod.configuration?.[option.name] ?? ''
}
form.value = {
name: paymentMethod.name,
description: paymentMethod.description ?? '',
active: paymentMethod.active,
configuration,
}
}
@@ -79,7 +96,17 @@ async function save() {
<tr><th>Name:</th><td><input type="text" v-model="form.name" class="form-input" /></td></tr>
<tr><th>Slug:</th><td>{{ editing.slug }}</td></tr>
<tr><th>Beschreibung:</th><td><textarea v-model="form.description" class="form-input"></textarea></td></tr>
<tr><th>Aktiv:</th><td><input type="checkbox" v-model="form.active" /></td></tr>
<tr v-for="option in editing.optionsSchema ?? []" :key="option.name">
<th>{{ option.label }}<span v-if="option.required" class="required-marker">*</span>:</th>
<td><input type="text" v-model="form.configuration[option.name]" class="form-input" /></td>
</tr>
<tr>
<th>Aktiv:</th>
<td>
<input type="checkbox" v-model="form.active" :disabled="!requiredComplete && !form.active" />
<span v-if="!requiredComplete" class="hint">Bitte zuerst alle Pflichtfelder (*) ausfüllen, um zu aktivieren.</span>
</td>
</tr>
</table>
<div class="btn-group">
<button class="btn-save" @click="save">Speichern</button>
@@ -177,6 +204,18 @@ async function save() {
box-sizing: border-box;
}
.required-marker {
color: #ef4444;
margin-left: 2px;
}
.hint {
display: block;
margin-top: 4px;
font-size: 0.8rem;
color: #b45309;
}
.btn-group {
display: flex;
gap: 10px;
@@ -64,6 +64,7 @@ class CreateEventCommand {
EventPaymentMethods::create([
'event_id' => $event->id,
'slug' => $availablePaymentMethod->slug,
'configuration' => $availablePaymentMethod->configuration ?? [],
]);
}
@@ -2,6 +2,10 @@
namespace App\Domains\Event\Actions\UpdateEvent;
use App\Models\AvailablePaymentMethod;
use App\Models\PaymentMethod;
use App\RelationModels\EventPaymentMethods;
class UpdateEventCommand {
public UpdateEventRequest $request;
public function __construct(UpdateEventRequest $request) {
@@ -32,7 +36,6 @@ class UpdateEventCommand {
$this->request->event->resetAllowedEatingHabits();
$this->request->event->resetContributingLocalGroups();
$this->request->event->resetPaymentMethods();
foreach($this->request->eatingHabits as $eatingHabit) {
$this->request->event->eatingHabits()->attach($eatingHabit);
@@ -42,13 +45,58 @@ class UpdateEventCommand {
$this->request->event->localGroups()->attach($contributingLocalGroup);
}
foreach($this->request->paymentMethods as $paymentMethod) {
$this->request->event->paymentMethods()->attach($paymentMethod);
}
$this->syncPaymentMethods();
$this->request->event->save();
$response->success = true;
return $response;
}
/**
* Differenzieller Sync der Zahlungsmethoden mit Snapshot-Copy-Semantik:
* - entfernte Methoden werden gelöscht,
* - neue Methoden erhalten einen Snapshot der Tenant-Config (oder eines expliziten Overrides),
* - bestehende Methoden behalten ihre pro-Event-Config, sofern kein Override übergeben wird.
*/
private function syncPaymentMethods(): void
{
$event = $this->request->event;
$desiredSlugs = $this->request->paymentMethods;
$overrides = $this->request->paymentMethodConfigurations;
$existing = EventPaymentMethods::where('event_id', $event->id)->get()->keyBy('slug');
// Nicht mehr gewünschte Methoden entfernen.
foreach ($existing as $slug => $row) {
if (!in_array($slug, $desiredSlugs, true)) {
$row->delete();
}
}
// Tenant-Instanzen (für Snapshot-Kopie neuer Methoden) laden.
$tenantMethods = AvailablePaymentMethod::whereIn('slug', $desiredSlugs)->get()->keyBy('slug');
foreach ($desiredSlugs as $slug) {
$override = $overrides[$slug] ?? null;
if (isset($existing[$slug])) {
// Bestehende Zuweisung: Config nur bei explizitem Override anpassen, sonst unverändert lassen.
if ($override !== null) {
$row = $existing[$slug];
$row->configuration = PaymentMethod::sanitizeConfiguration($slug, $override);
$row->save();
}
continue;
}
// Neue Zuweisung: expliziter Override oder Snapshot der Tenant-Config.
$snapshot = $override ?? ($tenantMethods[$slug]->configuration ?? []);
EventPaymentMethods::create([
'event_id' => $event->id,
'slug' => $slug,
'configuration' => PaymentMethod::sanitizeConfiguration($slug, $snapshot ?? []),
]);
}
}
}
@@ -23,7 +23,16 @@ class UpdateEventRequest {
public array $eatingHabits;
public array $paymentMethods;
public function __construct(Event $event, string $eventName, string $eventLocation, string $postalCode, string $email, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $alcoholicsAge, bool $sendWeeklyReports, bool $registrationAllowed, Amount $flatSupport, Amount $supportPerPerson, array $contributingLocalGroups, array $eatingHabits, array $paymentMethods) {
/**
* Optionale pro-Event-Config-Overrides je Zahlungsmethode: [slug => [option => value]].
* Fehlt ein Slug, bleibt die bestehende Config erhalten bzw. wird beim ersten Zuweisen
* aus der Tenant-Config als Snapshot kopiert.
*
* @var array<string, array<string, mixed>>
*/
public array $paymentMethodConfigurations;
public function __construct(Event $event, string $eventName, string $eventLocation, string $postalCode, string $email, DateTime $earlyBirdEnd, DateTime $registrationFinalEnd, int $alcoholicsAge, bool $sendWeeklyReports, bool $registrationAllowed, Amount $flatSupport, Amount $supportPerPerson, array $contributingLocalGroups, array $eatingHabits, array $paymentMethods, array $paymentMethodConfigurations = []) {
$this->event = $event;
$this->eventName = $eventName;
$this->eventLocation = $eventLocation;
@@ -39,5 +48,6 @@ class UpdateEventRequest {
$this->contributingLocalGroups = $contributingLocalGroups;
$this->eatingHabits = $eatingHabits;
$this->paymentMethods = $paymentMethods;
$this->paymentMethodConfigurations = $paymentMethodConfigurations;
}
}
@@ -43,6 +43,7 @@ class DetailsController extends CommonController {
$contributinLocalGroups = $request->input('contributingLocalGroups');
$eatingHabits = $request->input('eatingHabits');
$paymentMethods = $request->input('paymentMethods');
$paymentMethodConfigurations = (array) $request->input('paymentMethodConfigurations', []);
$eventUpdateRequest = new UpdateEventRequest(
$event,
@@ -59,7 +60,8 @@ class DetailsController extends CommonController {
$supportPerPerson,
$contributinLocalGroups,
$eatingHabits,
$paymentMethods
$paymentMethods,
$paymentMethodConfigurations
);
$eventUpdateCommand = new UpdateEventCommand($eventUpdateRequest);
@@ -22,6 +22,8 @@
const contributingLocalGroups = ref([])
const eatingHabits = ref([]);
const paymentMethods = ref([]);
// pro-Event-Config je Zahlungsmethode: { slug: { option: value } }
const paymentMethodConfigurations = reactive({});
const errors = reactive({})
@@ -49,6 +51,21 @@
contributingLocalGroups.value = props.event.contributingLocalGroups?.map(t => t.id) ?? []
eatingHabits.value = props.event.eatingHabits?.map(t => t.id) ?? []
paymentMethods.value = props.event.paymentMethods?.map(t => t.slug) ?? []
// Pro-Event-Config vorbelegen: bereits gespeicherte Pivot-Config des Events hat Vorrang,
// sonst der Tenant-Standard (Snapshot-Quelle für neu zugewiesene Methoden).
const eventConfigBySlug = {}
for (const pm of props.event.paymentMethods ?? []) {
eventConfigBySlug[pm.slug] = pm.configuration ?? {}
}
for (const method of dynmicProps.paymentMethods ?? []) {
const source = eventConfigBySlug[method.slug] ?? method.configuration ?? {}
const config = {}
for (const option of method.optionsSchema ?? []) {
config[option.name] = source[option.name] ?? ''
}
paymentMethodConfigurations[method.slug] = config
}
});
async function save() {
@@ -80,6 +97,7 @@
contributingLocalGroups: contributingLocalGroups.value,
eatingHabits: eatingHabits.value,
paymentMethods: paymentMethods.value,
paymentMethodConfigurations: paymentMethodConfigurations,
}
})
@@ -220,6 +238,16 @@
<td>
<input type="checkbox" :id="'paymentmethod' + paymentMethod.id" :value="paymentMethod.slug" v-model="paymentMethods" />
<label style="padding-left: 5px;" :for="'paymentmethod' + paymentMethod.id">{{paymentMethod.name}}</label>
<div
v-if="paymentMethods.includes(paymentMethod.slug) && (paymentMethod.optionsSchema?.length ?? 0) > 0"
class="payment-method-config"
>
<div v-for="option in paymentMethod.optionsSchema" :key="option.name" class="payment-method-config-row">
<label>{{ option.label }}<span v-if="option.required" class="required-marker">*</span></label>
<input type="text" v-model="paymentMethodConfigurations[paymentMethod.slug][option.name]" class="width-full" />
</div>
</div>
</td>
</tr>
</table>
@@ -278,4 +306,26 @@
width: 250px;
}
.payment-method-config {
margin: 6px 0 12px 24px;
padding-left: 10px;
border-left: 2px solid #d1d5db;
}
.payment-method-config-row {
margin-bottom: 6px;
}
.payment-method-config-row label {
display: block;
font-size: 0.85rem;
color: #374151;
margin-bottom: 2px;
}
.required-marker {
color: #ef4444;
margin-left: 2px;
}
</style>
+11
View File
@@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
* @property string $name
* @property ?string $description
* @property bool $active
* @property ?array $configuration
*/
class AvailablePaymentMethod extends InstancedModel
{
@@ -24,10 +25,12 @@ class AvailablePaymentMethod extends InstancedModel
'name',
'description',
'active',
'configuration',
];
protected $casts = [
'active' => 'boolean',
'configuration' => 'array',
];
public function paymentMethod(): BelongsTo
@@ -35,6 +38,14 @@ class AvailablePaymentMethod extends InstancedModel
return $this->belongsTo(PaymentMethod::class, 'slug', 'slug');
}
/**
* Sind alle Pflicht-Optionen dieses Moduls (siehe PaymentMethod::OPTIONS) befüllt?
*/
public function isComplete(): bool
{
return PaymentMethod::isConfigurationComplete($this->slug, $this->configuration ?? []);
}
public function events(): BelongsToMany
{
// Pivot verknüpft über den Slug -- Event::paymentMethods() greift daher automatisch nur
+5 -1
View File
@@ -7,6 +7,7 @@ use App\Casts\AmountCast;
use App\Enumerations\EatingHabit;
use App\Enumerations\ParticipationFeeType;
use App\RelationModels\EventParticipationFee;
use App\RelationModels\EventPaymentMethods;
use App\Resources\EventResource;
use App\Scopes\CommonModel;
use App\Scopes\InstancedModel;
@@ -157,7 +158,10 @@ class Event extends InstancedModel
// Pivot verknüpft über den Slug (payment_methods.slug), nicht über die numerische id
// von available_payment_methods -- die tenant-spezifische Instanz wird dadurch implizit
// über den globalen SiteScope von AvailablePaymentMethod (gefiltert auf app('tenant')) aufgelöst.
return $this->belongsToMany(AvailablePaymentMethod::class, 'event_payment_methods', 'event_id', 'slug', 'id', 'slug')->withTimestamps();
return $this->belongsToMany(AvailablePaymentMethod::class, 'event_payment_methods', 'event_id', 'slug', 'id', 'slug')
->using(EventPaymentMethods::class)
->withPivot('configuration')
->withTimestamps();
}
public function resetContributingLocalGroups() {
+77
View File
@@ -24,7 +24,84 @@ class PaymentMethod extends CommonModel
self::PAYMENT_NOT_DEFINED => ['name' => 'Sonstiges (Barzahlung, Zahlung vor Ort)', 'description' => null],
];
/**
* Options-Schema je Zahlungsmodul. Definiert im Code (nicht in der DB), damit es
* nicht mit dem Modul-Code auseinanderdriftet. In der DB stehen nur die Werte
* (configuration-JSON auf available_payment_methods bzw. event_payment_methods).
*
* Jede Option: ['name' => string, 'label' => string, 'type' => string, 'required' => bool]
*
* @var array<string, array<int, array{name: string, label: string, type: string, required: bool}>>
*/
public const array OPTIONS = [
self::PAYMENT_ACCOUNT_TRANSACTION => [
['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],
],
self::PAYMENT_NOT_DEFINED => [],
];
protected $fillable = [
'slug',
];
/**
* Options-Schema für einen Slug.
*
* @return array<int, array{name: string, label: string, type: string, required: bool}>
*/
public static function optionsFor(string $slug): array
{
return self::OPTIONS[$slug] ?? [];
}
/**
* Namen aller Pflicht-Optionen eines Slugs.
*
* @return array<int, string>
*/
public static function requiredOptionKeys(string $slug): array
{
return array_values(array_map(
static fn (array $option) => $option['name'],
array_filter(self::optionsFor($slug), static fn (array $option) => $option['required'] ?? false)
));
}
/**
* Reduziert eine Konfiguration auf die im Code definierten Options-Keys des Slugs.
* Unbekannte Keys werden verworfen -- das Schema (self::OPTIONS) ist die Autorität.
*
* @param array<string, mixed> $config
* @return array<string, mixed>
*/
public static function sanitizeConfiguration(string $slug, array $config): array
{
$result = [];
foreach (self::optionsFor($slug) as $option) {
if (array_key_exists($option['name'], $config)) {
$result[$option['name']] = $config[$option['name']];
}
}
return $result;
}
/**
* Prüft, ob alle Pflicht-Optionen eines Slugs in der Konfiguration befüllt (non-empty) sind.
*
* @param array<string, mixed> $config
*/
public static function isConfigurationComplete(string $slug, array $config): bool
{
foreach (self::requiredOptionKeys($slug) as $key) {
$value = $config[$key] ?? null;
if ($value === null || $value === '' || $value === []) {
return false;
}
}
return true;
}
}
+4 -2
View File
@@ -10,6 +10,7 @@ use App\Models\Tenant;
use App\Models\User;
use App\Repositories\EventRepository;
use App\Repositories\PageTextRepository;
use App\Resources\AvailablePaymentMethodResource;
use App\Resources\UserResource;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\JsonResponse;
@@ -183,12 +184,13 @@ class GlobalDataProvider {
return $activeUsers;
}
public function getEventSettingData() : JsonResponse {
public function getEventSettingData(Request $request) : JsonResponse {
return response()->json(
[
'localGroups' => Tenant::where(['is_active_local_group' => true])->get(),
'eatingHabits' => EatingHabit::all(),
'paymentMethods' => AvailablePaymentMethod::where('active', true)->get(),
'paymentMethods' => AvailablePaymentMethod::where('active', true)->get()
->map(fn (AvailablePaymentMethod $method) => new AvailablePaymentMethodResource($method)->toArray($request)),
]);
}
+15 -3
View File
@@ -2,10 +2,22 @@
namespace App\RelationModels;
use App\Scopes\CommonModel;
use Illuminate\Database\Eloquent\Relations\Pivot;
class EventPaymentMethods extends CommonModel
/**
* @property int $event_id
* @property string $slug
* @property ?array $configuration
*/
class EventPaymentMethods extends Pivot
{
protected $table = 'event_payment_methods';
protected $fillable = ['event_id', 'slug'];
public $incrementing = true;
protected $fillable = ['event_id', 'slug', 'configuration'];
protected $casts = [
'configuration' => 'array',
];
}
@@ -3,6 +3,7 @@
namespace App\Resources;
use App\Models\AvailablePaymentMethod;
use App\Models\PaymentMethod;
use Illuminate\Http\Resources\Json\JsonResource;
class AvailablePaymentMethodResource extends JsonResource {
@@ -13,12 +14,21 @@ class AvailablePaymentMethodResource extends JsonResource {
}
public function toArray($request) : array {
// Wird die Instanz über Event::paymentMethods() geladen, liegt die pro-Event-Konfiguration
// im Pivot; ansonsten (Tenant-Kontext) gilt die Konfiguration der Instanz selbst.
$pivot = $this->availablePaymentMethod->pivot ?? null;
$configuration = $pivot !== null
? ($pivot->configuration ?? [])
: ($this->availablePaymentMethod->configuration ?? []);
return [
'id' => $this->availablePaymentMethod->id,
'slug' => $this->availablePaymentMethod->slug,
'name' => $this->availablePaymentMethod->name,
'description' => $this->availablePaymentMethod->description,
'active' => $this->availablePaymentMethod->active,
'configuration' => (object) $configuration,
'optionsSchema' => PaymentMethod::optionsFor($this->availablePaymentMethod->slug),
];
}
}
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('available_payment_methods', function (Blueprint $table) {
$table->json('configuration')->nullable();
});
}
public function down(): void
{
Schema::table('available_payment_methods', function (Blueprint $table) {
$table->dropColumn('configuration');
});
}
};
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('event_payment_methods', function (Blueprint $table) {
$table->json('configuration')->nullable();
});
}
public function down(): void
{
Schema::table('event_payment_methods', function (Blueprint $table) {
$table->dropColumn('configuration');
});
}
};
@@ -0,0 +1,84 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Reparatur-Migration für eine Drift in event_payment_methods:
*
* Eine frühere Version von 2026_07_28_140020_create_event_payment_methods hatte die Verknüpfung
* über available_payment_method_id (FK auf available_payment_methods.id) modelliert. Die Migration
* wurde später in-place auf slug (FK auf payment_methods.slug) umgestellt -- Systeme, die noch die
* alte Version ausgeführt haben, tragen daher available_payment_method_id statt slug.
*
* Diese Migration konvertiert solche Systeme verlustfrei auf slug. Auf Systemen, die bereits die
* korrekte slug-Struktur haben (frische Installationen), ist sie ein No-Op.
*/
return new class extends Migration {
public function up(): void
{
// Frisch / bereits korrekt -> nichts zu tun.
if (!Schema::hasColumn('event_payment_methods', 'available_payment_method_id')
|| Schema::hasColumn('event_payment_methods', 'slug')) {
return;
}
// 1. slug zunächst nullable anlegen (für den Backfill).
Schema::table('event_payment_methods', function (Blueprint $table) {
$table->string('slug')->nullable()->after('event_id');
});
// 2. slug aus der verknüpften available_payment_methods-Zeile übernehmen.
DB::statement('
UPDATE event_payment_methods epm
JOIN available_payment_methods apm ON apm.id = epm.available_payment_method_id
SET epm.slug = apm.slug
');
// 3. Alte Spalte samt FK entfernen.
Schema::table('event_payment_methods', function (Blueprint $table) {
$table->dropForeign(['available_payment_method_id']);
$table->dropColumn('available_payment_method_id');
});
// 4. slug final absichern: NOT NULL, Unique (event_id, slug), FK auf payment_methods.slug.
Schema::table('event_payment_methods', function (Blueprint $table) {
$table->string('slug')->nullable(false)->change();
$table->unique(['event_id', 'slug']);
$table->foreign('slug')->references('slug')->on('payment_methods')->restrictOnDelete()->cascadeOnUpdate();
});
}
public function down(): void
{
// Nur umkehren, wenn tatsächlich die slug-Struktur vorliegt.
if (!Schema::hasColumn('event_payment_methods', 'slug')
|| Schema::hasColumn('event_payment_methods', 'available_payment_method_id')) {
return;
}
Schema::table('event_payment_methods', function (Blueprint $table) {
$table->unsignedBigInteger('available_payment_method_id')->nullable()->after('event_id');
});
// Backfill über den Tenant des Events, da available_payment_methods pro (tenant, slug) eindeutig ist.
DB::statement('
UPDATE event_payment_methods epm
JOIN events e ON e.id = epm.event_id
JOIN available_payment_methods apm ON apm.slug = epm.slug AND apm.tenant = e.tenant
SET epm.available_payment_method_id = apm.id
');
Schema::table('event_payment_methods', function (Blueprint $table) {
$table->dropForeign(['slug']);
$table->dropUnique(['event_id', 'slug']);
$table->dropColumn('slug');
});
Schema::table('event_payment_methods', function (Blueprint $table) {
$table->unsignedBigInteger('available_payment_method_id')->nullable(false)->change();
$table->foreign('available_payment_method_id')->references('id')->on('available_payment_methods')->cascadeOnUpdate();
});
}
};