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 $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; } }