174 lines
7.3 KiB
PHP
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']]),
|
|
);
|
|
}
|
|
}
|