Finanzmanager/tests/44_BuchungenVerknuepfenTest.php
fds2610-bot e02bac22c0 Fix n.n.: Buchungen im Kontoblatt lassen sich manuell verknüpfen
Liegen zwei Buchungen, die eigentlich derselbe Vorgang sind, zu weit
auseinander oder gehörte eine ursprünglich einem anderen Ursprungskonto,
findet der automatische Abgleich sie nicht – die Umbuchung und ihre
einzeln erfasste Dublette stehen dann dauerhaft nebeneinander im
Kontoblatt.

Buchung::verknuepfen() führt eine Umbuchung (eigene Seite oder
Gegenseite auf diesem Konto) und eine einzeln erfasste Buchung mit
passendem Betrag zusammen: Die Umbuchung bleibt, die Dublette fällt
weg, ihr Bankumsatz hängt sich an die Umbuchung. Im Kontoblatt lässt
sich dafür jetzt auch die Gegenseite einer fremden Umbuchung ankreuzen;
ein neuer Knopf „Verknüpfen“ nutzt dieselbe Auswahl wie „Auswahl
löschen“.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 14:53:56 +02:00

210 lines
8.1 KiB
PHP
Raw 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\Models\Bankumsatz;
use Finanzmanager\Models\Bankzugang;
use Finanzmanager\Models\Buchung;
use Finanzmanager\Models\Konto;
/**
* Manuelles Verknüpfen zweier Buchungen im Kontoblatt: eine Umbuchung und eine
* einzeln erfasste Buchung, die denselben Vorgang doppelt zeigen, weil sie zu
* weit auseinanderlagen, um sich beim automatischen Abgleich zu finden (siehe
* auch Test 16, „Umbuchung wird auch von der Gegenkontoseite erkannt“ – dort
* geht es um den automatischen Fall, hier um den, den nur ein Mensch auflösen
* kann).
*/
class BuchungenVerknuepfenTest extends TestCase
{
protected string $testName = 'Test 44: Buchungen im Kontoblatt verknüpfen';
private int $paypal = 0;
private int $dkb = 0;
private int $zugang = 0;
public function run(): void
{
$this->mandantAnlegen();
$this->stammdaten();
$this->gegenseiteUndDublette();
$this->eigeneUmbuchungUndDublette();
$this->reihenfolgeDerAuswahlSpieltKeineRolle();
$this->zweiEinzelneLassenSichNichtVerknuepfen();
$this->zweiUmbuchungenLassenSichNichtVerknuepfen();
$this->unpassenderBetragWirdAbgewiesen();
}
private function stammdaten(): void
{
$this->paypal = Konto::anlegen([
'name' => 'Paypal', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
]);
$this->dkb = Konto::anlegen([
'name' => 'DKB Giro', 'kontoart' => 'bank', 'anfangssaldo' => 0, 'aktiv' => 1,
]);
$this->zugang = Bankzugang::anlegen([
'name' => 'Testbank', 'bankname' => 'Testbank AG', 'blz' => '12030000',
'fints_url' => 'https://banking.example.org/fints', 'benutzerkennung' => 'testkunde',
'pin' => 'geheim1234', 'abrufzeitraum' => 'letzter_abruf', 'aktiv' => 1,
]);
}
/** @param array<string,mixed> $daten */
private function umsatzAnlegen(array $daten): int
{
return (int)Bankumsatz::anlegen(array_merge([
'zugang_id' => $this->zugang,
'extern_id' => 'verknuepfen-' . bin2hex(random_bytes(4)),
'wertstellung' => $daten['buchungstag'],
'empfaenger' => '',
'verwendungszweck' => '',
'buchungsart' => 'GUTSCHRIFT',
], $daten));
}
/**
* Der Fall aus der Praxis: Die Umbuchung wurde von Paypal aus gebucht (ihr
* Ursprungskonto), im DKB-Kontoblatt steht sie nur als Gegenseite. Die
* echte Bankbuchung kommt einen Tag später herein und wird – weil der
* automatische Abgleich sie nicht fand – als eigene Buchung übernommen.
*/
private function gegenseiteUndDublette(): void
{
$umbuchungId = Buchung::anlegen([
'konto_id' => $this->paypal, 'datum' => '2026-09-23', 'betrag' => 26.00,
'empfaenger' => 'Umbuchung', 'gegenkonto_id' => $this->dkb,
]);
$dubletteId = Buchung::anlegen([
'konto_id' => $this->dkb, 'datum' => '2026-09-24', 'betrag' => -26.00,
'empfaenger' => 'PayPal Europe',
]);
$umsatzId = $this->umsatzAnlegen([
'konto_id' => $this->dkb, 'buchungstag' => '2026-09-24', 'betrag' => -26.00,
]);
Bankumsatz::verbinden($umsatzId, $dubletteId);
Buchung::verknuepfen($this->dkb, $umbuchungId, $dubletteId);
$this->assertNull(Buchung::find($dubletteId), 'Die einzeln erfasste Dublette ist weg');
$this->assertNotEmpty(Buchung::find($umbuchungId), 'Die Umbuchung bleibt bestehen');
$this->assertEquals(
1,
(int)Buchung::find($umbuchungId)['abgeglichen'],
'Die Umbuchung gilt jetzt als abgeglichen'
);
$umsatz = Bankumsatz::find($umsatzId);
$this->assertEquals($umbuchungId, (int)$umsatz['buchung_id'], 'Ihr Bankumsatz zeigt jetzt auf die Umbuchung');
$this->assertEquals('uebernommen', $umsatz['status'], 'Und gilt weiter als erledigt');
}
/**
* Gespiegelter Fall: Die Umbuchung wurde direkt im DKB-Konto gebucht (ihr
* Ursprungskonto liegt hier), die Dublette ebenfalls hier.
*/
private function eigeneUmbuchungUndDublette(): void
{
$umbuchungId = Buchung::anlegen([
'konto_id' => $this->dkb, 'datum' => '2026-09-23', 'betrag' => -75.20,
'empfaenger' => 'Umbuchung', 'gegenkonto_id' => $this->paypal,
]);
$dubletteId = Buchung::anlegen([
'konto_id' => $this->dkb, 'datum' => '2026-09-26', 'betrag' => -75.20,
'empfaenger' => 'Reservix',
]);
Buchung::verknuepfen($this->dkb, $umbuchungId, $dubletteId);
$this->assertNull(Buchung::find($dubletteId), 'Die Dublette ist weg');
$this->assertEquals(
$this->paypal,
(int)Buchung::find($umbuchungId)['gegenkonto_id'],
'Die Umbuchung bleibt, was sie war'
);
}
private function reihenfolgeDerAuswahlSpieltKeineRolle(): void
{
$umbuchungId = Buchung::anlegen([
'konto_id' => $this->paypal, 'datum' => '2026-09-23', 'betrag' => 12.00,
'empfaenger' => 'Umbuchung', 'gegenkonto_id' => $this->dkb,
]);
$dubletteId = Buchung::anlegen([
'konto_id' => $this->dkb, 'datum' => '2026-09-24', 'betrag' => -12.00,
'empfaenger' => 'Dublette',
]);
// Zuerst die Dublette, dann die Umbuchung ausgewählt.
Buchung::verknuepfen($this->dkb, $dubletteId, $umbuchungId);
$this->assertNull(Buchung::find($dubletteId), 'Auch andersherum verschwindet die Dublette');
$this->assertNotEmpty(Buchung::find($umbuchungId), 'Und die Umbuchung bleibt');
}
private function zweiEinzelneLassenSichNichtVerknuepfen(): void
{
$ersteId = Buchung::anlegen([
'konto_id' => $this->dkb, 'datum' => '2026-09-25', 'betrag' => -10.00, 'empfaenger' => 'Erste',
]);
$zweiteId = Buchung::anlegen([
'konto_id' => $this->dkb, 'datum' => '2026-09-25', 'betrag' => -10.00, 'empfaenger' => 'Zweite',
]);
$this->assert(
$this->wirftFehler(fn() => Buchung::verknuepfen($this->dkb, $ersteId, $zweiteId)),
'Zwei einzeln erfasste Buchungen lassen sich nicht verknüpfen'
);
$this->assertNotEmpty(Buchung::find($ersteId), 'Keine der beiden wurde gelöscht');
$this->assertNotEmpty(Buchung::find($zweiteId), 'Auch die zweite nicht');
}
private function zweiUmbuchungenLassenSichNichtVerknuepfen(): void
{
$ersteId = Buchung::anlegen([
'konto_id' => $this->paypal, 'datum' => '2026-09-25', 'betrag' => 5.00,
'empfaenger' => 'Umbuchung 1', 'gegenkonto_id' => $this->dkb,
]);
$zweiteId = Buchung::anlegen([
'konto_id' => $this->dkb, 'datum' => '2026-09-25', 'betrag' => -7.00,
'empfaenger' => 'Umbuchung 2', 'gegenkonto_id' => $this->paypal,
]);
$this->assert(
$this->wirftFehler(fn() => Buchung::verknuepfen($this->dkb, $ersteId, $zweiteId)),
'Zwei Umbuchungen lassen sich nicht verknüpfen'
);
}
private function unpassenderBetragWirdAbgewiesen(): void
{
$umbuchungId = Buchung::anlegen([
'konto_id' => $this->paypal, 'datum' => '2026-09-23', 'betrag' => 26.00,
'empfaenger' => 'Umbuchung', 'gegenkonto_id' => $this->dkb,
]);
$dubletteId = Buchung::anlegen([
'konto_id' => $this->dkb, 'datum' => '2026-09-24', 'betrag' => -19.99,
'empfaenger' => 'Anderer Betrag',
]);
$this->assert(
$this->wirftFehler(fn() => Buchung::verknuepfen($this->dkb, $umbuchungId, $dubletteId)),
'Unterschiedliche Beträge werden abgewiesen'
);
$this->assertNotEmpty(Buchung::find($dubletteId), 'Die Dublette bleibt unangetastet');
}
/** Hilfsmittel: Wirft der Aufruf eine Ausnahme? */
private function wirftFehler(callable $aufruf): bool
{
try {
$aufruf();
} catch (\Throwable) {
return true;
}
return false;
}
}