Damit ist der geplante Funktionsumfang beisammen. Phase 4 abgeschlossen - Konfigurierbare Cockpit-Kacheln; abgewählte werden nicht berechnet - Einnahmen/Ausgaben je Monat als Balkenpaare mit Saldolinie, aufgesetzt auf den Summenbericht statt auf eine eigene Abfrage - Meine Ausgaben als Ring auf der obersten Kategorieebene - Offene Vorgänge aus allen Modulen Phase 5 – Online-Banking - nemiah/php-fints als Fork auf Codeberg, psr/log direkt eingebaut: die Anwendung hängt an keinem fremden Update-Zyklus - PIN AES-256-GCM-verschlüsselt; Schlüssel aus APP_KEY oder aus data/schluessel/app_key, den der Entrypoint einmalig erzeugt - TAN-Dialog über mehrere Requests (FinTs::persist plus serialisierte Aktion in der Sitzung) – ein wartender Prozess wäre mit php-fpm der falsche Weg - Abgleich statt Direktbuchung: verbinden, übernehmen oder verwerfen - Regeln mit vier Vergleichsarten, Betragsgrenzen und Split-Schema Phase 6 – Wertpapiere - Depotbuchung und Geldbuchung als zwei Seiten eines Geschäfts; betrag ist der Bruttowert, Nebenkosten stehen daneben - Einstand nach gleitendem Durchschnitt, realisierte Gewinne, Erträge - Kursabruf über Leeway oder FMP, täglicher Lauf per Cron - QIF-Import für !Type:Invst Dabei behoben: Der Import buchte den Geldfluss jedes Wertpapiergeschäfts als Abgang – auch bei Verkäufen und Dividenden; und er erkannte das Verrechnungskonto nicht, wenn dem Konto im L-Feld eine Kategorie vorangeht (bei Verkäufen also immer). Phase 7 – Nebenmodule und Zahlungsverkehr - Buchungsvorlagen inklusive QIF-Import, Splits werden mitskaliert - Regelmäßige Buchungen mit Fälligkeitslauf, Finanzkalender, Erinnerungen - Adressen mit IBAN-Prüfung, Verträge mit Kündigungsfrist, Sparziele - Kredite mit gerechnetem Tilgungsplan - Überweisungen über FinTS; die Buchung entsteht erst nach Annahme durch die Bank. Die pain.001-Nachricht erzeugt SepaXml selbst Nebenbei: .dockerignore ergänzt (ohne sie brach der Build an data/mariadb ab), PlatzhalterController entfernt, zwei Inline-Handler beseitigt, die die Content-Security-Policy ohnehin blockierte. 888 PHP-Prüfungen und 45 E2E-Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
2.7 KiB
PHP
89 lines
2.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Finanzmanager\Models;
|
||
|
||
use Finanzmanager\Database;
|
||
use Finanzmanager\Mandant;
|
||
|
||
/**
|
||
* Erinnerungen: alles, was zu einem Datum ansteht, aber keine Buchung ist –
|
||
* „Versicherung kündigen“, „Steuererklärung abgeben“.
|
||
*
|
||
* Sie erscheinen im Finanzkalender neben den fälligen Buchungen.
|
||
*/
|
||
class Erinnerung
|
||
{
|
||
/** @return list<array<string,mixed>> */
|
||
public static function alle(bool $nurOffene = false): array
|
||
{
|
||
$sql = 'SELECT * FROM erinnerungen WHERE benutzer_id = ?';
|
||
if ($nurOffene) {
|
||
$sql .= ' AND erledigt = 0';
|
||
}
|
||
|
||
return Database::fetchAll($sql . ' ORDER BY erledigt, faellig_am, id', [Mandant::id()]);
|
||
}
|
||
|
||
/** @return list<array<string,mixed>> */
|
||
public static function faellige(?string $bis = null): array
|
||
{
|
||
return Database::fetchAll(
|
||
'SELECT * FROM erinnerungen
|
||
WHERE benutzer_id = ? AND erledigt = 0 AND faellig_am <= ?
|
||
ORDER BY faellig_am, id',
|
||
[Mandant::id(), $bis ?? date('Y-m-d')]
|
||
);
|
||
}
|
||
|
||
public static function find(int $id): ?array
|
||
{
|
||
$zeile = Database::fetchOne(
|
||
'SELECT * FROM erinnerungen WHERE id = ? AND benutzer_id = ?',
|
||
[$id, Mandant::id()]
|
||
);
|
||
return $zeile === false ? null : $zeile;
|
||
}
|
||
|
||
public static function anlegen(string $text, string $faelligAm, string $notiz = ''): int
|
||
{
|
||
$text = trim($text);
|
||
if ($text === '') {
|
||
throw new \InvalidArgumentException('Die Erinnerung braucht einen Text.');
|
||
}
|
||
if (!Buchung::istDatum($faelligAm)) {
|
||
throw new \InvalidArgumentException('Das Datum muss als JJJJ-MM-TT vorliegen.');
|
||
}
|
||
|
||
Database::execute(
|
||
'INSERT INTO erinnerungen (benutzer_id, text, faellig_am, notiz) VALUES (?, ?, ?, ?)',
|
||
[Mandant::id(), mb_substr($text, 0, 255), $faelligAm, trim($notiz)]
|
||
);
|
||
|
||
return Database::lastInsertId();
|
||
}
|
||
|
||
public static function erledigen(int $id, bool $erledigt = true): void
|
||
{
|
||
Database::execute(
|
||
'UPDATE erinnerungen SET erledigt = ?, erledigt_am = ?
|
||
WHERE id = ? AND benutzer_id = ?',
|
||
[$erledigt ? 1 : 0, $erledigt ? date('Y-m-d H:i:s') : null, $id, Mandant::id()]
|
||
);
|
||
}
|
||
|
||
public static function loeschen(int $id): void
|
||
{
|
||
Database::execute('DELETE FROM erinnerungen WHERE id = ? AND benutzer_id = ?', [$id, Mandant::id()]);
|
||
}
|
||
|
||
public static function anzahlOffen(): int
|
||
{
|
||
return (int)Database::fetchValue(
|
||
'SELECT COUNT(*) FROM erinnerungen WHERE benutzer_id = ? AND erledigt = 0',
|
||
[Mandant::id()],
|
||
0
|
||
);
|
||
}
|
||
}
|