Merge pull request 'Fix n.n.: Buchungen im Kontoblatt lassen sich manuell verknüpfen' (#82) from fds2610-bot/Finanzmanager:main into main
Reviewed-on: https://codeberg.org/fds2610/Finanzmanager/pulls/82
This commit is contained in:
commit
09fd8af15f
7 changed files with 368 additions and 9 deletions
27
Changes.md
27
Changes.md
|
|
@ -1,5 +1,32 @@
|
|||
# Änderungen
|
||||
|
||||
## Umbuchung wird auch von der Gegenkontoseite erkannt (27.09.2026)
|
||||
|
||||
Eine Umbuchung gehört ihrem Ursprungskonto; der automatische Abgleich suchte eine
|
||||
passende Buchung bisher nur über deren `konto_id`. Kam der Kontoauszug der
|
||||
Empfängerseite mit ein paar Tagen Verzug herein, fand er die schon gebuchte Umbuchung
|
||||
nie und legte sie als eigenständige Buchung an – die Umbuchung stand danach doppelt im
|
||||
Kontoblatt und verfälschte den Saldo.
|
||||
|
||||
- `Bankumsatz::passendeBuchung()` berücksichtigt jetzt auch die Gegenkonto-Seite einer
|
||||
Umbuchung, mit umgekehrtem Vorzeichen wie im Kontoblatt auch.
|
||||
- Test in `tests/16_BankingTest.php`
|
||||
(`umbuchungWirdAuchVonDerGegenkontoseiteErkannt`).
|
||||
|
||||
## Buchungen im Kontoblatt verknüpfen (27.09.2026)
|
||||
|
||||
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.
|
||||
|
||||
- Neu: `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 auch die Gegenseite einer fremden Umbuchung ankreuzen;
|
||||
der Knopf „Verknüpfen“ neben „Auswahl löschen“ nutzt dieselbe Auswahl.
|
||||
- Test in `tests/44_BuchungenVerknuepfenTest.php`.
|
||||
|
||||
## Buchungsbericht: Splitbuchung nach Konto/Empfänger zusammengefasst (27.09.2026)
|
||||
|
||||
Nach Kategorie oder Klasse gruppiert erscheint eine Splitbuchung weiterhin mit jeder
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ $router->get('/konten/{id}/buchung', fn(array $p) => (new BuchungenController()
|
|||
$router->post('/konten/{id}/buchung', fn(array $p) => (new BuchungenController())->anlegen($p));
|
||||
$router->post('/konten/{id}/buchungen/loeschen',
|
||||
fn(array $p) => (new BuchungenController())->auswahlLoeschen($p));
|
||||
$router->post('/konten/{id}/buchungen/verknuepfen',
|
||||
fn(array $p) => (new BuchungenController())->verknuepfen($p));
|
||||
$router->get('/buchungen/{id}', fn(array $p) => (new BuchungenController())->bearbeiten($p));
|
||||
$router->post('/buchungen/{id}', fn(array $p) => (new BuchungenController())->speichern($p));
|
||||
$router->post('/buchungen/{id}/loeschen', fn(array $p) => (new BuchungenController())->loeschen($p));
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Formulare mit data-bestaetigen fragen vor dem Absenden nach.
|
||||
// Formulare mit data-bestaetigen fragen vor dem Absenden nach. Trägt der
|
||||
// gedrückte Knopf selbst ein data-bestaetigen, gilt seine Frage – so
|
||||
// können mehrere Knöpfe im selben Formular (verschiedene Ziele über
|
||||
// formaction) je ihre eigene Rückfrage stellen.
|
||||
// In der Capture-Phase, damit die Rückfrage auch dann greift, wenn ein
|
||||
// anderer Handler das Ereignis unterwegs stoppt.
|
||||
document.addEventListener('submit', function (ereignis) {
|
||||
|
|
@ -38,7 +41,8 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const frage = formular.dataset.bestaetigen;
|
||||
const frage = (ereignis.submitter && ereignis.submitter.dataset.bestaetigen)
|
||||
|| formular.dataset.bestaetigen;
|
||||
if (frage && !window.confirm(frage)) {
|
||||
ereignis.preventDefault();
|
||||
ereignis.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -500,6 +500,41 @@ class BuchungenController extends BaseController
|
|||
: $anzahl . ' Buchungen gelöscht.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt zwei angehakte Buchungen dieses Kontoblatts zusammen: eine schon
|
||||
* fertige Umbuchung und eine einzeln erfasste Buchung, die denselben
|
||||
* Vorgang doppelt zeigen, weil sie zu weit auseinanderlagen, um sich
|
||||
* automatisch zu finden.
|
||||
*
|
||||
* @param array<string,string> $params
|
||||
*/
|
||||
public function verknuepfen(array $params): void
|
||||
{
|
||||
$this->verlangeAnmeldung();
|
||||
|
||||
$kontoId = (int)$params['id'];
|
||||
if (Konto::find($kontoId) === null) {
|
||||
$this->nichtGefunden('/konten/' . $params['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$ziel = $this->kontoblattPfad($kontoId);
|
||||
$ids = $this->postIds('buchungen');
|
||||
if (count($ids) !== 2) {
|
||||
View::redirect($ziel, 'Zum Verknüpfen bitte genau zwei Buchungen ankreuzen.', 'warnung');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Buchung::verknuepfen($kontoId, $ids[0], $ids[1]);
|
||||
} catch (\Throwable $e) {
|
||||
View::redirect($ziel, $e->getMessage(), 'fehler');
|
||||
return;
|
||||
}
|
||||
|
||||
View::redirect($ziel, 'Buchungen verknüpft.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Weg zurück ins Kontoblatt – mit demselben Filter und derselben Seite.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -266,6 +266,66 @@ class Buchung
|
|||
Database::execute('DELETE FROM buchungen WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt zwei Buchungen zusammen, die auf diesem Konto dieselbe Umbuchung
|
||||
* doppelt zeigen: einmal als schon fertige Umbuchung (eigene Seite oder
|
||||
* Gegenseite), einmal als einzeln erfasste Buchung mit demselben Betrag –
|
||||
* etwa weil die Bank sie erst Tage später gebucht hat und der automatische
|
||||
* Abgleich sie deshalb nicht zusammenführte.
|
||||
*
|
||||
* Die Umbuchung bleibt bestehen, die einzelne Buchung fällt weg; ihr
|
||||
* Bankumsatz hängt sich stattdessen an die Umbuchung, wie es ein
|
||||
* automatischer Abgleich auch täte.
|
||||
*/
|
||||
public static function verknuepfen(int $kontoId, int $idA, int $idB): void
|
||||
{
|
||||
$a = self::find($idA);
|
||||
$b = self::find($idB);
|
||||
if ($a === null || $b === null) {
|
||||
throw new \RuntimeException('Eine der beiden Buchungen gibt es nicht.');
|
||||
}
|
||||
|
||||
$istUmbuchungHier = static fn(array $z): bool => $z['gegenkonto_id'] !== null
|
||||
&& (int)$z['gesplittet'] === 0
|
||||
&& ((int)$z['konto_id'] === $kontoId || (int)$z['gegenkonto_id'] === $kontoId);
|
||||
|
||||
$istEinzelnHier = static fn(array $z): bool => $z['gegenkonto_id'] === null
|
||||
&& (int)$z['gesplittet'] === 0
|
||||
&& (int)$z['konto_id'] === $kontoId;
|
||||
|
||||
[$umbuchung, $einzeln] = match (true) {
|
||||
$istUmbuchungHier($a) && $istEinzelnHier($b) => [$a, $b],
|
||||
$istUmbuchungHier($b) && $istEinzelnHier($a) => [$b, $a],
|
||||
default => throw new \RuntimeException(
|
||||
'Verknüpfen geht nur zwischen einer Umbuchung und einer einzeln erfassten '
|
||||
. 'Buchung auf diesem Konto.'
|
||||
),
|
||||
};
|
||||
|
||||
// Auf der Gegenkontoseite zählt der Betrag mit umgekehrtem Vorzeichen,
|
||||
// wie im Kontoblatt auch – ein grober Kontrollblick, kein Ersatz für die
|
||||
// eigene Prüfung der Auswahl.
|
||||
$vorzeichen = (int)$umbuchung['konto_id'] === $kontoId ? 1 : -1;
|
||||
$effektiv = $vorzeichen * (float)$umbuchung['betrag'];
|
||||
if (abs($effektiv - (float)$einzeln['betrag']) >= self::CENT) {
|
||||
throw new \RuntimeException(
|
||||
'Die Beträge der beiden Buchungen passen nicht zusammen – bitte die Auswahl prüfen.'
|
||||
);
|
||||
}
|
||||
|
||||
Database::transaktion(static function () use ($umbuchung, $einzeln): void {
|
||||
Database::execute(
|
||||
'UPDATE bank_umsaetze SET buchung_id = ? WHERE buchung_id = ? AND benutzer_id = ?',
|
||||
[(int)$umbuchung['id'], (int)$einzeln['id'], Mandant::id()]
|
||||
);
|
||||
Database::execute(
|
||||
'UPDATE buchungen SET abgeglichen = 1 WHERE id = ? AND benutzer_id = ?',
|
||||
[(int)$umbuchung['id'], Mandant::id()]
|
||||
);
|
||||
self::loeschen((int)$einzeln['id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Kontoblatt: eigene Buchungen und die Gegenseiten fremder Umbuchungen,
|
||||
* chronologisch mit laufendem Saldo.
|
||||
|
|
|
|||
|
|
@ -47,8 +47,9 @@ $seitenPfad = static function (int $nr) use ($filter): string {
|
|||
// Nach dem Löschen soll dieselbe Ansicht wieder erscheinen – Filter und Seite
|
||||
// hängen deshalb am Ziel des Löschformulars. Dasselbe gilt für die
|
||||
// Schnellzuordnung: Ihre Ziele setzt kontoblatt.js aus dieser Abfrage zusammen.
|
||||
$abfrage = $seitenPfad($seite);
|
||||
$loeschZiel = '/konten/' . (int)$konto['id'] . '/buchungen/loeschen' . $abfrage;
|
||||
$abfrage = $seitenPfad($seite);
|
||||
$loeschZiel = '/konten/' . (int)$konto['id'] . '/buchungen/loeschen' . $abfrage;
|
||||
$verknuepfenZiel = '/konten/' . (int)$konto['id'] . '/buchungen/verknuepfen' . $abfrage;
|
||||
|
||||
/**
|
||||
* Die Zuordnung einer Zeile als Wert der gemeinsamen Auswahlliste – dieselben
|
||||
|
|
@ -135,7 +136,19 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
data-frage-eine="Die ausgewählte Buchung wirklich löschen?"
|
||||
data-frage-mehrere="{anzahl} ausgewählte Buchungen wirklich löschen?">
|
||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken ?? '') ?>">
|
||||
<?php /* Der erste Knopf im Formular bleibt „Auswahl löschen“ –
|
||||
kontoblatt.js/app.js greift sich per querySelector den
|
||||
ersten Absende-Knopf, um ihn nach Anzahl der Auswahl zu
|
||||
beschriften und zu sperren. Verknüpfen nutzt dasselbe
|
||||
Formular und Ankreuzfeld, nur mit anderem Ziel: zwei
|
||||
ausgewählte Buchungen, die denselben Vorgang doppelt
|
||||
zeigen (eine Umbuchung und ihre einzeln erfasste
|
||||
Dublette), führt es zusammen. */ ?>
|
||||
<button type="submit" class="knopf gefahr">Auswahl löschen</button>
|
||||
<button type="submit" formaction="<?= View::h($verknuepfenZiel) ?>" class="knopf"
|
||||
data-bestaetigen="Beide ausgewählten Buchungen zu einer Umbuchung verknüpfen? Die einzeln erfasste Buchung wird dabei gelöscht.">
|
||||
Verknüpfen
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<span class="fuellung"></span>
|
||||
|
|
@ -222,11 +235,19 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
<?php foreach ($zeilen as $zeile): ?>
|
||||
<tr class="<?= $zeile['gegensicht'] ? 'gegensicht' : '' ?>">
|
||||
<td class="wahlspalte">
|
||||
<?php if (!$zeile['gegensicht']): ?>
|
||||
<input type="checkbox" name="buchungen[]" value="<?= (int)$zeile['id'] ?>"
|
||||
form="buchungsauswahl"
|
||||
aria-label="Buchung vom <?= View::h(View::datum($zeile['datum'])) ?> auswählen">
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// Auch die Gegenseite einer fremden Umbuchung lässt sich
|
||||
// ankreuzen – gelöscht wird sie dadurch nicht (das prüft
|
||||
// der Controller anhand ihres wirklichen Kontos), aber
|
||||
// verknüpfen lässt sie sich mit einer einzeln erfassten
|
||||
// Dublette auf diesem Konto.
|
||||
$auswahlLabel = $zeile['gegensicht']
|
||||
? 'Umbuchung vom ' . View::datum($zeile['datum']) . ' auswählen'
|
||||
: 'Buchung vom ' . View::datum($zeile['datum']) . ' auswählen';
|
||||
?>
|
||||
<input type="checkbox" name="buchungen[]" value="<?= (int)$zeile['id'] ?>"
|
||||
form="buchungsauswahl"
|
||||
aria-label="<?= View::h($auswahlLabel) ?>">
|
||||
</td>
|
||||
<td class="nowrap"><?= View::datum($zeile['datum']) ?></td>
|
||||
<td class="nebentext"><?= View::h($zeile['nummer']) ?></td>
|
||||
|
|
|
|||
210
tests/44_BuchungenVerknuepfenTest.php
Normal file
210
tests/44_BuchungenVerknuepfenTest.php
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue