- 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>
229 lines
9.6 KiB
PHP
229 lines
9.6 KiB
PHP
<?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'
|
||
);
|
||
}
|
||
}
|