Files
mareike/tests/Unit/BankStatementParseTest.php
2026-09-09 16:55:08 +02:00

174 lines
7.3 KiB
PHP

<?php
namespace Tests\Unit;
use App\Providers\BankStatementParseProvider;
use App\Support\BankStatementParseException;
use App\ValueObjects\BankStatementRuleset;
use PHPUnit\Framework\TestCase;
/**
* Der Parser -- zahlartneutral und ohne Framework. Getestet werden die Formatfallen, an denen ein
* Bank-Export scheitert: Zeichensatz, Trennzeichen, Zahlformat, fehlende Spalten.
*/
class BankStatementParseTest extends TestCase
{
private function ruleset(array $overrides = []): BankStatementRuleset
{
$base = [
'delimiter' => ';',
'enclosure' => '',
'charset' => 'Windows-1252',
'has_header' => true,
'date_format' => 'd.m.Y',
'decimal_separator' => ',',
'thousands_separator' => '.',
'columns' => [
'payment_date' => 'Buchungstag',
'purpose' => 'Verwendungszweck',
'amount' => 'Betrag',
'payer_name' => 'Name Zahlungsbeteiligter',
'payer_iban' => 'IBAN Zahlungsbeteiligter',
],
];
return new BankStatementRuleset(
delimiter: $overrides['delimiter'] ?? $base['delimiter'],
enclosure: $overrides['enclosure'] ?? $base['enclosure'],
charset: $overrides['charset'] ?? $base['charset'],
hasHeader: $overrides['has_header'] ?? $base['has_header'],
dateFormat: $overrides['date_format'] ?? $base['date_format'],
decimalSeparator: $overrides['decimal_separator'] ?? $base['decimal_separator'],
thousandsSeparator: $overrides['thousands_separator'] ?? $base['thousands_separator'],
columns: $overrides['columns'] ?? $base['columns'],
);
}
/** Das Format, das die GLS heute liefert: Semikolon, keine Anführungszeichen, CRLF. */
public function test_reads_the_semicolon_export_without_enclosures(): void
{
$csv = "Buchungstag;Valutadatum;Name Zahlungsbeteiligter;IBAN Zahlungsbeteiligter;Verwendungszweck;Betrag\r\n"
. "09.09.2026;09.09.2026;Max Meier;DE02120300000000202051;Sommerlager - Beitrag Max Meier;120,00\r\n";
$transactions = new BankStatementParseProvider()->parse($csv, $this->ruleset());
$this->assertCount(1, $transactions);
$this->assertSame('2026-09-09', $transactions[0]->paymentDate->format('Y-m-d'));
$this->assertSame(120.0, $transactions[0]->amount->getAmount());
$this->assertSame('Max Meier', $transactions[0]->payerName);
$this->assertSame('DE02120300000000202051', $transactions[0]->payerIban);
$this->assertSame('Sommerlager - Beitrag Max Meier', $transactions[0]->purpose);
// Kopfzeile ist Zeile 1.
$this->assertSame(2, $transactions[0]->rowNumber);
}
public function test_reads_comma_separated_export_with_enclosures(): void
{
$csv = "\"Buchungstag\",\"Verwendungszweck\",\"Betrag\"\n"
. "\"09.09.2026\",\"Beitrag, erste Rate\",\"85,50\"\n";
$transactions = new BankStatementParseProvider()->parse(
$csv,
$this->ruleset([
'delimiter' => ',',
'enclosure' => '"',
'columns' => ['payment_date' => 'Buchungstag', 'purpose' => 'Verwendungszweck', 'amount' => 'Betrag'],
]),
);
$this->assertCount(1, $transactions);
// Das Komma im Zweck darf die Zeile nicht zerreißen.
$this->assertSame('Beitrag, erste Rate', $transactions[0]->purpose);
$this->assertSame(85.5, $transactions[0]->amount->getAmount());
}
public function test_converts_windows_1252_umlauts(): void
{
$csv = mb_convert_encoding(
"Buchungstag;Verwendungszweck;Betrag\r\n09.09.2026;Beitrag Jörg Müller;60,00\r\n",
'Windows-1252',
'UTF-8',
);
$transactions = new BankStatementParseProvider()->parse($csv, $this->ruleset([
'columns' => ['payment_date' => 'Buchungstag', 'purpose' => 'Verwendungszweck', 'amount' => 'Betrag'],
]));
$this->assertSame('Beitrag Jörg Müller', $transactions[0]->purpose);
}
/**
* Der Schutz gegen Doppel-Kodierung: Liefert die Bank entgegen der Einstellung UTF-8, darf aus
* „Müller" kein „Müller" werden. Die Konvertierung würde technisch gelingen -- der Fehler fiele
* erst auf der Rechnung auf.
*/
public function test_does_not_double_encode_utf8_content(): void
{
$csv = "Buchungstag;Verwendungszweck;Betrag\r\n09.09.2026;Beitrag Jörg Müller;60,00\r\n";
$transactions = new BankStatementParseProvider()->parse($csv, $this->ruleset([
'charset' => 'Windows-1252',
'columns' => ['payment_date' => 'Buchungstag', 'purpose' => 'Verwendungszweck', 'amount' => 'Betrag'],
]));
$this->assertSame('Beitrag Jörg Müller', $transactions[0]->purpose);
}
public function test_parses_german_thousands_and_negative_amounts(): void
{
$csv = "Buchungstag;Verwendungszweck;Betrag\n"
. "09.09.2026;Grosse Zahlung;1.234,56\n"
. "10.09.2026;Rueckbuchung;-56,00\n";
$transactions = new BankStatementParseProvider()->parse($csv, $this->ruleset([
'columns' => ['payment_date' => 'Buchungstag', 'purpose' => 'Verwendungszweck', 'amount' => 'Betrag'],
]));
// Der Parser filtert nicht -- ob eine Belastung zählt, entscheidet das Zahlungsmodul.
$this->assertCount(2, $transactions);
$this->assertSame(1234.56, $transactions[0]->amount->getAmount());
$this->assertSame(-56.0, $transactions[1]->amount->getAmount());
$this->assertTrue($transactions[0]->isCredit());
$this->assertFalse($transactions[1]->isCredit());
}
/** Summenzeilen und Fußnoten sind keine Umsätze und dürfen den Import nicht abbrechen. */
public function test_skips_rows_without_date_or_amount(): void
{
$csv = "Buchungstag;Verwendungszweck;Betrag\n"
. "09.09.2026;Beitrag;120,00\n"
. ";Summe;;\n"
. "10.09.2026;Ohne Betrag;\n";
$transactions = new BankStatementParseProvider()->parse($csv, $this->ruleset([
'columns' => ['payment_date' => 'Buchungstag', 'purpose' => 'Verwendungszweck', 'amount' => 'Betrag'],
]));
$this->assertCount(1, $transactions);
}
/** Der Normalfall bei falsch eingestelltem Ruleset -- die Meldung muss die Spalte nennen. */
public function test_names_the_missing_column(): void
{
$csv = "Buchungstag;Verwendungszweck\n09.09.2026;Beitrag\n";
$this->expectException(BankStatementParseException::class);
$this->expectExceptionMessageMatches('/Betrag/');
new BankStatementParseProvider()->parse($csv, $this->ruleset([
'columns' => ['payment_date' => 'Buchungstag', 'purpose' => 'Verwendungszweck', 'amount' => 'Betrag'],
]));
}
public function test_rejects_a_ruleset_without_required_mapping(): void
{
$this->expectException(BankStatementParseException::class);
$this->expectExceptionMessageMatches('/amount/');
new BankStatementParseProvider()->parse(
"Buchungstag;Verwendungszweck\n",
$this->ruleset(['columns' => ['payment_date' => 'Buchungstag', 'purpose' => 'Verwendungszweck']]),
);
}
}