Merge pull request 'Test: Umbuchungen spiegeln sich zuverlässig' (#33) from fds2610-bot/Finanzmanager:main into main
Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/33
This commit is contained in:
commit
ff022baf3a
2 changed files with 487 additions and 0 deletions
26
Changes.md
26
Changes.md
|
|
@ -1,5 +1,31 @@
|
|||
# Änderungen
|
||||
|
||||
## Spiegelung der Umbuchungen unter Test (17.08.2026)
|
||||
|
||||
Neuer Test `tests/30_UmbuchungSpiegelungTest.php`. Er nagelt die zwei Zusagen fest, die
|
||||
sich aus dem Datenmodell ergeben – eine Umbuchung ist EIN Datensatz mit `gegenkonto_id`,
|
||||
die Gegenseite leitet `Buchung::kontoblatt()` daraus ab:
|
||||
|
||||
1. **Was ein Gegenkonto trägt, steht im Kontoblatt dieses Gegenkontos** – auf allen vier
|
||||
Wegen, auf denen ein Gegenkonto an eine Buchung kommt: beim Anlegen, über die
|
||||
Schnellzuordnung im Kontoblatt, über die Buchungsmaske und als einzelne Splitzeile.
|
||||
Ein gewechseltes Gegenkonto bleibt nicht beim alten hängen, ein entferntes taucht
|
||||
nirgends mehr auf, und Such-, Zeitraum- und Kategoriefilter verstecken die Gegensicht
|
||||
nicht.
|
||||
2. **Wer die Buchung löscht, löscht beide Sichten** – Kontoblatt und Saldo des
|
||||
Gegenkontos sind danach wieder wie vorher, Splitzeilen bleiben nicht als Waisen
|
||||
zurück. Eine Gegenseite ohne Ursprungsbuchung kann nicht entstehen: Sie trägt die
|
||||
Nummer des Originals, ein Löschen „auf dem Gegenkonto“ trifft deshalb dieses.
|
||||
|
||||
Dazu eine Invariantenprüfung über alle Daten des Mandanten, die an sieben Stellen des
|
||||
Testablaufs läuft: Zu jedem gesetzten Gegenkonto gibt es eine Gegensicht, und keine
|
||||
Buchung trägt ein Gegenkonto, das durch `gesplittet = 1` verdeckt wäre – das ist die
|
||||
einzige Konstellation, in der eine Umbuchung unsichtbar würde, weil dann weder der Zweig
|
||||
für Buchungen (`gesplittet = 0`) noch der für Splitzeilen greift. Gegenprobe: Vier
|
||||
absichtliche Eingriffe ins Modell (beide Gegensicht-Zweige des Kontoblatts abgeklemmt,
|
||||
Gegenkonto hinter einer Splitbuchung stehen gelassen, Löschen wirkungslos gemacht)
|
||||
lassen den Test zuverlässig fehlschlagen.
|
||||
|
||||
## Buchungsmaske nennt das Konto und passt auf einen Bildschirm (17.08.2026)
|
||||
|
||||
*Neue Buchung* und *Buchung bearbeiten* zeigen oben, in welchem Konto die Buchung liegt
|
||||
|
|
|
|||
461
tests/30_UmbuchungSpiegelungTest.php
Normal file
461
tests/30_UmbuchungSpiegelungTest.php
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Finanzmanager\Database;
|
||||
use Finanzmanager\Mandant;
|
||||
use Finanzmanager\Models\Buchung;
|
||||
use Finanzmanager\Models\Kategorie;
|
||||
use Finanzmanager\Models\Konto;
|
||||
|
||||
/**
|
||||
* Zwei Zusagen des Datenmodells, festgenagelt:
|
||||
*
|
||||
* 1. Was ein Gegenkonto trägt, steht auch im Kontoblatt dieses Gegenkontos –
|
||||
* egal, auf welchem Weg das Gegenkonto dorthin kam.
|
||||
* 2. Wer die Buchung löscht, löscht beide Sichten. Eine Gegenseite ohne
|
||||
* Ursprungsbuchung kann es nicht geben.
|
||||
*
|
||||
* Beides folgt daraus, dass eine Umbuchung EIN Datensatz mit `gegenkonto_id`
|
||||
* ist und die Gegenseite in `Buchung::kontoblatt()` abgeleitet wird. Diese
|
||||
* Tests halten die Ableitung dicht: Die einzige Lücke, durch die eine
|
||||
* Umbuchung unsichtbar würde, wäre eine Buchung, die zugleich `gesplittet`
|
||||
* ist und oben ein Gegenkonto trägt – dann griffe weder der Zweig für
|
||||
* Buchungen (`gesplittet = 0`) noch der für Splitzeilen.
|
||||
*/
|
||||
class UmbuchungSpiegelungTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 30: Umbuchungen spiegeln sich zuverlässig';
|
||||
|
||||
private int $giro = 0;
|
||||
private int $tagesgeld = 0;
|
||||
private int $bargeld = 0;
|
||||
private int $kategorie = 0;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->mandantAnlegen();
|
||||
$this->stammdaten();
|
||||
|
||||
$this->jederWegErzeugtDieGegensicht();
|
||||
$this->gegenkontoWechseltUndVerschwindet();
|
||||
$this->splitbuchungVerstecktKeineUmbuchung();
|
||||
$this->filterVerstecktDieGegensichtNicht();
|
||||
$this->loeschenNimmtDieGegenseiteMit();
|
||||
$this->gegenseiteIstNichtEinzelnLoeschbar();
|
||||
}
|
||||
|
||||
private function stammdaten(): void
|
||||
{
|
||||
$this->giro = Konto::anlegen([
|
||||
'name' => 'Spiegelgiro', 'kontoart' => 'bank', 'anfangssaldo' => 2000, 'aktiv' => 1,
|
||||
]);
|
||||
$this->tagesgeld = Konto::anlegen([
|
||||
'name' => 'Spiegeltagesgeld', 'kontoart' => 'sparkonto', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||||
]);
|
||||
$this->bargeld = Konto::anlegen([
|
||||
'name' => 'Spiegelkasse', 'kontoart' => 'bar', 'anfangssaldo' => 0, 'aktiv' => 1,
|
||||
]);
|
||||
$this->kategorie = Kategorie::anlegen(['name' => 'Spiegelausgabe', 'art' => 'ausgabe']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Gegenkonto kann auf vier Wegen an eine Buchung kommen. Jeder davon
|
||||
* muss im Kontoblatt des Gegenkontos ankommen – sonst gäbe es Umbuchungen,
|
||||
* die nur auf einer Seite sichtbar sind.
|
||||
*/
|
||||
private function jederWegErzeugtDieGegensicht(): void
|
||||
{
|
||||
// Weg 1: gleich beim Anlegen.
|
||||
$beimAnlegen = Buchung::anlegen([
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-01',
|
||||
'betrag' => -100.00,
|
||||
'empfaenger' => 'Übertrag beim Anlegen',
|
||||
'gegenkonto_id' => $this->tagesgeld,
|
||||
]);
|
||||
$this->assert(
|
||||
$this->gegensichtVorhanden($this->tagesgeld, $beimAnlegen),
|
||||
'Beim Anlegen gesetztes Gegenkonto erscheint im Gegenkonto'
|
||||
);
|
||||
|
||||
// Weg 2: nachträglich über die Schnellzuordnung im Kontoblatt.
|
||||
$perZuordnung = Buchung::anlegen([
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-02',
|
||||
'betrag' => -200.00,
|
||||
'empfaenger' => 'Übertrag per Zuordnung',
|
||||
]);
|
||||
$this->assert(
|
||||
!$this->gegensichtVorhanden($this->tagesgeld, $perZuordnung),
|
||||
'Ohne Gegenkonto steht die Buchung nur auf ihrem eigenen Konto'
|
||||
);
|
||||
Buchung::zuordnen($perZuordnung, null, $this->tagesgeld);
|
||||
$this->assert(
|
||||
$this->gegensichtVorhanden($this->tagesgeld, $perZuordnung),
|
||||
'Nachträglich zugeordnetes Gegenkonto erscheint im Gegenkonto'
|
||||
);
|
||||
|
||||
// Weg 3: über die Buchungsmaske.
|
||||
$perSpeichern = Buchung::anlegen([
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-03',
|
||||
'betrag' => -300.00,
|
||||
'empfaenger' => 'Übertrag per Maske',
|
||||
'kategorie_id' => $this->kategorie,
|
||||
]);
|
||||
Buchung::speichern($perSpeichern, [
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-03',
|
||||
'betrag' => -300.00,
|
||||
'empfaenger' => 'Übertrag per Maske',
|
||||
'gegenkonto_id' => $this->tagesgeld,
|
||||
]);
|
||||
$this->assert(
|
||||
$this->gegensichtVorhanden($this->tagesgeld, $perSpeichern),
|
||||
'Über die Maske gesetztes Gegenkonto erscheint im Gegenkonto'
|
||||
);
|
||||
|
||||
// Weg 4: als einzelne Splitzeile über den Splitdialog.
|
||||
$perSplit = Buchung::anlegen([
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-04',
|
||||
'betrag' => -400.00,
|
||||
'empfaenger' => 'Teils Einkauf, teils Rücklage',
|
||||
'kategorie_id' => $this->kategorie,
|
||||
]);
|
||||
Buchung::splitsErsetzen($perSplit, [
|
||||
['kategorie_id' => $this->kategorie, 'betrag' => -150.00, 'notiz' => 'Einkauf'],
|
||||
['gegenkonto_id' => $this->tagesgeld, 'betrag' => -250.00, 'notiz' => 'Rücklage'],
|
||||
]);
|
||||
$splitZeile = Buchung::splits($perSplit)[1];
|
||||
$this->assert(
|
||||
$this->gegensichtVorhanden($this->tagesgeld, $perSplit, (int)$splitZeile['id']),
|
||||
'Gegenkonto einer Splitzeile erscheint im Gegenkonto'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
4,
|
||||
count($this->gegensichten($this->tagesgeld)),
|
||||
'Alle vier Wege stehen im Kontoblatt des Gegenkontos'
|
||||
);
|
||||
$this->assertEquals(
|
||||
850.0,
|
||||
Konto::saldo($this->tagesgeld),
|
||||
'Und zwar mit umgekehrtem Vorzeichen: 100 + 200 + 300 + 250'
|
||||
);
|
||||
|
||||
$this->pruefeInvariante('nach allen vier Wegen');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein gewechseltes Gegenkonto darf nicht auf dem alten hängenbleiben, ein
|
||||
* entferntes nirgends mehr auftauchen.
|
||||
*/
|
||||
private function gegenkontoWechseltUndVerschwindet(): void
|
||||
{
|
||||
$id = Buchung::anlegen([
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-10',
|
||||
'betrag' => -50.00,
|
||||
'empfaenger' => 'Wanderndes Gegenkonto',
|
||||
'gegenkonto_id' => $this->tagesgeld,
|
||||
]);
|
||||
|
||||
Buchung::zuordnen($id, null, $this->bargeld);
|
||||
$this->assert(
|
||||
!$this->gegensichtVorhanden($this->tagesgeld, $id),
|
||||
'Nach dem Wechsel steht die Gegensicht nicht mehr beim alten Gegenkonto'
|
||||
);
|
||||
$this->assert(
|
||||
$this->gegensichtVorhanden($this->bargeld, $id),
|
||||
'Sondern beim neuen'
|
||||
);
|
||||
$this->assertEquals(50.0, Konto::saldo($this->bargeld), 'Der Saldo wandert mit');
|
||||
|
||||
// Umbuchung zurücknehmen: aus der Umbuchung wird eine Ausgabe.
|
||||
Buchung::zuordnen($id, $this->kategorie, null);
|
||||
$this->assert(
|
||||
!$this->gegensichtVorhanden($this->bargeld, $id),
|
||||
'Wird das Gegenkonto entfernt, verschwindet die Gegensicht'
|
||||
);
|
||||
$this->assertEquals(0.0, Konto::saldo($this->bargeld), 'Und der Saldo des Gegenkontos mit ihr');
|
||||
|
||||
$this->pruefeInvariante('nach Wechsel und Rücknahme des Gegenkontos');
|
||||
|
||||
Buchung::loeschen($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die einzige Konstellation, in der eine Umbuchung unsichtbar würde: oben
|
||||
* ein Gegenkonto, gleichzeitig Splitzeilen. Kein Schreibweg darf sie
|
||||
* herstellen – bei einer Splitbuchung steckt die Zuordnung in den Zeilen.
|
||||
*/
|
||||
private function splitbuchungVerstecktKeineUmbuchung(): void
|
||||
{
|
||||
// Beim Anlegen: Splitzeilen schlagen das Gegenkonto oben aus.
|
||||
$id = Buchung::anlegen(
|
||||
[
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-20',
|
||||
'betrag' => -80.00,
|
||||
'empfaenger' => 'Split trotz Gegenkonto',
|
||||
'gegenkonto_id' => $this->tagesgeld,
|
||||
],
|
||||
[
|
||||
['kategorie_id' => $this->kategorie, 'betrag' => -80.00, 'notiz' => 'alles eine Zeile'],
|
||||
]
|
||||
);
|
||||
$buchung = Buchung::find($id);
|
||||
$this->assertEquals(1, (int)$buchung['gesplittet'], 'Die Buchung ist gesplittet');
|
||||
$this->assertNull($buchung['gegenkonto_id'], 'Beim Anlegen fällt das Gegenkonto oben weg');
|
||||
$this->pruefeInvariante('nach dem Anlegen einer Splitbuchung mit Gegenkonto');
|
||||
|
||||
// Beim Speichern über die Maske ebenso.
|
||||
Buchung::speichern(
|
||||
$id,
|
||||
[
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-20',
|
||||
'betrag' => -80.00,
|
||||
'empfaenger' => 'Split trotz Gegenkonto',
|
||||
'gegenkonto_id' => $this->tagesgeld,
|
||||
],
|
||||
[
|
||||
['kategorie_id' => $this->kategorie, 'betrag' => -80.00, 'notiz' => 'alles eine Zeile'],
|
||||
]
|
||||
);
|
||||
$this->assertNull(Buchung::find($id)['gegenkonto_id'], 'Beim Speichern fällt es ebenfalls weg');
|
||||
$this->pruefeInvariante('nach dem Speichern einer Splitbuchung mit Gegenkonto');
|
||||
|
||||
// Umgekehrt: Wer eine Splitbuchung zur Umbuchung macht, verliert die Zeilen.
|
||||
Buchung::zuordnen($id, null, $this->tagesgeld);
|
||||
$nachher = Buchung::find($id);
|
||||
$this->assertEquals(0, (int)$nachher['gesplittet'], 'Die Zuordnung zur Umbuchung hebt den Split auf');
|
||||
$this->assertEquals(0, count(Buchung::splits($id)), 'Die Splitzeilen sind fort');
|
||||
$this->assert(
|
||||
$this->gegensichtVorhanden($this->tagesgeld, $id),
|
||||
'Und die Umbuchung steht im Gegenkonto'
|
||||
);
|
||||
|
||||
// Und wer aus der Umbuchung eine Splitbuchung macht, verliert das Gegenkonto.
|
||||
Buchung::splitsErsetzen($id, [
|
||||
['kategorie_id' => $this->kategorie, 'betrag' => -80.00, 'notiz' => 'wieder gesplittet'],
|
||||
]);
|
||||
$this->assertNull(Buchung::find($id)['gegenkonto_id'], 'Der Splitdialog räumt das Gegenkonto ab');
|
||||
$this->assert(
|
||||
!$this->gegensichtVorhanden($this->tagesgeld, $id),
|
||||
'Damit ist die Gegensicht fort – sie hätte sonst kein Gegenüber'
|
||||
);
|
||||
|
||||
$this->pruefeInvariante('nach dem Wechsel zwischen Split und Umbuchung');
|
||||
|
||||
Buchung::loeschen($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein gesetzter Filter schränkt das Kontoblatt ein – aber er darf die
|
||||
* Gegenseite nicht anders behandeln als die eigenen Buchungen.
|
||||
*/
|
||||
private function filterVerstecktDieGegensichtNicht(): void
|
||||
{
|
||||
$id = Buchung::anlegen([
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-15',
|
||||
'betrag' => -33.00,
|
||||
'empfaenger' => 'Findbarer Übertrag',
|
||||
'gegenkonto_id' => $this->tagesgeld,
|
||||
]);
|
||||
|
||||
$gefunden = Buchung::kontoblatt($this->tagesgeld, ['suche' => 'Findbarer']);
|
||||
$this->assertEquals(1, count($gefunden), 'Die Volltextsuche findet die Gegensicht');
|
||||
$this->assert($gefunden[0]['gegensicht'], 'Und kennzeichnet sie als solche');
|
||||
$this->assertEquals(33.0, (float)$gefunden[0]['betrag'], 'Mit umgekehrtem Vorzeichen');
|
||||
|
||||
$imZeitraum = Buchung::kontoblatt($this->tagesgeld, ['von' => '2026-07-15', 'bis' => '2026-07-15']);
|
||||
$this->assertEquals(1, count($imZeitraum), 'Der Zeitraumfilter greift auf dem Buchungsdatum');
|
||||
|
||||
Buchung::loeschen($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zusage 2: Die Gegenseite überlebt ihre Buchung nicht – weder im
|
||||
* Kontoblatt noch im Saldo, weder als ganze Buchung noch als Splitzeile.
|
||||
*/
|
||||
private function loeschenNimmtDieGegenseiteMit(): void
|
||||
{
|
||||
$vorherGiro = Konto::saldo($this->giro);
|
||||
$vorherSpar = Konto::saldo($this->tagesgeld);
|
||||
$vorherBlatt = count(Buchung::kontoblatt($this->tagesgeld));
|
||||
|
||||
$umbuchung = Buchung::anlegen([
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-25',
|
||||
'betrag' => -600.00,
|
||||
'empfaenger' => 'Zu löschende Umbuchung',
|
||||
'gegenkonto_id' => $this->tagesgeld,
|
||||
]);
|
||||
$mitSplit = Buchung::anlegen(
|
||||
['konto_id' => $this->giro, 'datum' => '2026-07-26', 'betrag' => -700.00],
|
||||
[
|
||||
['kategorie_id' => $this->kategorie, 'betrag' => -300.00, 'notiz' => 'Einkauf'],
|
||||
['gegenkonto_id' => $this->tagesgeld, 'betrag' => -400.00, 'notiz' => 'Rücklage'],
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
$vorherBlatt + 2,
|
||||
count(Buchung::kontoblatt($this->tagesgeld)),
|
||||
'Beide Gegenseiten stehen im Gegenkonto'
|
||||
);
|
||||
|
||||
Buchung::loeschen($umbuchung);
|
||||
$this->assert(
|
||||
!$this->gegensichtVorhanden($this->tagesgeld, $umbuchung),
|
||||
'Mit der Buchung verschwindet ihre Gegenseite aus dem Gegenkonto'
|
||||
);
|
||||
|
||||
Buchung::loeschen($mitSplit);
|
||||
$this->assert(
|
||||
!$this->gegensichtVorhanden($this->tagesgeld, $mitSplit),
|
||||
'Auch die Gegenseite einer Splitzeile verschwindet mit der Buchung'
|
||||
);
|
||||
$this->assertEquals(
|
||||
0,
|
||||
(int)Database::fetchValue('SELECT COUNT(*) FROM buchung_splits WHERE buchung_id = ?', [$mitSplit]),
|
||||
'Die Splitzeile selbst bleibt nicht als Waise zurück'
|
||||
);
|
||||
|
||||
$this->assertEquals($vorherGiro, Konto::saldo($this->giro), 'Der Saldo des Ausgangskontos ist zurück');
|
||||
$this->assertEquals($vorherSpar, Konto::saldo($this->tagesgeld), 'Der des Gegenkontos ebenso');
|
||||
$this->assertEquals(
|
||||
$vorherBlatt,
|
||||
count(Buchung::kontoblatt($this->tagesgeld)),
|
||||
'Und das Kontoblatt des Gegenkontos ist wieder so lang wie vorher'
|
||||
);
|
||||
|
||||
$this->pruefeInvariante('nach dem Löschen');
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Gegenseite ist keine eigene Buchung: Sie trägt die Nummer der
|
||||
* Ursprungsbuchung. Ein Löschen „auf dem Gegenkonto“ trifft deshalb
|
||||
* zwangsläufig das Original – eine halbe Umbuchung kann nicht entstehen.
|
||||
* Das Kontoblatt bietet auf solchen Zeilen entsprechend keine Auswahl an
|
||||
* (`gegensicht` steuert die Kästchen).
|
||||
*/
|
||||
private function gegenseiteIstNichtEinzelnLoeschbar(): void
|
||||
{
|
||||
$id = Buchung::anlegen([
|
||||
'konto_id' => $this->giro,
|
||||
'datum' => '2026-07-28',
|
||||
'betrag' => -120.00,
|
||||
'empfaenger' => 'Vom Gegenkonto aus gelöscht',
|
||||
'gegenkonto_id' => $this->tagesgeld,
|
||||
]);
|
||||
|
||||
$gegenzeilen = array_values(array_filter(
|
||||
$this->gegensichten($this->tagesgeld),
|
||||
static fn (array $zeile): bool => (string)$zeile['empfaenger'] === 'Vom Gegenkonto aus gelöscht'
|
||||
));
|
||||
$this->assertEquals(1, count($gegenzeilen), 'Auf dem Gegenkonto steht genau eine Gegensicht dazu');
|
||||
$this->assertEquals(
|
||||
$id,
|
||||
(int)$gegenzeilen[0]['id'],
|
||||
'Sie trägt die Nummer der Ursprungsbuchung, keine eigene'
|
||||
);
|
||||
|
||||
// Genau diese Nummer bekäme ein Löschbefehl vom Gegenkonto aus in die Hand.
|
||||
Buchung::loeschen((int)$gegenzeilen[0]['id']);
|
||||
|
||||
$this->assertNull(Buchung::find($id), 'Damit ist die Ursprungsbuchung fort');
|
||||
$this->assert(
|
||||
!$this->gegensichtVorhanden($this->tagesgeld, $id),
|
||||
'Und auf dem Gegenkonto bleibt keine verwaiste Gegenseite stehen'
|
||||
);
|
||||
|
||||
$this->pruefeInvariante('am Ende');
|
||||
}
|
||||
|
||||
// ── Hilfen ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Die Kernzusage als Invariante über alle Daten dieses Mandanten: Zu jedem
|
||||
* gesetzten Gegenkonto – ob an der Buchung oder an einer Splitzeile – gibt
|
||||
* es im Kontoblatt jenes Kontos eine Gegensicht. Und keine Buchung trägt
|
||||
* ein Gegenkonto, das durch `gesplittet = 1` verdeckt würde.
|
||||
*/
|
||||
private function pruefeInvariante(string $kontext): void
|
||||
{
|
||||
$verdeckt = (int)Database::fetchValue(
|
||||
'SELECT COUNT(*) FROM buchungen
|
||||
WHERE benutzer_id = ? AND gegenkonto_id IS NOT NULL AND gesplittet = 1',
|
||||
[Mandant::id()]
|
||||
);
|
||||
$this->assertEquals(0, $verdeckt, "Kein Gegenkonto hinter einer Splitbuchung verdeckt ({$kontext})");
|
||||
|
||||
$umbuchungen = Database::fetchAll(
|
||||
'SELECT id, gegenkonto_id FROM buchungen
|
||||
WHERE benutzer_id = ? AND gegenkonto_id IS NOT NULL AND gesplittet = 0',
|
||||
[Mandant::id()]
|
||||
);
|
||||
$splitzeilen = Database::fetchAll(
|
||||
'SELECT s.id, s.gegenkonto_id, s.buchung_id FROM buchung_splits s
|
||||
JOIN buchungen b ON b.id = s.buchung_id
|
||||
WHERE b.benutzer_id = ? AND s.gegenkonto_id IS NOT NULL',
|
||||
[Mandant::id()]
|
||||
);
|
||||
|
||||
$fehlend = 0;
|
||||
foreach ($umbuchungen as $zeile) {
|
||||
if (!$this->gegensichtVorhanden((int)$zeile['gegenkonto_id'], (int)$zeile['id'])) {
|
||||
$fehlend++;
|
||||
}
|
||||
}
|
||||
foreach ($splitzeilen as $zeile) {
|
||||
$vorhanden = $this->gegensichtVorhanden(
|
||||
(int)$zeile['gegenkonto_id'],
|
||||
(int)$zeile['buchung_id'],
|
||||
(int)$zeile['id']
|
||||
);
|
||||
if (!$vorhanden) {
|
||||
$fehlend++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertEquals(
|
||||
0,
|
||||
$fehlend,
|
||||
"Jede Umbuchung steht im Kontoblatt ihres Gegenkontos ({$kontext})"
|
||||
);
|
||||
$this->assertGreaterThan(
|
||||
0,
|
||||
count($umbuchungen) + count($splitzeilen),
|
||||
"Es gab überhaupt Umbuchungen zu prüfen ({$kontext})"
|
||||
);
|
||||
}
|
||||
|
||||
/** Steht die Buchung (oder eine ihrer Splitzeilen) als Gegensicht im Kontoblatt? */
|
||||
private function gegensichtVorhanden(int $kontoId, int $buchungId, ?int $splitId = null): bool
|
||||
{
|
||||
foreach ($this->gegensichten($kontoId) as $zeile) {
|
||||
$passt = (int)$zeile['id'] === $buchungId
|
||||
&& ($splitId === null
|
||||
? $zeile['split_id'] === null
|
||||
: (int)$zeile['split_id'] === $splitId);
|
||||
if ($passt) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @return list<array<string,mixed>> */
|
||||
private function gegensichten(int $kontoId): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
Buchung::kontoblatt($kontoId),
|
||||
static fn (array $zeile): bool => (bool)$zeile['gegensicht']
|
||||
));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue