Development 4.6.0 #12

Merged
th.guenther merged 8 commits from development-4.6.0 into main 2026-08-05 11:07:58 +02:00
19 changed files with 584 additions and 51 deletions
Showing only changes of commit 78f4d813f9 - Show all commits
@@ -31,8 +31,9 @@ class CreateTenantAction
'has_active_instance' => true, 'has_active_instance' => true,
]); ]);
$paymentMethodDefaults = PaymentMethod::defaults();
foreach (PaymentMethod::all() as $paymentMethod) { foreach (PaymentMethod::all() as $paymentMethod) {
$defaults = PaymentMethod::DEFAULTS[$paymentMethod->slug] ?? ['name' => $paymentMethod->slug, 'description' => null]; $defaults = $paymentMethodDefaults[$paymentMethod->slug] ?? ['name' => $paymentMethod->slug, 'description' => null];
AvailablePaymentMethod::create([ AvailablePaymentMethod::create([
'tenant' => $tenant->slug, 'tenant' => $tenant->slug,
@@ -17,7 +17,7 @@ class UpdateAvailablePaymentMethodAction
$paymentMethod = $this->request->availablePaymentMethod; $paymentMethod = $this->request->availablePaymentMethod;
$wasActive = $paymentMethod->active; $wasActive = $paymentMethod->active;
// Nur bekannte Options-Keys übernehmen -- das Schema (PaymentMethod::OPTIONS) ist die Autorität. // Nur bekannte Options-Keys übernehmen -- das Options-Schema des Zahlungsmoduls ist die Autorität.
$configuration = PaymentMethod::sanitizeConfiguration($paymentMethod->slug, $this->request->configuration); $configuration = PaymentMethod::sanitizeConfiguration($paymentMethod->slug, $this->request->configuration);
// Guard: Aktivierung nur erlaubt, wenn alle Pflicht-Optionen befüllt sind. // Guard: Aktivierung nur erlaubt, wenn alle Pflicht-Optionen befüllt sind.
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Enumerations;
use App\Scopes\CommonModel;
/**
* Lebenszyklus-Status einer Zahlung -- DB-gestützt, analog zu {@see InvoiceStatus}.
* Reine Steuersignale (z.B. Redirect) gehören NICHT hierher, sondern bleiben transiente
* Felder auf {@see \App\EventPaymentModules\DTO\DoPaymentResponse}.
*/
class PaymentStatus extends CommonModel
{
public const PAYMENT_STATUS_PENDING = 'pending';
public const PAYMENT_STATUS_SCHEDULED = 'scheduled';
public const PAYMENT_STATUS_PAID = 'paid';
public const PAYMENT_STATUS_FAILED = 'failed';
protected $table = 'payment_status';
protected $fillable = [
'slug',
];
}
@@ -0,0 +1,118 @@
<?php
namespace App\EventPaymentModules;
use App\Enumerations\PaymentStatus;
use App\EventPaymentModules\DTO\CreateInvoiceRequest;
use App\EventPaymentModules\DTO\CreateInvoiceResponse;
use App\EventPaymentModules\DTO\DoPaymentRequest;
use App\EventPaymentModules\DTO\DoPaymentResponse;
use App\EventPaymentModules\DTO\RegistrationSummaryRequest;
use App\EventPaymentModules\DTO\RegistrationSummaryResponse;
/**
* Gemeinsame Basis aller Zahlungsmodule. Hält den geteilten Code und die aus {@see getOptions()}
* abgeleiteten Options-Helfer; konkrete Module definieren nur ihre Identität, ihr Options-Schema
* und -- später -- die je Zahlungsart variierenden Teile (z.B. den Rechnungs-Schlusssatz).
*/
abstract class AbstractEventPaymentModule implements EventPaymentModule
{
public function defaultDescription(): ?string
{
return null;
}
// -----------------------------------------------------------------------------------------
// Aus getOptions() abgeleitete Helfer (zuvor statisch auf PaymentMethod).
// -----------------------------------------------------------------------------------------
/**
* Namen aller Pflicht-Optionen dieses Moduls.
*
* @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)
));
}
/**
* 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
{
$result = [];
foreach ($this->getOptions() as $option) {
if (array_key_exists($option['name'], $config)) {
$result[$option['name']] = $config[$option['name']];
}
}
return $result;
}
/**
* Prüft, ob alle Pflicht-Optionen befüllt (non-empty) sind.
*
* @param array<string, mixed> $config
*/
public function isConfigurationComplete(array $config): bool
{
foreach ($this->requiredOptionKeys() as $key) {
$value = $config[$key] ?? null;
if ($value === null || $value === '' || $value === []) {
return false;
}
}
return true;
}
// -----------------------------------------------------------------------------------------
// Operative Methoden -- in dieser Iteration bewusst als Stubs.
// -----------------------------------------------------------------------------------------
public function registrationSummary(RegistrationSummaryRequest $request): RegistrationSummaryResponse
{
// TODO: In einer Folge-Iteration ausformulieren (zahlungsspezifischer Teil der Zusammenfassung).
return new RegistrationSummaryResponse();
}
public function doPayment(DoPaymentRequest $request): DoPaymentResponse
{
// TODO: In einer Folge-Iteration ausformulieren. Default: nichts aktiv anzustoßen (Status offen).
$response = new DoPaymentResponse();
$response->success = true;
$response->status = PaymentStatus::PAYMENT_STATUS_PENDING;
return $response;
}
/**
* Template-Method: Der gemeinsame Rechnungs-Rumpf wird hier gebaut, der variierende Schlusssatz
* kommt aus {@see invoiceClosingStatement()} des jeweiligen Moduls.
*/
public function createInvoice(CreateInvoiceRequest $request): CreateInvoiceResponse
{
$response = new CreateInvoiceResponse();
// TODO: In einer Folge-Iteration den gemeinsamen Rumpf (Betrag, Leistung, Teilnehmerdaten) befüllen
// und an die Invoice-Domain anbinden.
$response->success = true;
$response->closingStatement = $this->invoiceClosingStatement($request);
return $response;
}
/**
* Der je Zahlungsart variierende Schlusssatz der Rechnung
* ("Bitte überweise bis ..." / "wird abgebucht ..." / "wurde per PayPal beglichen").
*/
abstract protected function invoiceClosingStatement(CreateInvoiceRequest $request): string;
}
@@ -0,0 +1,25 @@
<?php
namespace App\EventPaymentModules\DTO;
use App\Models\Event;
use App\Models\EventParticipant;
use App\ValueObjects\Amount;
/**
* Eingabe für {@see \App\EventPaymentModules\EventPaymentModule::createInvoice()}.
* Von allen Modulen geteilt.
*/
final class CreateInvoiceRequest
{
/**
* @param array<string, mixed> $configuration Aufgelöste (pro-Event-)Konfiguration des Moduls.
*/
public function __construct(
public readonly Event $event,
public readonly EventParticipant $participant,
public readonly Amount $amount,
public readonly array $configuration = [],
) {
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\EventPaymentModules\DTO;
/**
* Ausgabe von {@see \App\EventPaymentModules\EventPaymentModule::createInvoice()}.
*
* Der gemeinsame Rechnungs-Rumpf (Betrag, Leistung, ...) wird von der Basisklasse befüllt,
* $closingStatement ist der je Modul variierende Schlusssatz
* ("Bitte überweise bis ..." / "wird abgebucht ..." / "wurde per PayPal beglichen").
*/
final class CreateInvoiceResponse
{
public bool $success = false;
/** @var array<int, array<string, mixed>> Gemeinsame Rechnungspositionen (Rumpf). */
public array $lines = [];
public ?string $closingStatement = null;
public ?string $message = null;
}
@@ -0,0 +1,25 @@
<?php
namespace App\EventPaymentModules\DTO;
use App\Models\Event;
use App\Models\EventParticipant;
use App\ValueObjects\Amount;
/**
* Eingabe für {@see \App\EventPaymentModules\EventPaymentModule::doPayment()}.
* Von allen Modulen geteilt.
*/
final class DoPaymentRequest
{
/**
* @param array<string, mixed> $configuration Aufgelöste (pro-Event-)Konfiguration des Moduls.
*/
public function __construct(
public readonly Event $event,
public readonly EventParticipant $participant,
public readonly Amount $amount,
public readonly array $configuration = [],
) {
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\EventPaymentModules\DTO;
use App\Enumerations\PaymentStatus;
/**
* Ausgabe von {@see \App\EventPaymentModules\EventPaymentModule::doPayment()}.
* Von allen Modulen geteilt -- jedes Modul befüllt nur die für es relevanten Felder.
*
* $status ist ein Lebenszyklus-Status (PaymentStatus-Slug, DB-gestützt). Reine Steuersignale
* wie ein Redirect bleiben transiente Felder (redirectUrl), nicht Teil des Status-Vokabulars.
*/
final class DoPaymentResponse
{
public bool $success = false;
public string $status = PaymentStatus::PAYMENT_STATUS_PENDING;
public ?string $redirectUrl = null;
public ?\DateTimeImmutable $scheduledFor = null;
public ?string $reference = null;
public ?string $message = null;
}
@@ -0,0 +1,25 @@
<?php
namespace App\EventPaymentModules\DTO;
use App\Models\Event;
use App\ValueObjects\Amount;
/**
* Eingabe für {@see \App\EventPaymentModules\EventPaymentModule::registrationSummary()}.
*
* Bewusst ohne persistierten EventParticipant: Die Zusammenfassung wird im Anmeldefluss VOR dem
* Absenden erzeugt, es existiert also noch kein Teilnehmer-Datensatz.
*/
final class RegistrationSummaryRequest
{
/**
* @param array<string, mixed> $configuration Aufgelöste (pro-Event-)Konfiguration des Moduls.
*/
public function __construct(
public readonly Event $event,
public readonly Amount $amount,
public readonly array $configuration = [],
) {
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\EventPaymentModules\DTO;
/**
* Ausgabe von {@see \App\EventPaymentModules\EventPaymentModule::registrationSummary()}.
*
* Strukturierte Daten (kein reines Vue) -- speisen sowohl die Anmelde-Zusammenfassung im Frontend
* als auch spätere API-Actions. Der obere Teil der Zusammenfassung ist für alle gleich; hier kommt
* ausschließlich der zahlungsspezifische Abschnitt.
*/
final class RegistrationSummaryResponse
{
public string $title = '';
/** @var array<int, array{label: string, value: string}> Zeilen des Zahlungs-Abschnitts. */
public array $lines = [];
/** Bestätigungstext für die Checkbox, z.B. "Ich bestätige, den Betrag von X zu überweisen.". */
public ?string $confirmationLabel = null;
}
@@ -0,0 +1,57 @@
<?php
namespace App\EventPaymentModules;
use App\EventPaymentModules\DTO\CreateInvoiceRequest;
use App\EventPaymentModules\DTO\CreateInvoiceResponse;
use App\EventPaymentModules\DTO\DoPaymentRequest;
use App\EventPaymentModules\DTO\DoPaymentResponse;
use App\EventPaymentModules\DTO\RegistrationSummaryRequest;
use App\EventPaymentModules\DTO\RegistrationSummaryResponse;
/**
* Vertrag eines Zahlungsmoduls. Jede Zahlungsart (Überweisung, künftig SEPA-Lastschrift, PayPal, ...)
* kapselt ihr Verhalten in einer eigenen Implementierung. Aufgelöst wird ein Modul über seinen Slug
* via {@see EventPaymentModuleRegistry}.
*
* Die operativen Methoden (doPayment/createInvoice/registrationSummary) arbeiten mit geteilten
* Request/Response-DTOs -- das Modul stellt die polymorphe "Command"-Logik; schwere Arbeit
* (PayPal-API, SEPA-XML) wird später an reguläre Domain-Actions delegiert.
*/
interface EventPaymentModule
{
/** Eindeutiger Slug der Zahlungsart (entspricht payment_methods.slug). */
public static function slug(): string;
/** Standard-Anzeigename beim Anlegen der Tenant-Instanz. */
public function defaultName(): string;
/** Standard-Beschreibung beim Anlegen der Tenant-Instanz. */
public function defaultDescription(): ?string;
/**
* Options-Schema dieses Moduls (welche Konfigurationsfelder es benötigt).
*
* @return array<int, array{name: string, label: string, type: string, required: bool}>
*/
public function getOptions(): array;
/**
* Liefert den zahlungsspezifischen Teil der Anmelde-Zusammenfassung.
* (In dieser Iteration nur als Stub vorhanden.)
*/
public function registrationSummary(RegistrationSummaryRequest $request): RegistrationSummaryResponse;
/**
* Stößt die Zahlung an (bzw. beschreibt, was zu tun ist -- z.B. Redirect, Lastschrift, manuelle Überweisung).
* (In dieser Iteration nur als Stub vorhanden.)
*/
public function doPayment(DoPaymentRequest $request): DoPaymentResponse;
/**
* Erzeugt die Rechnung/Zahlungsaufforderung. Der gemeinsame Rumpf kommt aus der Basisklasse,
* nur der Schlusssatz variiert je Modul.
* (In dieser Iteration nur als Stub vorhanden.)
*/
public function createInvoice(CreateInvoiceRequest $request): CreateInvoiceResponse;
}
@@ -0,0 +1,54 @@
<?php
namespace App\EventPaymentModules;
use App\EventPaymentModules\Modules\AccountTransferPaymentModule;
use App\EventPaymentModules\Modules\UndefinedPaymentModule;
/**
* Zentrale Auflösung Slug -> Zahlungsmodul. Neue Zahlungsarten werden hier eingetragen.
* Bewusst statisch + manuelle Instanziierung (kein Container-Binding), passend zum Projektstil.
*/
class EventPaymentModuleRegistry
{
/**
* Alle registrierten Modul-Klassen. Reihenfolge bestimmt u.a. die Seed-Reihenfolge.
*
* @var array<int, class-string<EventPaymentModule>>
*/
private const MODULES = [
AccountTransferPaymentModule::class,
UndefinedPaymentModule::class,
];
/** @var array<string, EventPaymentModule>|null Lazy gecachte Instanzen (slug => Modul). */
private static ?array $instances = null;
/**
* @return array<string, EventPaymentModule>
*/
public static function all(): array
{
if (self::$instances === null) {
self::$instances = [];
foreach (self::MODULES as $moduleClass) {
self::$instances[$moduleClass::slug()] = new $moduleClass();
}
}
return self::$instances;
}
public static function forSlug(string $slug): ?EventPaymentModule
{
return self::all()[$slug] ?? null;
}
/**
* @return array<int, string>
*/
public static function slugs(): array
{
return array_keys(self::all());
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\EventPaymentModules\Modules;
use App\EventPaymentModules\AbstractEventPaymentModule;
use App\EventPaymentModules\DTO\CreateInvoiceRequest;
use App\Models\PaymentMethod;
/**
* Überweisung auf das Veranstaltungskonto -- die Zahlung erfolgt manuell durch die teilnehmende Person.
*/
class AccountTransferPaymentModule extends AbstractEventPaymentModule
{
public static function slug(): string
{
return PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION;
}
public function defaultName(): string
{
return 'Überweisung auf Veranstaltungskonto';
}
public function getOptions(): array
{
return [
['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],
];
}
protected function invoiceClosingStatement(CreateInvoiceRequest $request): string
{
// TODO: In einer Folge-Iteration ausformulieren (z.B. "Bitte überweise bis zum ... auf IBAN ...").
return '';
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\EventPaymentModules\Modules;
use App\EventPaymentModules\AbstractEventPaymentModule;
use App\EventPaymentModules\DTO\CreateInvoiceRequest;
use App\Models\PaymentMethod;
/**
* Sonstiges (Barzahlung, Zahlung vor Ort) -- keine konfigurierbaren Optionen, keine aktive Zahlung.
*/
class UndefinedPaymentModule extends AbstractEventPaymentModule
{
public static function slug(): string
{
return PaymentMethod::PAYMENT_NOT_DEFINED;
}
public function defaultName(): string
{
return 'Sonstiges (Barzahlung, Zahlung vor Ort)';
}
public function getOptions(): array
{
return [];
}
protected function invoiceClosingStatement(CreateInvoiceRequest $request): string
{
return '';
}
}
+13 -2
View File
@@ -11,8 +11,10 @@ use App\Enumerations\InvoiceStatus;
use App\Enumerations\InvoiceType; use App\Enumerations\InvoiceType;
use App\Enumerations\ParticipationFeeType; use App\Enumerations\ParticipationFeeType;
use App\Enumerations\ParticipationType; use App\Enumerations\ParticipationType;
use App\Enumerations\PaymentStatus;
use App\Enumerations\SwimmingPermission; use App\Enumerations\SwimmingPermission;
use App\Enumerations\UserRole; use App\Enumerations\UserRole;
use App\EventPaymentModules\EventPaymentModuleRegistry;
use App\Models\CronTask; use App\Models\CronTask;
use App\Models\PaymentMethod; use App\Models\PaymentMethod;
use App\Models\Tenant; use App\Models\Tenant;
@@ -30,11 +32,20 @@ class ProductionDataSeeder {
$this->installParticipationTypes(); $this->installParticipationTypes();
$this->installEfzStatus(); $this->installEfzStatus();
$this->installPaymentMethods(); $this->installPaymentMethods();
$this->installPaymentStatus();
}
private function installPaymentStatus() {
PaymentStatus::create(['slug' => PaymentStatus::PAYMENT_STATUS_PENDING]);
PaymentStatus::create(['slug' => PaymentStatus::PAYMENT_STATUS_SCHEDULED]);
PaymentStatus::create(['slug' => PaymentStatus::PAYMENT_STATUS_PAID]);
PaymentStatus::create(['slug' => PaymentStatus::PAYMENT_STATUS_FAILED]);
} }
private function installPaymentMethods() { private function installPaymentMethods() {
PaymentMethod::create(['slug' => PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION]); foreach (EventPaymentModuleRegistry::slugs() as $slug) {
PaymentMethod::create(['slug' => PaymentMethod::PAYMENT_NOT_DEFINED]); PaymentMethod::create(['slug' => $slug]);
}
} }
private function installEfzStatus() { private function installEfzStatus() {
+1 -1
View File
@@ -39,7 +39,7 @@ class AvailablePaymentMethod extends InstancedModel
} }
/** /**
* Sind alle Pflicht-Optionen dieses Moduls (siehe PaymentMethod::OPTIONS) befüllt? * Sind alle Pflicht-Optionen dieses Moduls (siehe Options-Schema des Zahlungsmoduls) befüllt?
*/ */
public function isComplete(): bool public function isComplete(): bool
{ {
+26 -46
View File
@@ -2,12 +2,17 @@
namespace App\Models; namespace App\Models;
use App\EventPaymentModules\EventPaymentModuleRegistry;
use App\Scopes\CommonModel; use App\Scopes\CommonModel;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
/** /**
* @property string $id * @property string $id
* @property string $slug * @property string $slug
*
* Hinweis: Das Verhalten je Zahlungsart (Options-Schema, Defaults, Zahlung, Rechnung) liegt in den
* Zahlungsmodulen unter {@see \App\EventPaymentModules}. Dieses Model bleibt eine dünne Fassade, damit
* bestehende Aufrufstellen (PaymentMethod::optionsFor/... ) unverändert weiterlaufen.
*/ */
class PaymentMethod extends CommonModel class PaymentMethod extends CommonModel
{ {
@@ -19,33 +24,25 @@ class PaymentMethod extends CommonModel
public const string PAYMENT_ACCOUNT_TRANSACTION = 'PAYMENT_ACCOUNT_TRANSACTION'; public const string PAYMENT_ACCOUNT_TRANSACTION = 'PAYMENT_ACCOUNT_TRANSACTION';
public const string PAYMENT_NOT_DEFINED = 'PAYMENT_NOT_DEFINED'; public const string PAYMENT_NOT_DEFINED = 'PAYMENT_NOT_DEFINED';
public const array DEFAULTS = [
self::PAYMENT_ACCOUNT_TRANSACTION => ['name' => 'Überweisung auf Veranstaltungskonto', 'description' => null],
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 = [ protected $fillable = [
'slug', 'slug',
]; ];
/**
* Standard-Werte (name/description) je Slug -- aus den Zahlungsmodulen aufgebaut.
*
* @return array<string, array{name: string, description: ?string}>
*/
public static function defaults(): array
{
$defaults = [];
foreach (EventPaymentModuleRegistry::all() as $slug => $module) {
$defaults[$slug] = ['name' => $module->defaultName(), 'description' => $module->defaultDescription()];
}
return $defaults;
}
/** /**
* Options-Schema für einen Slug. * Options-Schema für einen Slug.
* *
@@ -53,7 +50,7 @@ class PaymentMethod extends CommonModel
*/ */
public static function optionsFor(string $slug): array public static function optionsFor(string $slug): array
{ {
return self::OPTIONS[$slug] ?? []; return EventPaymentModuleRegistry::forSlug($slug)?->getOptions() ?? [];
} }
/** /**
@@ -63,45 +60,28 @@ class PaymentMethod extends CommonModel
*/ */
public static function requiredOptionKeys(string $slug): array public static function requiredOptionKeys(string $slug): array
{ {
return array_values(array_map( return EventPaymentModuleRegistry::forSlug($slug)?->requiredOptionKeys() ?? [];
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. * Reduziert eine Konfiguration auf die im Schema definierten Options-Keys des Slugs.
* Unbekannte Keys werden verworfen -- das Schema (self::OPTIONS) ist die Autorität.
* *
* @param array<string, mixed> $config * @param array<string, mixed> $config
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public static function sanitizeConfiguration(string $slug, array $config): array public static function sanitizeConfiguration(string $slug, array $config): array
{ {
$result = []; return EventPaymentModuleRegistry::forSlug($slug)?->sanitizeConfiguration($config) ?? [];
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. * Prüft, ob alle Pflicht-Optionen eines Slugs in der Konfiguration befüllt (non-empty) sind.
* Unbekannte Slugs (kein Modul) gelten als vollständig -- kein Modul, keine Pflichtfelder.
* *
* @param array<string, mixed> $config * @param array<string, mixed> $config
*/ */
public static function isConfigurationComplete(string $slug, array $config): bool public static function isConfigurationComplete(string $slug, array $config): bool
{ {
foreach (self::requiredOptionKeys($slug) as $key) { return EventPaymentModuleRegistry::forSlug($slug)?->isConfigurationComplete($config) ?? true;
$value = $config[$key] ?? null;
if ($value === null || $value === '' || $value === []) {
return false;
}
}
return true;
} }
} }
@@ -0,0 +1,19 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('payment_status', function (Blueprint $table) {
$table->string('slug')->primary();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('payment_status');
}
};
@@ -0,0 +1,59 @@
<?php
namespace Tests\Unit;
use App\EventPaymentModules\EventPaymentModuleRegistry;
use App\EventPaymentModules\Modules\AccountTransferPaymentModule;
use App\EventPaymentModules\Modules\UndefinedPaymentModule;
use App\Models\PaymentMethod;
use PHPUnit\Framework\TestCase;
class EventPaymentModuleRegistryTest extends TestCase
{
public function test_for_slug_resolves_known_modules(): void
{
$this->assertInstanceOf(
AccountTransferPaymentModule::class,
EventPaymentModuleRegistry::forSlug(PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION)
);
$this->assertInstanceOf(
UndefinedPaymentModule::class,
EventPaymentModuleRegistry::forSlug(PaymentMethod::PAYMENT_NOT_DEFINED)
);
}
public function test_for_slug_returns_null_for_unknown(): void
{
$this->assertNull(EventPaymentModuleRegistry::forSlug('DOES_NOT_EXIST'));
}
public function test_slugs_lists_all_registered_modules(): void
{
$slugs = EventPaymentModuleRegistry::slugs();
$this->assertContains(PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION, $slugs);
$this->assertContains(PaymentMethod::PAYMENT_NOT_DEFINED, $slugs);
}
public function test_module_option_helpers(): void
{
$module = new AccountTransferPaymentModule();
$this->assertSame(['account_owner', 'iban'], $module->requiredOptionKeys());
// Rückgabe folgt der Schema-Reihenfolge, unbekannte Keys werden verworfen.
$this->assertSame(
['account_owner' => 'Kasse', 'iban' => 'DE123'],
$module->sanitizeConfiguration(['iban' => 'DE123', 'account_owner' => 'Kasse', 'evil' => 'x'])
);
$this->assertFalse($module->isConfigurationComplete(['iban' => 'DE123']));
$this->assertTrue($module->isConfigurationComplete(['iban' => 'DE123', 'account_owner' => 'Kasse']));
}
public function test_defaults_are_built_from_modules(): void
{
$defaults = PaymentMethod::defaults();
$this->assertSame('Überweisung auf Veranstaltungskonto', $defaults[PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION]['name']);
$this->assertArrayHasKey(PaymentMethod::PAYMENT_NOT_DEFINED, $defaults);
}
}