"`$c`", $cols)) . ') VALUES (' . implode(',', array_fill(0, count($cols), '?')) . ')'; Database::execute($sql, array_values($row)); return (int)Database::lastInsertId(); } public static function update(int $id, array $data): void { $row = self::dehydrate($data); if (empty($row)) { return; } $sets = implode(', ', array_map(fn($c) => "`$c` = ?", array_keys($row))); $params = array_values($row); $params[] = $id; Database::execute("UPDATE bank_sources SET $sets WHERE id = ?", $params); } public static function updateField(int $id, string $field, mixed $value): void { if (in_array($field, ['client_id', 'secret', 'pin'], true)) { $field = $field . '_enc'; $value = $value !== null && $value !== '' ? Crypto::encrypt((string)$value) : null; } Database::execute("UPDATE bank_sources SET `$field` = ? WHERE id = ?", [$value, $id]); } public static function delete(int $id): void { Database::execute('DELETE FROM bank_sources WHERE id = ?', [$id]); } public static function updateLastFetched(int $id, ?float $balance = null): void { // Setzt zugleich fetch_range auf den Default zurück: einmalige Sonderauswahl // (z.B. "letzte Woche") gilt nur für genau einen Abruf. // last_balance wird nur überschrieben, wenn der Abruf einen Kontostand geliefert // hat – sonst bleibt der zuletzt bekannte Stand erhalten. if ($balance !== null) { Database::execute( "UPDATE bank_sources SET last_fetched_at = NOW(), fetch_range = 'last_fetched', last_balance = ? WHERE id = ?", [$balance, $id] ); return; } Database::execute( "UPDATE bank_sources SET last_fetched_at = NOW(), fetch_range = 'last_fetched' WHERE id = ?", [$id] ); } /** Erlaubte Werte für die "Abrufen ab"-Auswahl. */ public const FETCH_RANGES = [ 'last_fetched', 'last_week', 'current_month', 'last_30_days', 'full', ]; /** * Berechnet das Start-Datum für einen Direktverbindungs-Abruf anhand der * Auswahl im Bearbeiten-Dialog und des letzten Abruf-Zeitstempels. * * Liefert ein Tupel [\DateTimeImmutable $start, string $label]. * $label ist eine sprechende Beschreibung für das Abruf-Log. */ public static function computeStartDate(?string $range, ?string $lastFetchedAt, ?\DateTimeImmutable $today = null): array { $today = $today ?? new \DateTimeImmutable('today'); $range = in_array($range, self::FETCH_RANGES, true) ? $range : 'last_fetched'; switch ($range) { case 'last_week': return [$today->modify('-7 days'), 'letzte 7 Tage']; case 'current_month': $start = $today->modify('first day of this month'); return [$start, 'aktueller Monat ab ' . $start->format('d.m.Y')]; case 'last_30_days': return [$today->modify('-30 days'), 'letzte 30 Tage']; case 'full': return [$today->modify('-365 days'), 'letzte 365 Tage (ohne Datumsbegrenzung)']; case 'last_fetched': default: if ($lastFetchedAt) { $start = new \DateTimeImmutable( (new \DateTimeImmutable($lastFetchedAt))->format('Y-m-d') ); return [$start, 'ab letztem Abruf am ' . $start->format('d.m.Y') . ' 00:00 Uhr']; } return [$today->modify('-365 days'), 'kein vorheriger Abruf – starte mit den letzten 365 Tagen']; } } /** * Berechnet einen neuen account_filter aus dem bisherigen Filter und einem * POST-Payload des Bearbeiten-Dialogs. * * Rückgabe: * ['update' => true, 'value' => null] – Filter löschen (Reset) * ['update' => true, 'value' => '[...]'] – neuen JSON-Filter setzen * ['update' => false, 'value' => null] – Filter unverändert lassen * * Nur Identifier, die bereits im bisherigen Filter standen, werden übernommen * (kein Unterschieben fremder Konten via POST). */ public static function computeFilterFromPost(?string $currentJson, array $post): array { if (isset($post['reset_account_filter'])) { return ['update' => true, 'value' => null]; } if (!array_key_exists('active_accounts', $post)) { return ['update' => false, 'value' => null]; } $known = []; if ($currentJson !== null && $currentJson !== '') { $decoded = json_decode($currentJson, true); if (is_array($decoded)) { $known = array_values(array_map('strval', $decoded)); } } $postedRaw = is_array($post['active_accounts']) ? $post['active_accounts'] : []; $posted = array_map('strval', $postedRaw); $kept = array_values(array_intersect($known, $posted)); if ($kept === [] && $known !== []) { // Alle abgehakt → wie Reset behandeln (beim nächsten Abruf neu fragen) return ['update' => true, 'value' => null]; } if ($kept !== []) { return ['update' => true, 'value' => json_encode($kept, JSON_UNESCAPED_UNICODE)]; } return ['update' => false, 'value' => null]; } /** Aktualisiert sort_order anhand der übergebenen ID-Reihenfolge (Position 0..N). */ public static function updateOrder(array $orderedIds): void { foreach (array_values($orderedIds) as $pos => $id) { $id = (int)$id; if ($id <= 0) continue; Database::execute( 'UPDATE bank_sources SET sort_order = ? WHERE id = ?', [$pos, $id] ); } } private static function hydrate(array $row): array { $row['name'] = $row['bezeichnung']; $row['client_id'] = Crypto::decrypt((string)($row['client_id_enc'] ?? '')); $row['secret'] = Crypto::decrypt((string)($row['secret_enc'] ?? '')); $row['pin'] = Crypto::decrypt((string)($row['pin_enc'] ?? '')); return $row; } /** * Überführt ein Eingabe-Array in die DB-Spalten: * - client_id/secret/pin → *_enc (verschlüsselt) * - nur erlaubte Spalten werden zurückgegeben */ private static function dehydrate(array $data): array { $allowed = [ 'type', 'provider_id', 'bezeichnung', 'kuerzel', 'farbe', 'skip_rows', 'has_header_row', 'delimiter', 'enclosure', 'col_buchungstag', 'col_wertstellung', 'col_vorgang', 'col_buchungstext', 'col_wert', 'col_zahlungspflichtiger', 'col_zahlungsempfaenger', 'col_verwendungszweck', 'date_format', 'decimal_separator', 'thousands_separator', 'buchungstext_template_debit', 'buchungstext_template_credit', 'sandbox', 'last_fetched_at', 'fetch_range', 'app_name', 'client_id_enc', 'secret_enc', 'blz', 'fints_url', 'benutzerkennung', 'pin_enc', 'account_filter', 'tan_mode_id', ]; $out = []; foreach ($allowed as $col) { if (array_key_exists($col, $data)) { $out[$col] = $data[$col]; } } // Klartext-Felder verschlüsseln foreach (['client_id' => 'client_id_enc', 'secret' => 'secret_enc', 'pin' => 'pin_enc'] as $plain => $enc) { if (array_key_exists($plain, $data)) { $val = $data[$plain]; $out[$enc] = ($val === null || $val === '') ? null : Crypto::encrypt((string)$val); } } return $out; } }