Finanzmanager/tests/51_DepotBankbestandTest.php
fds2610-bot 9a3472f4b7 Fix n.n.: Depot-Zuordnung, Bestand einbuchen mit Stichtag
- Depot der Bank wird einem Depot im Finanzmanager zugeordnet (Migration 022)
- Bestand einbuchen: Ein-/Auslieferungen aus dem Bankbestand, Stichtag per Dialog
- Zuordnungsliste: Depots nur noch als Wertpapiergeschäft

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 15:10:43 +02:00

229 lines
9.6 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 Fhp\Model\StatementOfHoldings\Holding;
use Finanzmanager\Models\Bankprotokoll;
use Finanzmanager\Models\Bankzugang;
use Finanzmanager\Models\Depotbuchung;
use Finanzmanager\Models\Konto;
use Finanzmanager\Models\Wertpapier;
use Finanzmanager\Models\Zahlungsauftrag;
use Finanzmanager\Services\Depotabgleich;
/**
* Depotbestand laut Bank (FinTS-Depotaufstellung) und Nachweis zur Überweisung.
*
* Geprüft wird, was nach dem Abruf in der Datenbank steht: der Bestand des
* letzten Abrufs, der Kurs bekannter Papiere, der Vergleich mit dem eigenen
* Bestand – und beim Nachweis die Prüfsumme des Protokolls.
*/
class DepotBankbestandTest extends TestCase
{
protected string $testName = 'Test 51: Depotbestand laut Bank, Nachweis';
private int $depot = 0;
public function run(): void
{
$this->mandantAnlegen();
$this->depot = Konto::anlegen([
'name' => 'Bankdepot', 'kontoart' => 'depot', 'kontonummer' => '502022353',
'anfangssaldo' => 0, 'aktiv' => 1,
]);
$this->bestandUndKurs();
$this->vergleichZeigtAbweichungen();
$this->einbuchen();
$this->einbuchenMitStichtag();
$this->zweiterAbrufErsetztDenErsten();
$this->depotZuordnung();
$this->nachweisMitPruefsumme();
}
private function position(string $isin, string $name, float $stueck, float $kurs): Holding
{
return (new Holding())
->setISIN($isin)
->setName($name)
->setAmount($stueck)
->setPrice($kurs)
->setValue($stueck * $kurs)
->setCurrency('EUR')
->setDate(new \DateTime('2026-09-30'));
}
private function bestandUndKurs(): void
{
$bekannt = Wertpapier::findenOderAnlegen([
'name' => 'Bekannte AG', 'isin' => 'DE0001111111', 'art' => 'aktie',
]);
$ergebnis = Depotabgleich::uebernehmen($this->depot, [
$this->position('DE0001111111', 'Bekannte AG', 10.0, 12.5),
$this->position('DE0002222222', 'Unbekannte AG', 4.0, 100.0),
]);
$this->assertEquals(2, $ergebnis['positionen'], 'Beide Positionen stehen im Bankbestand');
$this->assertEquals(1, $ergebnis['kurse'], 'Nur das bekannte Papier bekommt einen Kurs');
$this->assertEquals(
12.5,
(float)Wertpapier::letzterKurs($bekannt)['kurs'],
'Der Bewertungskurs der Bank ist der Kurs des Papiers'
);
$this->assertNull(
Wertpapier::finde('DE0002222222'),
'Das unbekannte Papier wird nicht angelegt'
);
$this->assertNotEmpty(Depotabgleich::abgerufenAm($this->depot), 'Der Abrufzeitpunkt ist vermerkt');
}
private function einbuchen(): void
{
$e = Depotabgleich::einbuchen($this->depot);
$this->assertEquals(2, $e['eingeliefert'], 'Zwei Positionen mit Mehrbestand eingeliefert');
$this->assertEquals(1, $e['ausgeliefert'], 'Das nur im Finanzmanager geführte Papier ausgeliefert');
$this->assertEquals(1, $e['angelegt'], 'Das unbekannte Papier wurde angelegt');
$this->assertNotEmpty(Wertpapier::finde('DE0002222222'), 'Neues Wertpapier existiert');
foreach (Depotabgleich::vergleich($this->depot) as $zeile) {
$this->assert(abs((float)$zeile['abweichung']) < 0.000001, 'Nach dem Einbuchen stimmt ' . $zeile['name']);
}
$this->assertEquals(
['eingeliefert' => 0, 'ausgeliefert' => 0, 'angelegt' => 0],
Depotabgleich::einbuchen($this->depot),
'Ein zweites Einbuchen ändert nichts'
);
}
private function einbuchenMitStichtag(): void
{
$depot = Konto::anlegen([
'name' => 'Stichtagsdepot', 'kontoart' => 'depot', 'kontonummer' => '502022999',
'anfangssaldo' => 0, 'aktiv' => 1,
]);
Depotabgleich::uebernehmen($depot, [$this->position('DE0004444444', 'Stichtag AG', 5.0, 20.0)]);
$e = Depotabgleich::einbuchen($depot, '2025-12-31');
$this->assertEquals(1, $e['eingeliefert'], 'Der Mehrbestand wird eingeliefert');
$buchungen = Depotbuchung::liste(['konto_id' => $depot]);
$this->assertEquals(1, count($buchungen), 'Eine Depotbuchung entstanden');
$this->assertEquals('2025-12-31', substr((string)$buchungen[0]['datum'], 0, 10), 'Die Einlieferung trägt den Stichtag');
$this->assert(str_contains((string)$buchungen[0]['notiz'], '30.09.2026'), 'Die Notiz nennt den Bankstand');
}
private function vergleichZeigtAbweichungen(): void
{
$bekannt = Wertpapier::finde('DE0001111111');
Depotbuchung::anlegen([
'konto_id' => $this->depot, 'wertpapier_id' => (int)$bekannt['id'],
'vorgang' => 'kauf', 'datum' => '2026-01-05', 'stueck' => 8, 'kurs' => 10, 'betrag' => 80,
]);
$verkauftes = Wertpapier::findenOderAnlegen([
'name' => 'Alte AG', 'isin' => 'DE0003333333', 'art' => 'aktie',
]);
Depotbuchung::anlegen([
'konto_id' => $this->depot, 'wertpapier_id' => $verkauftes,
'vorgang' => 'kauf', 'datum' => '2026-01-05', 'stueck' => 3, 'kurs' => 10, 'betrag' => 30,
]);
$je = [];
foreach (Depotabgleich::vergleich($this->depot) as $zeile) {
$je[$zeile['isin']] = $zeile;
}
$this->assertEquals(2.0, $je['DE0001111111']['abweichung'], 'Die Bank hat 2 Stück mehr als gebucht');
$this->assertEquals(4.0, $je['DE0002222222']['abweichung'], 'Nur bei der Bank: ganze Position abweichend');
$this->assertNull($je['DE0002222222']['eigen'], 'Für das unbekannte Papier gibt es keinen eigenen Bestand');
$this->assertEquals(-3.0, $je['DE0003333333']['abweichung'], 'Nur im Finanzmanager: Bank führt es nicht');
$this->assertNull($je['DE0003333333']['bank'], 'Bei der Bank fehlt das Papier');
}
private function zweiterAbrufErsetztDenErsten(): void
{
Depotabgleich::uebernehmen($this->depot, [
$this->position('DE0001111111', 'Bekannte AG', 8.0, 13.0),
]);
$bank = array_filter(
Depotabgleich::vergleich($this->depot),
static fn(array $zeile): bool => $zeile['bank'] !== null
);
$this->assertEquals(1, count($bank), 'Der neue Abruf ersetzt den alten Bankbestand');
}
private function depotZuordnung(): void
{
$zugang = Bankzugang::anlegen([
'name' => 'Depotbank', 'blz' => '12030000', 'fints_url' => 'https://example.org/fints',
'benutzerkennung' => 'kennung', 'pin' => '1234',
]);
$giro = Bankzugang::findeOderLegeKontoAn($zugang, 'DE02120300000000202051', '202051', 'Giro');
$bankdepot = Bankzugang::findeOderLegeDepotAn($zugang, '0502022353', 'Depot');
$this->assertEquals('depot', $bankdepot['art'], 'Das Depot der Bank ist als Depot vermerkt');
$this->assertEquals(
$this->depot,
(int)$bankdepot['konto_id'],
'Passt die Kontonummer (auch mit führender Null), wird gleich zugeordnet'
);
$this->assertEquals(
(int)$bankdepot['id'],
(int)Bankzugang::findeOderLegeDepotAn($zugang, '0502022353')['id'],
'Ein zweites Erkennen legt nichts doppelt an'
);
$this->assertEquals(
[(int)$giro['id']],
array_map(static fn(array $z): int => (int)$z['id'], Bankzugang::konten($zugang)),
'Der Umsatzabruf sieht das Depot nicht'
);
$this->assertEquals(1, count(Bankzugang::depots($zugang)), 'Das Depot steht in der Depotliste');
$unbekannt = Bankzugang::findeOderLegeDepotAn($zugang, '999');
$this->assertNull($unbekannt['konto_id'], 'Ohne passende Kontonummer bleibt das Depot unzugeordnet');
Bankzugang::ordneKontoZu((int)$unbekannt['id'], $this->depot, true);
$this->assertEquals(
$this->depot,
(int)Bankzugang::depots($zugang)[1]['konto_id'],
'Die Zuordnung lässt sich von Hand treffen'
);
}
private function nachweisMitPruefsumme(): void
{
$verrechnung = Konto::anlegen([
'name' => 'Nachweisgiro', 'kontoart' => 'bank', 'anfangssaldo' => 100, 'aktiv' => 1,
]);
$auftragId = Zahlungsauftrag::anlegen([
'konto_id' => $verrechnung, 'empfaenger' => 'Beweis GmbH',
'iban' => 'DE02120300000000202051', 'betrag' => 12.34,
'verwendungszweck' => 'Rechnung 42',
]);
$dialog = "> HNHBK:1:3+000000000100'\n< HIRMG:2:2+0010::Nachricht entgegengenommen'";
$id = Bankprotokoll::speichern(null, 'ueberweisung', 'erfolg', 'übergeben', $dialog, $auftragId);
$this->assertNotEmpty($id, 'Das Protokoll wird gespeichert');
$auftrag = Zahlungsauftrag::find($auftragId);
$nachweis = Bankprotokoll::nachweis($auftrag);
$this->assert(str_contains($nachweis, 'Beweis GmbH'), 'Der Nachweis nennt den Empfänger');
$this->assert(str_contains($nachweis, $dialog), 'Der Nachweis enthält den vollständigen Dialog');
$this->assert(str_contains($nachweis, hash('sha256', $dialog)), 'Der Nachweis nennt die Prüfsumme');
$this->assert(str_contains($nachweis, 'unverändert seit dem Speichern'), 'Unveränderter Text wird bestätigt');
\Finanzmanager\Database::execute(
'UPDATE bank_protokolle SET protokoll = ? WHERE id = ?',
[$dialog . "\nnachträglich", $id]
);
$this->assert(
str_contains(Bankprotokoll::nachweis($auftrag), 'ABWEICHUNG'),
'Ein nachträglich geänderter Text fällt im Nachweis auf'
);
}
}