- Migration 071 ergänzt bank_sources.sort_order - Drag-Handle (⠿) oben links jeder Kachel - Gezogene Kachel hängt am Mauszeiger, Ursprung mit lila Rahmen - Blauer Strich im Gap markiert die Einfügeposition - Reihenfolge wird per AJAX nach /bank/direkt/_order persistiert
151 lines
5.5 KiB
PHP
151 lines
5.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Steuerkram\Models;
|
||
|
||
use Steuerkram\Database;
|
||
use Steuerkram\Services\Crypto;
|
||
|
||
/**
|
||
* Einheitliche Tabelle für alle Bank-Importquellen:
|
||
* - type='csv' – CSV-Importformat (Spaltenmapping, Trennzeichen, Templates)
|
||
* - type='api' – Direktverbindung über REST/NVP API (PayPal)
|
||
* - type='fints' – Direktverbindung über FinTS (Comdirect, DKB, …)
|
||
*
|
||
* Beim Lesen werden die verschlüsselten Spalten (client_id_enc, secret_enc, pin_enc)
|
||
* automatisch zu Klartext-Schlüsseln (client_id, secret, pin) entschlüsselt.
|
||
* Außerdem wird "name" als Alias für "bezeichnung" gesetzt, damit der bestehende
|
||
* BankImportParser-Code ($format['name'] etc.) ohne Anpassung funktioniert.
|
||
*/
|
||
class BankSource
|
||
{
|
||
public static function all(?string $type = null): array
|
||
{
|
||
$sql = 'SELECT bs.*, bp.name AS provider_name, bp.verbindungstyp, bp.default_fints_url
|
||
FROM bank_sources bs
|
||
LEFT JOIN bank_providers bp ON bp.id = bs.provider_id';
|
||
$params = [];
|
||
if ($type !== null) {
|
||
$sql .= ' WHERE bs.type = ?';
|
||
$params[] = $type;
|
||
}
|
||
$sql .= ' ORDER BY bs.sort_order, bs.type, bs.bezeichnung';
|
||
$rows = Database::fetchAll($sql, $params);
|
||
return array_map([self::class, 'hydrate'], $rows);
|
||
}
|
||
|
||
public static function find(int $id): ?array
|
||
{
|
||
$row = Database::fetchOne(
|
||
'SELECT bs.*, bp.name AS provider_name, bp.verbindungstyp, bp.default_fints_url
|
||
FROM bank_sources bs
|
||
LEFT JOIN bank_providers bp ON bp.id = bs.provider_id
|
||
WHERE bs.id = ?',
|
||
[$id]
|
||
);
|
||
return $row ? self::hydrate($row) : null;
|
||
}
|
||
|
||
public static function create(array $data): int
|
||
{
|
||
$type = $data['type'] ?? 'csv';
|
||
$row = self::dehydrate($data);
|
||
|
||
$cols = array_keys($row);
|
||
$sql = 'INSERT INTO bank_sources (' . implode(',', array_map(fn($c) => "`$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): void
|
||
{
|
||
Database::execute('UPDATE bank_sources SET last_fetched_at = NOW() WHERE id = ?', [$id]);
|
||
}
|
||
|
||
/** 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',
|
||
'app_name', 'client_id_enc', 'secret_enc',
|
||
'blz', 'fints_url', 'benutzerkennung', 'pin_enc', 'account_filter',
|
||
];
|
||
|
||
$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;
|
||
}
|
||
}
|