Files
mareike/app/EventPaymentModules/AbstractEventPaymentModule.php
T

248 lines
9.1 KiB
PHP

<?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\Enumerations\RefundAccountSource;
use App\EventPaymentModules\DTO\GetRefundDataRequest;
use App\EventPaymentModules\DTO\GetRefundDataResponse;
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;
}
/**
* Standard-Konfiguration beim Anlegen der Tenant-Instanz. Standard: leer.
*
* @return array<string, mixed>
*/
public function defaultConfiguration(): array
{
return [];
}
/**
* Payer-seitige Eingaben, die eine Zahlungsart vom Teilnehmer benötigt (z.B. später SEPA-IBAN,
* PayPal-Mail). Standard: keine. Gleiche Form wie getOptions(): [{name,label,type,required}].
*
* @return array<int, array{name: string, label: string, type: string, required: bool}>
*/
public function getParticipantOptions(): array
{
return [];
}
// -----------------------------------------------------------------------------------------
// Aus dem jeweiligen Schema abgeleitete Helfer (Admin-Config = getOptions(),
// Teilnehmer-Eingaben = getParticipantOptions()).
// -----------------------------------------------------------------------------------------
/** @return array<int, string> */
public function requiredOptionKeys(): array
{
return $this->requiredKeys($this->getOptions());
}
/**
* @param array<string, mixed> $config
* @return array<string, mixed>
*/
public function sanitizeConfiguration(array $config): array
{
return $this->sanitizeAgainst($this->getOptions(), $config);
}
/** @param array<string, mixed> $config */
public function isConfigurationComplete(array $config): bool
{
return $this->allRequiredFilled($this->getOptions(), $config);
}
/**
* Optionen mit `scope => 'tenant'`: Sie gelten für den ganzen Mandanten und werden **nicht** pro
* Veranstaltung eingefroren.
*
* Beispiel ist das Kontoauszug-Format: Es beschreibt die Bank, nicht die Zusage an die
* Teilnehmenden. Läge es im Event-Snapshot, ließe sich nach einem Bankwechsel für laufende
* Aktionen nichts mehr importieren, bis das Format bei jeder einzeln nachgezogen wurde.
* IBAN und Kontoinhaber bleiben dagegen weiterhin pro Aktion eingefroren.
*
* @return array<int, string>
*/
public function tenantScopedOptionKeys(): array
{
return array_values(array_map(
static fn (array $option) => $option['name'],
array_filter($this->getOptions(), static fn (array $option) => ($option['scope'] ?? null) === 'tenant')
));
}
/**
* Entfernt die tenant-weiten Optionen aus einer Konfiguration -- angewandt auf den
* Copy-on-Assign-Snapshot einer Veranstaltung.
*
* @param array<string, mixed> $config
* @return array<string, mixed>
*/
public function stripTenantScopedOptions(array $config): array
{
return array_diff_key($config, array_flip($this->tenantScopedOptionKeys()));
}
/**
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
public function sanitizeParticipantOptions(array $input): array
{
return $this->sanitizeAgainst($this->getParticipantOptions(), $input);
}
/** @param array<string, mixed> $input */
public function participantOptionsComplete(array $input): bool
{
return $this->allRequiredFilled($this->getParticipantOptions(), $input);
}
/**
* Die Teilnehmer-Optionen, die im Anmeldeformular tatsächlich abgefragt werden.
*
* Optionen mit `system => true` fallen heraus: Sie liegen zwar in `payment_options`, entstehen aber
* im Programm statt durch eine Eingabe -- das Zahler-Konto etwa trägt der Import aus dem
* Kontoauszug nach. Im Schema müssen sie trotzdem stehen, sonst verwirft
* {@see sanitizeParticipantOptions()} sie als unbekannte Schlüssel.
*
* @return array<int, array{name: string, label: string, type: string, required: bool}>
*/
public function participantInputOptions(): array
{
return array_values(array_filter(
$this->getParticipantOptions(),
static fn (array $option): bool => ($option['system'] ?? false) !== true,
));
}
/**
* @param array<int, array{name: string, required?: bool}> $schema
* @return array<int, string>
*/
private function requiredKeys(array $schema): array
{
return array_values(array_map(
static fn (array $option) => $option['name'],
array_filter($schema, static fn(array $option) => $option['required'] ?? false)
));
}
/**
* Reduziert Eingaben auf die im Schema definierten Keys (unbekannte werden verworfen).
*
* @param array<int, array{name: string}> $schema
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
private function sanitizeAgainst(array $schema, array $input): array
{
$result = [];
foreach ($schema as $option) {
if (array_key_exists($option['name'], $input)) {
$result[$option['name']] = $input[$option['name']];
}
}
return $result;
}
/**
* Sind alle Pflichtfelder des Schemas befüllt (non-empty)?
*
* @param array<int, array{name: string, required?: bool}> $schema
* @param array<string, mixed> $input
*/
private function allRequiredFilled(array $schema, array $input): bool
{
foreach ($this->requiredKeys($schema) as $key) {
$value = $input[$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();
}
/**
* Standard: Es gab ein Ursprungskonto, wir kennen es nur nicht ({@see RefundAccountSource::Origin}).
*
* Bewusst die strengere Annahme. Der Teili wird dann gefragt, ob es dasselbe Konto ist, von dem der
* Beitrag kam -- die Kontrolle, die verhindert, dass sich über eine Erstattung Geld auf ein fremdes
* Konto umleiten lässt. Eine Zahlungsart ohne Ursprungskonto (Barzahlung) muss das ausdrücklich
* sagen; stillschweigend die Kontrolle fallen zu lassen wäre die falsche Vorgabe.
*/
public function getRefundData(GetRefundDataRequest $request): GetRefundDataResponse
{
return new GetRefundDataResponse();
}
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.
*
* `$request->amount` ist der noch offene Betrag. Ist nichts mehr offen, gilt unabhängig von der
* Zahlungsart derselbe Satz -- eine Zahlungsaufforderung wäre dann schlicht falsch.
*/
public function createInvoice(CreateInvoiceRequest $request): CreateInvoiceResponse
{
$response = new CreateInvoiceResponse();
$response->success = true;
$response->closingStatement = $request->amount->getAmount() <= 0
? 'Der Betrag ist bereits vollständig beglichen — vielen Dank.'
: $this->invoiceClosingStatement($request);
return $response;
}
/**
* Der je Zahlungsart variierende Schlusssatz der Rechnung
* ("Bitte überweise bis ..." / "wird abgebucht ..." / "wurde per PayPal beglichen").
*
* Wird nur bei tatsächlich offenem Betrag aufgerufen.
*/
abstract protected function invoiceClosingStatement(CreateInvoiceRequest $request): string;
}