Configurations for payment modules
This commit is contained in:
+17
-3
@@ -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;
|
||||
|
||||
+4
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user