Finanzmanager/tests/28_UmsaetzeLoeschenTest.php
fds2610-bot 9839bd5b16 Banking: Umsätze sammelweise löschen
Die Umsatzlisten haben jetzt dieselbe Mehrfachauswahl wie ein Kontoblatt:
ein Ankreuzfeld vor jedem Umsatz, eines in der Titelleiste für alle
angezeigten und darüber der Knopf „Auswahl löschen“. Er steht in jedem
Reiter – offen, übernommen und verworfen –, nennt die Anzahl der Auswahl
und bleibt ohne Auswahl gesperrt. Bisher ließ sich ein abgerufener Umsatz
nur übernehmen oder verwerfen; wegräumen ging gar nicht.

Gelöscht wird allein die abgerufene Zeile. Eine daraus entstandene
Buchung gehört ins Kontoblatt und bleibt dort stehen – wer sie loswerden
will, löscht sie dort, und dann kehrt ihr Umsatz ohnehin in die
Vorschlagsliste zurück. Mit dem Umsatz geht aber sein Fingerabdruck: Was
im nächsten Abrufzeitraum liegt, kommt danach wieder herein, bei einem
verworfenen Umsatz also erneut als offen.

- Neue Route POST /banking/umsaetze/loeschen auf
  Bankumsatz::mehrereLoeschen(); der Status hängt am Formularziel, sodass
  der Weg in denselben Reiter zurückführt. Ohne JavaScript weist der
  Controller eine leere Auswahl ab.
- Die Liste ist keine Tabelle, sondern eine Folge von Karten. Die
  Kopfzeile, die eine Tabelle von selbst hätte, gibt es deshalb neu als
  .listenkopf; die Mechanik dahinter (.alle-waehlen mit data-waehlt) ist
  unverändert die des Kontoblatts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:24:14 +02:00

