Steuerkram/src/Models/BankTransaction.php

270 lines
9 KiB
PHP

<?php
declare(strict_types=1);
namespace Steuerkram\Models;
use Steuerkram\Database;
class BankTransaction
{
public static function all(array $filter = [], int $page = 1, int $perPage = 50): array
{
$where = ['1=1'];
$params = [];
$status = $filter['status'] ?? 'pending';
if ($status === 'pending') {
$where[] = "bt.status IN ('offen','teilweise')";
} elseif ($status !== '') {
$where[] = 'bt.status = ?';
$params[] = $status;
}
if (!empty($filter['year'])) {
$where[] = 'YEAR(bt.wertstellung) = ?';
$params[] = (int)$filter['year'];
}
if (!empty($filter['month'])) {
$where[] = 'MONTH(bt.wertstellung) = ?';
$params[] = (int)$filter['month'];
}
if (!empty($filter['typ'])) {
if ($filter['typ'] === 'einnahme') {
$where[] = 'bt.wert > 0';
} elseif ($filter['typ'] === 'ausgabe') {
$where[] = 'bt.wert < 0';
}
}
if (!empty($filter['search'])) {
$where[] = '(bt.buchungstext LIKE ? OR bt.vorgang LIKE ?)';
$s = '%' . $filter['search'] . '%';
$params[] = $s;
$params[] = $s;
}
$whereStr = implode(' AND ', $where);
$offset = ($page - 1) * $perPage;
$total = (int)(Database::fetchOne(
"SELECT COUNT(*) AS n FROM bank_transactions bt WHERE $whereStr",
$params
)['n'] ?? 0);
$rows = Database::fetchAll(
"SELECT bt.*, bib.filename AS batch_filename, bif.name AS format_name,
btb_agg.beleg_id AS beleg_id, btb_agg.beleg_count,
b.beleg_nr
FROM bank_transactions bt
JOIN bank_import_batches bib ON bt.batch_id = bib.id
JOIN bank_import_formats bif ON bib.format_id = bif.id
LEFT JOIN (
SELECT bank_tx_id, MIN(beleg_id) AS beleg_id, COUNT(*) AS beleg_count
FROM bank_transaction_belege
GROUP BY bank_tx_id
) btb_agg ON btb_agg.bank_tx_id = bt.id
LEFT JOIN belege b ON b.id = btb_agg.beleg_id
WHERE $whereStr
ORDER BY bt.wertstellung DESC, bt.id DESC
LIMIT ? OFFSET ?",
array_merge($params, [$perPage, $offset])
);
return [
'rows' => $rows,
'total' => $total,
'pages' => (int)ceil($total / $perPage),
];
}
public static function find(int $id): ?array
{
$row = Database::fetchOne(
'SELECT bt.*, bib.filename AS batch_filename, bif.name AS format_name
FROM bank_transactions bt
JOIN bank_import_batches bib ON bt.batch_id = bib.id
JOIN bank_import_formats bif ON bib.format_id = bif.id
WHERE bt.id = ?',
[$id]
);
return $row ?: null;
}
public static function updateStatus(int $id, string $status): void
{
Database::execute(
"UPDATE bank_transactions SET status = ? WHERE id = ?",
[$status, $id]
);
}
/** Verknüpft einen Beleg mit zugeordnetem Betrag und aktualisiert den Status. */
public static function linkBeleg(int $txId, int $belegId, float $betrag): void
{
Database::execute(
'INSERT INTO bank_transaction_belege (bank_tx_id, beleg_id, betrag)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE betrag = VALUES(betrag)',
[$txId, $belegId, $betrag]
);
self::recalcStatus($txId);
}
/** Entfernt eine Beleg-Verknüpfung und aktualisiert den Status. */
public static function unlinkBeleg(int $txId, int $belegId): void
{
Database::execute(
'DELETE FROM bank_transaction_belege WHERE bank_tx_id = ? AND beleg_id = ?',
[$txId, $belegId]
);
self::recalcStatus($txId);
}
/** Berechnet Status neu: 0 Links→offen, Restsaldo>0→teilweise, Restsaldo=0→gebucht. */
public static function recalcStatus(int $txId): void
{
$row = Database::fetchOne(
'SELECT ABS(bt.wert) AS brutto, COALESCE(SUM(btb.betrag), 0) AS allocated
FROM bank_transactions bt
LEFT JOIN bank_transaction_belege btb ON btb.bank_tx_id = bt.id
WHERE bt.id = ?
GROUP BY bt.id',
[$txId]
);
if (!$row) return;
$remaining = round((float)$row['brutto'] - (float)$row['allocated'], 2);
$allocated = (float)$row['allocated'];
if ($allocated <= 0) {
$status = 'offen';
} elseif ($remaining > 0.01) {
$status = 'teilweise';
} else {
$status = 'gebucht';
}
Database::execute(
"UPDATE bank_transactions SET status = ? WHERE id = ?",
[$status, $txId]
);
}
/** Gibt alle verknüpften Belege mit ihren Beträgen zurück. */
public static function getLinkedBelege(int $txId): array
{
return Database::fetchAll(
'SELECT btb.betrag, b.id AS beleg_id, b.beleg_nr, b.beleg_datum,
b.belegtyp, b.betrag_brutto, s.name AS supplier_name
FROM bank_transaction_belege btb
JOIN belege b ON b.id = btb.beleg_id
LEFT JOIN suppliers s ON s.id = b.supplier_id
WHERE btb.bank_tx_id = ?
ORDER BY b.beleg_nr',
[$txId]
);
}
/** Gibt den noch nicht zugeordneten Restbetrag zurück. */
public static function getRemainingBetrag(int $txId): float
{
$row = Database::fetchOne(
'SELECT ABS(bt.wert) AS brutto, COALESCE(SUM(btb.betrag), 0) AS allocated
FROM bank_transactions bt
LEFT JOIN bank_transaction_belege btb ON btb.bank_tx_id = bt.id
WHERE bt.id = ?
GROUP BY bt.id',
[$txId]
);
if (!$row) return 0.0;
return round(max(0.0, (float)$row['brutto'] - (float)$row['allocated']), 2);
}
/** Sucht die Bankbuchung, die mit einem bestimmten Beleg verknüpft ist. */
public static function findByBelegId(int $belegId): ?array
{
$row = Database::fetchOne(
'SELECT bt.*, bib.filename AS batch_filename, btb.betrag AS alloc_betrag
FROM bank_transaction_belege btb
JOIN bank_transactions bt ON bt.id = btb.bank_tx_id
JOIN bank_import_batches bib ON bib.id = bt.batch_id
WHERE btb.beleg_id = ?
LIMIT 1',
[$belegId]
);
return $row ?: null;
}
/**
* Teilt $rows in ['new' => [...], 'dup' => [...]] auf.
*/
public static function findDuplicates(array $rows): array
{
if (empty($rows)) {
return ['new' => [], 'dup' => []];
}
$placeholders = implode(',', array_fill(0, count($rows), '(?,?,?)'));
$params = [];
foreach ($rows as $r) {
$params[] = $r['buchungstag'];
$params[] = $r['wertstellung'];
$params[] = (string)$r['wert'];
}
$existing = Database::fetchAll(
"SELECT buchungstag, wertstellung, CAST(wert AS CHAR) AS wert, LEFT(buchungstext,200) AS btext
FROM bank_transactions
WHERE (buchungstag, wertstellung, CAST(wert AS CHAR)) IN ($placeholders)",
$params
);
$lookup = [];
foreach ($existing as $e) {
$lookup[$e['buchungstag'] . '|' . $e['wertstellung'] . '|' . $e['wert'] . '|' . $e['btext']] = true;
}
$new = [];
$dup = [];
foreach ($rows as $row) {
$key = $row['buchungstag'] . '|'
. $row['wertstellung'] . '|'
. (string)$row['wert'] . '|'
. mb_substr($row['buchungstext'], 0, 200);
if (isset($lookup[$key])) {
$dup[] = $row;
} else {
$new[] = $row;
}
}
return ['new' => $new, 'dup' => $dup];
}
public static function createBatch(int $formatId, string $filename, int $rowCount): int
{
Database::execute(
'INSERT INTO bank_import_batches (format_id, filename, row_count) VALUES (?, ?, ?)',
[$formatId, $filename, $rowCount]
);
return (int)Database::lastInsertId();
}
public static function insertRow(int $batchId, array $data): void
{
Database::execute(
'INSERT INTO bank_transactions
(batch_id, buchungstag, wertstellung, vorgang, buchungstext, wert, raw_line)
VALUES (?, ?, ?, ?, ?, ?, ?)',
[
$batchId,
$data['buchungstag'],
$data['wertstellung'],
$data['vorgang'],
$data['buchungstext'],
$data['wert'],
$data['raw_line'] ?? null,
]
);
}
}