Configurations for payment modules

This commit is contained in:
2026-07-29 15:49:26 +02:00
parent 8d6a4041c1
commit 78f4d813f9
19 changed files with 584 additions and 51 deletions
@@ -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 '';
}
}