Automatischer Zahlungsparser
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Domains\ParticipantRefund\Actions\CreateRefundDocument\CreateRefundDocumentCommand;
|
||||
use App\Enumerations\RefundAccountSource;
|
||||
use App\EventPaymentModules\DTO\GetRefundDataRequest;
|
||||
use App\EventPaymentModules\DTO\GetRefundDataResponse;
|
||||
use App\EventPaymentModules\Modules\AccountTransferPaymentModule;
|
||||
use App\EventPaymentModules\Modules\UndefinedPaymentModule;
|
||||
use App\Models\EventParticipant;
|
||||
use App\Support\Iban;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Auf welches Konto wäre zu erstatten -- die Frage, die jede Zahlungsart beantwortet.
|
||||
*
|
||||
* Ohne Datenbank: Die Module bekommen den Teilnehmer und ihre Konfiguration hereingereicht und
|
||||
* fragen selbst nichts ab. Den Framework-Kontext braucht der Test trotzdem, weil der `date`-Cast
|
||||
* des Models sein Format über die Verbindung auflöst.
|
||||
*/
|
||||
class RefundDataTest extends TestCase
|
||||
{
|
||||
private const string VALID_IBAN = 'DE02120300000000202051';
|
||||
|
||||
private function participant(array $paymentOptions, ?string $lastPaymentDate = null): EventParticipant
|
||||
{
|
||||
$participant = new EventParticipant();
|
||||
$participant->setRawAttributes(['firstname' => 'Max', 'lastname' => 'Meier'], true);
|
||||
$participant->payment_options = $paymentOptions;
|
||||
$participant->last_payment_date = $lastPaymentDate === null
|
||||
? null
|
||||
: CarbonImmutable::createFromFormat('!Y-m-d', $lastPaymentDate);
|
||||
|
||||
return $participant;
|
||||
}
|
||||
|
||||
private function refundData(EventParticipant $participant)
|
||||
{
|
||||
return new AccountTransferPaymentModule()->getRefundData(
|
||||
new GetRefundDataRequest(participant: $participant)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_the_payer_account_from_the_bank_import_is_offered(): void
|
||||
{
|
||||
$response = $this->refundData($this->participant([
|
||||
'payer_iban' => self::VALID_IBAN,
|
||||
'payer_account_owner' => 'Max Meier',
|
||||
], '2026-09-09'));
|
||||
|
||||
$this->assertSame(RefundAccountSource::Known, $response->source);
|
||||
$this->assertTrue($response->hasAccount());
|
||||
$this->assertSame(self::VALID_IBAN, $response->accountIban);
|
||||
$this->assertSame('Max Meier', $response->accountOwner);
|
||||
$this->assertSame('Zahlungseingang vom 09.09.2026', $response->sourceNote);
|
||||
}
|
||||
|
||||
public function test_the_iban_is_normalized(): void
|
||||
{
|
||||
$response = $this->refundData($this->participant([
|
||||
'payer_iban' => 'de02 1203 0000 0000 2020 51',
|
||||
'payer_account_owner' => 'Max Meier',
|
||||
]));
|
||||
|
||||
$this->assertSame(self::VALID_IBAN, $response->accountIban);
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine IBAN mit Zahlendreher würde ungeprüft übernommen und das Geld ginge an eine fremde Person.
|
||||
* Lieber gar nichts melden -- dann fragt die Erstattung wie bisher nach dem Ursprungskonto.
|
||||
*/
|
||||
public function test_an_invalid_iban_falls_back_to_asking(): void
|
||||
{
|
||||
$response = $this->refundData($this->participant([
|
||||
'payer_iban' => 'DE02120300000000202015',
|
||||
'payer_account_owner' => 'Max Meier',
|
||||
]));
|
||||
|
||||
$this->assertSame(RefundAccountSource::Origin, $response->source);
|
||||
$this->assertNull($response->accountIban);
|
||||
}
|
||||
|
||||
/** Ohne Inhaber lässt sich nicht auszahlen -- der SEPA-Export braucht beides. */
|
||||
public function test_a_missing_owner_falls_back_to_asking(): void
|
||||
{
|
||||
$response = $this->refundData($this->participant(['payer_iban' => self::VALID_IBAN]));
|
||||
|
||||
$this->assertSame(RefundAccountSource::Origin, $response->source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Überweiser ohne erfassten Zahlungseingang: Es gab ein Ursprungskonto, wir kennen es nur nicht --
|
||||
* der Teili wird danach gefragt.
|
||||
*/
|
||||
public function test_a_transfer_without_payment_data_asks_for_the_origin_account(): void
|
||||
{
|
||||
$response = $this->refundData($this->participant([]));
|
||||
|
||||
$this->assertSame(RefundAccountSource::Origin, $response->source);
|
||||
$this->assertTrue($response->source->needsInput());
|
||||
}
|
||||
|
||||
/**
|
||||
* Barzahlung: Es gab nie ein Konto. Die Frage nach dem Ursprungskonto wäre sinnlos, und die
|
||||
* Erklärung, es sei dasselbe Konto, unwahr -- deshalb ein eigener Fall, kein „unbekannt".
|
||||
*/
|
||||
public function test_cash_payment_has_no_origin_account(): void
|
||||
{
|
||||
$response = new UndefinedPaymentModule()->getRefundData(new GetRefundDataRequest(
|
||||
// Selbst wenn zufällig Zahlerdaten in den Optionen stünden: bar ist bar.
|
||||
participant: $this->participant([
|
||||
'payer_iban' => self::VALID_IBAN,
|
||||
'payer_account_owner' => 'Max Meier',
|
||||
]),
|
||||
));
|
||||
|
||||
$this->assertSame(RefundAccountSource::None, $response->source);
|
||||
$this->assertNull($response->accountIban);
|
||||
$this->assertTrue($response->source->needsInput());
|
||||
}
|
||||
|
||||
/** Die Vorgabe der Basisklasse ist die strengere Annahme: Es gab ein Ursprungskonto. */
|
||||
public function test_the_default_assumes_an_origin_account(): void
|
||||
{
|
||||
$this->assertSame(RefundAccountSource::Origin, new GetRefundDataResponse()->source);
|
||||
}
|
||||
|
||||
/** Welche Erklärung gilt, hängt allein an der Herkunft -- eine Quelle für Seite und Beleg. */
|
||||
public function test_the_declaration_text_follows_the_source(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
CreateRefundDocumentCommand::ACCOUNT_DECLARATION_TEXT,
|
||||
RefundAccountSource::Origin->accountDeclarationText(),
|
||||
);
|
||||
$this->assertSame(
|
||||
CreateRefundDocumentCommand::ACCOUNT_DECLARATION_TEXT,
|
||||
RefundAccountSource::Known->accountDeclarationText(),
|
||||
);
|
||||
$this->assertSame(
|
||||
CreateRefundDocumentCommand::OWN_ACCOUNT_DECLARATION_TEXT,
|
||||
RefundAccountSource::None->accountDeclarationText(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert wie {@see Iban::format()}, damit beide Schreibweisen nebeneinander stimmig aussehen:
|
||||
* `DE02 1203 0000 0000 2020 51` wird zu `DE02 •••• •••• •••• ••20 51`. Die letzten vier Stellen
|
||||
* liegen bei 22 Zeichen über zwei Gruppen -- das ist bei der unmaskierten Form genauso.
|
||||
*/
|
||||
public function test_masking_keeps_country_check_digits_and_the_last_four(): void
|
||||
{
|
||||
$this->assertSame('DE02 •••• •••• •••• ••20 51', Iban::mask(self::VALID_IBAN));
|
||||
// Leerzeichen in der Eingabe ändern nichts.
|
||||
$this->assertSame('DE02 •••• •••• •••• ••20 51', Iban::mask('DE02 1203 0000 0000 2020 51'));
|
||||
|
||||
// Die Mitte ist wirklich weg -- niemand liest die Kontonummer aus der Maske heraus.
|
||||
$this->assertStringNotContainsString('1203', Iban::mask(self::VALID_IBAN));
|
||||
}
|
||||
|
||||
/** Zu kurz zum Erkennen ist auch zu kurz zum Maskieren -- dann lieber alles verdecken. */
|
||||
public function test_masking_hides_everything_when_there_is_too_little(): void
|
||||
{
|
||||
$this->assertSame('•••• ••••', Iban::mask('DE021203'));
|
||||
$this->assertSame('•', Iban::mask(''));
|
||||
}
|
||||
|
||||
/** Die Punkte sind Mehrbyte-Zeichen -- byteweises Gruppieren zerschnitte sie zu Zeichensalat. */
|
||||
public function test_masking_produces_valid_utf8(): void
|
||||
{
|
||||
$this->assertTrue(mb_check_encoding(Iban::mask(self::VALID_IBAN), 'UTF-8'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user