Development 4.6.0 #12
@@ -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,
|
||||||
|
|||||||
+1
-1
@@ -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.
|
||||||
|
|||||||
@@ -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 '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user