Das Adressbuch bekommt in der Verwaltung eine eigene Kachel statt nur über die Verträge erreichbar zu sein, denn es speist auch den Zahlungsverkehr. Auf der Adressliste klappt zu jeder Adresse ein <details>-Element ihre letzten Buchungen auf (Buchung::letzteFuerEmpfaenger, kontenübergreifend nach dem exakten Empfängernamen – die Buchung führt kein Fremdschlüsselfeld auf die Adresse). Die Empfängerauswahl im Zahlungsverkehr-Formular ist jetzt ein Typeahead: kombobox.js (bereits für die Kategorie im Kontoblatt im Einsatz) baut das <select> in ein gefiltertes Eingabefeld um. Geprüft (PHP- und E2E-Tests), dass "Empfänger ins Adressbuch übernehmen" beim Erfassen einer Überweisung tatsächlich eine Adresse anlegt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
154 lines
5.9 KiB
PHP
154 lines
5.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Finanzmanager\Models\Adresse;
|
||
use Finanzmanager\Models\Buchung;
|
||
use Finanzmanager\Models\Konto;
|
||
|
||
/**
|
||
* Fix #51: Adressverwaltung.
|
||
*
|
||
* Drei Teile: die letzten Buchungen zu einer Adresse (kontenübergreifend, nach
|
||
* dem Empfängernamen), die entsprechende Anzeige auf der Adressseite, und der
|
||
* Typeahead im Zahlungsverkehr, der aus dem Adressbuch heraus Name, IBAN und
|
||
* BIC übernimmt – umgebaut wie schon die Kategorie im Kontoblatt (Test 35),
|
||
* mit demselben kombobox.js.
|
||
*/
|
||
class AdressverwaltungTest extends TestCase
|
||
{
|
||
protected string $testName = 'Test 38: Adressverwaltung';
|
||
|
||
private int $girokonto = 0;
|
||
private int $sparkonto = 0;
|
||
|
||
public function run(): void
|
||
{
|
||
$this->mandantAnlegen();
|
||
$this->stammdaten();
|
||
|
||
$this->letzteBuchungenNachEmpfaengerKontenuebergreifend();
|
||
$this->limitUndReihenfolge();
|
||
$this->ohneTrefferBleibtDieListeLeer();
|
||
$this->adressenseiteZeigtDieLetztenBuchungen();
|
||
$this->zahlungsempfaengerIstEinTypeahead();
|
||
}
|
||
|
||
private function stammdaten(): void
|
||
{
|
||
$this->girokonto = Konto::anlegen([
|
||
'name' => 'Adressen-Giro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||
]);
|
||
$this->sparkonto = Konto::anlegen([
|
||
'name' => 'Adressen-Sparbuch', 'kontoart' => 'sparkonto', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||
]);
|
||
}
|
||
|
||
private function letzteBuchungenNachEmpfaengerKontenuebergreifend(): void
|
||
{
|
||
Buchung::anlegen([
|
||
'konto_id' => $this->girokonto, 'datum' => '2026-01-10', 'betrag' => -20.0,
|
||
'empfaenger' => 'Stadtwerke Musterstadt', 'buchungstext' => 'Strom Januar',
|
||
]);
|
||
Buchung::anlegen([
|
||
'konto_id' => $this->sparkonto, 'datum' => '2026-02-10', 'betrag' => -21.0,
|
||
'empfaenger' => 'Stadtwerke Musterstadt', 'buchungstext' => 'Strom Februar',
|
||
]);
|
||
Buchung::anlegen([
|
||
'konto_id' => $this->girokonto, 'datum' => '2026-01-15', 'betrag' => -5.0,
|
||
'empfaenger' => 'Anderer Empfänger', 'buchungstext' => 'Nichts damit zu tun',
|
||
]);
|
||
|
||
$treffer = Buchung::letzteFuerEmpfaenger('Stadtwerke Musterstadt');
|
||
|
||
$this->assertEquals(2, count($treffer), 'Beide Konten liefern ihre Buchung zum Empfänger');
|
||
$this->assertEquals('Adressen-Sparbuch', $treffer[0]['kontoname'], 'Das Sparbuch führt (jüngstes Datum zuerst)');
|
||
$this->assertEquals('Adressen-Giro', $treffer[1]['kontoname'], 'Das Girokonto folgt');
|
||
}
|
||
|
||
private function limitUndReihenfolge(): void
|
||
{
|
||
for ($i = 1; $i <= 7; $i++) {
|
||
Buchung::anlegen([
|
||
'konto_id' => $this->girokonto, 'datum' => sprintf('2026-03-%02d', $i), 'betrag' => -1.0 * $i,
|
||
'empfaenger' => 'Vielbucher GmbH', 'buchungstext' => 'Rechnung ' . $i,
|
||
]);
|
||
}
|
||
|
||
$treffer = Buchung::letzteFuerEmpfaenger('Vielbucher GmbH', 3);
|
||
|
||
$this->assertEquals(3, count($treffer), 'Das Limit wird eingehalten');
|
||
$this->assertEquals('2026-03-07', $treffer[0]['datum'], 'Die jüngste Buchung steht vorn');
|
||
$this->assertEquals('2026-03-05', $treffer[2]['datum'], 'Danach bricht die Liste beim Limit ab');
|
||
}
|
||
|
||
private function ohneTrefferBleibtDieListeLeer(): void
|
||
{
|
||
$this->assertEquals([], Buchung::letzteFuerEmpfaenger('Niemand hat je an diesen Namen gebucht'), 'Keine Treffer, keine Zeilen');
|
||
$this->assertEquals([], Buchung::letzteFuerEmpfaenger(''), 'Ein leerer Empfänger liefert keine Zeilen statt aller Buchungen');
|
||
}
|
||
|
||
private function adressenseiteZeigtDieLetztenBuchungen(): void
|
||
{
|
||
Adresse::anlegen(['name' => 'Stadtwerke Musterstadt', 'iban' => 'DE02120300000000202051']);
|
||
Adresse::anlegen(['name' => 'Ganz ohne Buchung']);
|
||
|
||
$adressen = Adresse::alle();
|
||
foreach ($adressen as $nummer => $adresse) {
|
||
$adressen[$nummer]['buchungen'] = Buchung::letzteFuerEmpfaenger((string)$adresse['name']);
|
||
}
|
||
|
||
$html = $this->adressenHtml($adressen);
|
||
|
||
$this->assert(str_contains($html, 'Strom Februar'), 'Die letzte Buchung zur Adresse erscheint');
|
||
$this->assert(str_contains($html, 'Strom Januar'), 'Auch die ältere Buchung erscheint');
|
||
$this->assert(str_contains($html, 'Noch keine Buchung mit diesem Empfänger.'), 'Eine Adresse ohne Buchung sagt das auch so');
|
||
$this->assert(str_contains($html, '<details>'), 'Die Buchungen stehen hinter einem Klappelement');
|
||
}
|
||
|
||
private function zahlungsempfaengerIstEinTypeahead(): void
|
||
{
|
||
$html = $this->zahlungsverkehrFormularHtml();
|
||
|
||
$this->assert(
|
||
(bool)preg_match(
|
||
'#<select id="adresse" name="adresse" aria-label="Aus dem Adressbuch" data-adressauswahl data-kombobox>#',
|
||
$html
|
||
),
|
||
'Die Adressauswahl ist als Kombobox (Typeahead) markiert'
|
||
);
|
||
$this->assert(str_contains($html, '/static/kombobox.js'), 'kombobox.js wird eingebunden');
|
||
}
|
||
|
||
/** @param list<array<string,mixed>> $adressen */
|
||
private function adressenHtml(array $adressen): string
|
||
{
|
||
return $this->rendere('vertraege/adressen.php', [
|
||
'adressen' => $adressen,
|
||
'suche' => '',
|
||
]);
|
||
}
|
||
|
||
private function zahlungsverkehrFormularHtml(): string
|
||
{
|
||
return $this->rendere('zahlungsverkehr/formular.php', [
|
||
'auftrag' => null,
|
||
'konten' => [],
|
||
'adressen' => Adresse::alle(),
|
||
'kategorien' => [],
|
||
'klassen' => [],
|
||
]);
|
||
}
|
||
|
||
/** @param array<string,mixed> $daten */
|
||
private function rendere(string $view, array $daten): string
|
||
{
|
||
$daten += ['titel' => 'Test', 'flash' => [], 'benutzer' => null, 'csrfToken' => 'x', 'aktuellerPfad' => '/'];
|
||
|
||
extract($daten, EXTR_SKIP);
|
||
|
||
ob_start();
|
||
include dirname(__DIR__) . '/src/Views/' . $view;
|
||
return (string)ob_get_clean();
|
||
}
|
||
}
|