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