238 lines
9.8 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
use Finanzmanager\Database;
use Finanzmanager\Models\Bankumsatz;
use Finanzmanager\Models\Bankzugang;
use Finanzmanager\Models\Buchung;
use Finanzmanager\Models\Kategorie;
use Finanzmanager\Models\Klasse;
use Finanzmanager\Models\Konto;
/**
* Sammellöschen in den Umsatzlisten des Bankings.
*
* Geprüft wird, was hinter dem Knopf *Auswahl löschen* passiert: Die gewählten
* Umsätze verschwinden – und zwar nur sie. Eine schon erzeugte Buchung gehört
* ins Kontoblatt und bleibt dort stehen; sie verliert lediglich den Umsatz, der
* sie belegt hat. Der Knopf steht in jedem Reiter, also müssen offene,
* übernommene und verworfene Umsätze gleichermaßen gehen.
*/
class UmsaetzeLoeschenTest extends TestCase
{
protected string $testName = 'Test 28: Bankumsätze sammelweise löschen';
private int $giro = 0;
private int $zugang = 0;
private int $bankkonto = 0;
private int $laufendeNummer = 0;
public function run(): void
{
$this->mandantAnlegen();
$this->stammdaten();
$this->auswahlVerschwindetAusDerListe();
$this->buchungUeberlebtDenGeloeschtenUmsatz();
$this->jederStatusLaesstSichLoeschen();
$this->leereUndUnbekannteAuswahlBleibtFolgenlos();
$this->jedeAnsichtBietetDieAuswahlAn();
}
private function stammdaten(): void
{
$this->giro = Konto::anlegen([
'name' => 'Sammelgiro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
]);
$this->zugang = Bankzugang::anlegen([
'name' => 'Sammelbank',
'bankname' => 'Sammelbank AG',
'blz' => '20041112',
'fints_url' => 'https://banking.example.org/fints',
'benutzerkennung' => 'testkunde',
'pin' => 'geheim1234',
'abrufzeitraum' => 'letzter_abruf',
'aktiv' => 1,
]);
$this->bankkonto = (int)Bankzugang::findeOderLegeKontoAn(
$this->zugang,
'DE53200411120847626901',
'847626901',
'Girokonto'
)['id'];
Bankzugang::ordneKontoZu($this->bankkonto, $this->giro);
}
private function umsatzAnlegen(string $zweck, float $betrag = -19.99): int
{
// Jeder Umsatz bekommt einen eigenen Tag: Sonst fiele der Fingerabdruck
// zweier gleich lautender Zeilen zusammen und der zweite käme nie an.
$tag = str_pad((string)(++$this->laufendeNummer), 2, '0', STR_PAD_LEFT);
$id = Bankumsatz::anlegen([
'zugang_id' => $this->zugang,
'konto_id' => $this->giro,
'bank_konto_id' => $this->bankkonto,
'extern_id' => Bankumsatz::externId($this->bankkonto, '2026-07-' . $tag, $betrag, $zweck),
'buchungstag' => '2026-07-' . $tag,
'wertstellung' => '2026-07-' . $tag,
'betrag' => $betrag,
'empfaenger' => 'Sammelempfänger',
'verwendungszweck' => $zweck,
'buchungsart' => 'LASTSCHRIFT',
]);
$this->assertNotEmpty($id, 'Umsatz angelegt: ' . $zweck);
return (int)$id;
}
private function existiert(int $umsatzId): bool
{
return Database::fetchValue('SELECT 1 FROM bank_umsaetze WHERE id = ?', [$umsatzId]) !== null;
}
/** Die Mehrfachauswahl trifft genau die angekreuzten Zeilen. */
private function auswahlVerschwindetAusDerListe(): void
{
$eins = $this->umsatzAnlegen('Sammelstrom');
$zwei = $this->umsatzAnlegen('Sammelwasser');
$drei = $this->umsatzAnlegen('Sammelgas');
$anzahl = Bankumsatz::mehrereLoeschen([$eins, $drei]);
$this->assertEquals(2, $anzahl, 'Zwei ausgewählte Umsätze gelöscht');
$this->assert(!$this->existiert($eins), 'Der erste ausgewählte Umsatz ist weg');
$this->assert(!$this->existiert($drei), 'Der zweite ausgewählte Umsatz ist weg');
$this->assert($this->existiert($zwei), 'Der nicht ausgewählte Umsatz steht noch da');
$offene = array_map('intval', array_column(Bankumsatz::liste(['status' => 'offen']), 'id'));
$this->assert(!in_array($eins, $offene, true), 'Der gelöschte Umsatz steht nicht mehr in der Liste');
$this->assert(in_array($zwei, $offene, true), 'Der übrige Umsatz steht weiter zum Abgleich bereit');
}
/**
* Der Unterschied zum Kontoblatt: Hier wird die abgerufene Zeile gelöscht,
* nicht die Buchung. Was einmal gebucht ist, bleibt gebucht.
*/
private function buchungUeberlebtDenGeloeschtenUmsatz(): void
{
$umsatz = $this->umsatzAnlegen('Sammelmiete', -750.00);
$buchungId = Bankumsatz::uebernehmen($umsatz, []);
$this->assertEquals(1, Bankumsatz::mehrereLoeschen([$umsatz]), 'Der übernommene Umsatz wurde gelöscht');
$this->assert(!$this->existiert($umsatz), 'Der übernommene Umsatz ist weg');
$buchung = Buchung::find((int)$buchungId);
$this->assertNotEmpty($buchung, 'Die Buchung steht weiter im Kontoblatt');
$this->assertEquals(-750.00, (float)$buchung['betrag'], 'Und zwar unverändert');
}
/** Der Knopf steht in jedem Reiter – offen, übernommen und verworfen. */
private function jederStatusLaesstSichLoeschen(): void
{
$offen = $this->umsatzAnlegen('Sammelversicherung', -44.00);
$uebernommen = $this->umsatzAnlegen('Sammelrundfunk', -55.08);
$verworfen = $this->umsatzAnlegen('Sammelfehlbuchung', -1.00);
Bankumsatz::uebernehmen($uebernommen, []);
Bankumsatz::verwerfen($verworfen);
$anzahl = Bankumsatz::mehrereLoeschen([$offen, $uebernommen, $verworfen]);
$this->assertEquals(3, $anzahl, 'Aus jedem Reiter lässt sich löschen');
$this->assert(!$this->existiert($offen), 'Offener Umsatz gelöscht');
$this->assert(!$this->existiert($uebernommen), 'Übernommener Umsatz gelöscht');
$this->assert(!$this->existiert($verworfen), 'Verworfener Umsatz gelöscht');
}
/**
* Ohne Auswahl darf nichts geschehen – der Controller weist den leeren Fall
* ab, das Modell darf ihn trotzdem nicht als „alles löschen“ lesen.
*/
private function leereUndUnbekannteAuswahlBleibtFolgenlos(): void
{
$bleibt = $this->umsatzAnlegen('Sammelabo', -9.99);
$this->assertEquals(0, Bankumsatz::mehrereLoeschen([]), 'Leere Auswahl löscht nichts');
$this->assertEquals(0, Bankumsatz::mehrereLoeschen([0, -3]), 'Unbrauchbare Nummern löschen nichts');
$this->assertEquals(0, Bankumsatz::mehrereLoeschen([$bleibt + 100000]), 'Unbekannte Nummer löscht nichts');
$this->assert($this->existiert($bleibt), 'Der vorhandene Umsatz ist unangetastet');
// Doppelt angekreuzt gibt es nicht, doppelt gesendet schon – die Anzahl
// in der Meldung darf dadurch nicht steigen.
$this->assertEquals(1, Bankumsatz::mehrereLoeschen([$bleibt, $bleibt]), 'Doppelte Nummer zählt einmal');
}
/**
* Die Oberfläche: In jedem Reiter stehen die Ankreuzfelder vor den Umsätzen,
* das Feld „alle wählen“ in der Titelleiste und darüber der Knopf.
*/
private function jedeAnsichtBietetDieAuswahlAn(): void
{
$offen = $this->umsatzAnlegen('Ansichtsstrom', -31.00);
$uebernommen = $this->umsatzAnlegen('Ansichtsmiete', -420.00);
$verworfen = $this->umsatzAnlegen('Ansichtsdublette', -2.50);
Bankumsatz::uebernehmen($uebernommen, []);
Bankumsatz::verwerfen($verworfen);
$erwartet = ['offen' => $offen, 'uebernommen' => $uebernommen, 'verworfen' => $verworfen];
foreach ($erwartet as $status => $umsatzId) {
$html = $this->ansicht($status);
$this->assert(
str_contains($html, 'action="/banking/umsaetze/loeschen?status=' . $status . '"'),
'Reiter „' . $status . '“: Das Löschformular kehrt in denselben Reiter zurück'
);
$this->assert(
str_contains($html, '>Auswahl löschen</button>'),
'Reiter „' . $status . '“: Der Knopf „Auswahl löschen“ steht über der Liste'
);
$this->assert(
str_contains($html, 'class="alle-waehlen" data-waehlt="umsatzauswahl"'),
'Reiter „' . $status . '“: Die Titelleiste hat ein Feld „alle wählen“'
);
$this->assert(
str_contains($html, 'name="umsaetze[]" value="' . $umsatzId . '" form="umsatzauswahl"'),
'Reiter „' . $status . '“: Vor dem Umsatz steht sein Ankreuzfeld'
);
}
// Ohne Umsätze steht auch kein Löschknopf da – sonst zeigte die leere
// Liste eine Schaltfläche, die nichts treffen kann.
Bankumsatz::mehrereLoeschen(array_values($erwartet));
$this->assert(
!str_contains($this->ansicht('verworfen'), '>Auswahl löschen</button>'),
'Ohne Umsätze gibt es nichts zu löschen'
);
}
/** Rendert die Abgleichliste eines Reiters so, wie der Controller es täte. */
private function ansicht(string $status): string
{
$umsaetze = [];
foreach (Bankumsatz::liste(['status' => $status]) as $umsatz) {
$umsatz['vorschlag'] = $status === 'offen' ? Bankumsatz::vorschlag($umsatz) : null;
$umsaetze[] = $umsatz;
}
$titel = 'Umsätze abgleichen';
$flash = [];
$benutzer = null;
$csrfToken = 'x';
$kategorien = Kategorie::baumFlach(null, true);
$klassen = Klasse::alle();
$konten = Konto::alle(true);
$offen = Bankumsatz::anzahlOffen();
ob_start();
include dirname(__DIR__) . '/src/Views/banking/umsaetze.php';
return (string)ob_get_clean();
}
}