Merge pull request 'Fix #219: Bank buchen standardmäßig älteste Umsätze zuerst' (#220) from fds2610-bot/Steuerkram:main into main

Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/220
This commit is contained in:
fds2610 2026-10-12 17:27:52 +02:00
commit 6a0634eacf
6 changed files with 96 additions and 3 deletions

View file

@ -2,6 +2,11 @@
--- ---
## 2026-10-12 – Bank buchen: älteste Umsätze zuerst (#219)
- **`src/Models/BankTransaction.php`** (`all()`) **und `src/Views/bank/index.php`:** Die Standard-Sortierrichtung ist jetzt aufsteigend statt absteigend – ohne `dir`-Parameter erscheinen die ältesten Umsätze oben, damit datumsgetreu gebucht wird und die Buchungsnummern zeitlich fortlaufend entstehen. Im Filter ist „↑ Aufsteigend“ vorbelegt; „↓ Absteigend“ bleibt über `dir=desc` wählbar, unbekannte Werte fallen auf aufsteigend zurück.
- **`tests/104_BankSortierungAeltesteZuerstTest.php`** (neu) und **`tests/e2e/specs/bank-sortierung.spec.js`** (neu): prüfen die Standardreihenfolge, `dir=desc` und die Vorbelegung im Filter.
## 2026-10-06 – Header-Injection über Dateinamen im Content-Disposition-Header behoben (#217) ## 2026-10-06 – Header-Injection über Dateinamen im Content-Disposition-Header behoben (#217)
- **`src/View.php`:** neue Methode `View::contentDispositionHeader(string $disposition, string $filename): string`. `PATHINFO_BASENAME` entfernt beim Beleg-Upload nur Verzeichnisanteile, nicht aber Anführungszeichen oder Backslashes im client-seitig gewählten Dateinamen – ein präparierter Name wie `legit"; filename*=UTF-8''%2e%2e%2fevil.exe` brach beim Download aus dem `filename="..."`-Parameter aus und schleuste einen zusätzlichen, von vielen Browsern bevorzugt ausgewerteten `filename*=`-Parameter ein. Damit ließ sich der im Download-Dialog angezeigte Dateiname/die Endung gegenüber anderen Nutzern fälschen (Dateityp-Spoofing). Die neue Methode maskiert Anführungszeichen, Backslashes und Steuerzeichen im ASCII-`filename`-Parameter und setzt zusätzlich RFC-6266-konform `filename*=UTF-8''...` mit dem unveränderten, `rawurlencode()`-kodierten Namen. - **`src/View.php`:** neue Methode `View::contentDispositionHeader(string $disposition, string $filename): string`. `PATHINFO_BASENAME` entfernt beim Beleg-Upload nur Verzeichnisanteile, nicht aber Anführungszeichen oder Backslashes im client-seitig gewählten Dateinamen – ein präparierter Name wie `legit"; filename*=UTF-8''%2e%2e%2fevil.exe` brach beim Download aus dem `filename="..."`-Parameter aus und schleuste einen zusätzlichen, von vielen Browsern bevorzugt ausgewerteten `filename*=`-Parameter ein. Damit ließ sich der im Download-Dialog angezeigte Dateiname/die Endung gegenüber anderen Nutzern fälschen (Dateityp-Spoofing). Die neue Methode maskiert Anführungszeichen, Backslashes und Steuerzeichen im ASCII-`filename`-Parameter und setzt zusätzlich RFC-6266-konform `filename*=UTF-8''...` mit dem unveränderten, `rawurlencode()`-kodierten Namen.

View file

@ -106,6 +106,7 @@ Anlagenspiegel für abnutzbare Wirtschaftsgüter – siehe [anlagevermögen.md](
- **Beleg-Zuordnung**: jede Banktransaktion kann einem oder mehreren Belegen zugeordnet werden; der zugeordnete Betrag wird separat erfasst - **Beleg-Zuordnung**: jede Banktransaktion kann einem oder mehreren Belegen zugeordnet werden; der zugeordnete Betrag wird separat erfasst
- **Teilzuordnung**: eine Transaktion kann schrittweise mehreren Belegen anteilig zugeordnet werden - **Teilzuordnung**: eine Transaktion kann schrittweise mehreren Belegen anteilig zugeordnet werden
- **Status-Logik**: `offen` → keine Zuordnung, `teilweise` → Restsaldo > 0,01 €, `gebucht` → vollständig zugeordnet - **Status-Logik**: `offen` → keine Zuordnung, `teilweise` → Restsaldo > 0,01 €, `gebucht` → vollständig zugeordnet
- **Sortierung**: Die Liste unter `/bank` zeigt standardmäßig die ältesten Umsätze zuerst (Wertstellung aufsteigend), damit datumsgetreu gebucht wird und die Buchungsnummern zeitlich fortlaufend vergeben werden; Sortierfeld und Richtung (*Absteigend*) sind im Filter umstellbar
- Direkter Beleg-Upload aus der Transaktionsansicht heraus (erzeugt neuen Beleg und verknüpft ihn) - Direkter Beleg-Upload aus der Transaktionsansicht heraus (erzeugt neuen Beleg und verknüpft ihn)
- **Verschlüsselung**: Der `APP_KEY` in `.env` ist Pflicht für Direktverbindungen – im Backup mitsichern, Verlust = Verlust aller gespeicherten Credentials - **Verschlüsselung**: Der `APP_KEY` in `.env` ist Pflicht für Direktverbindungen – im Backup mitsichern, Verlust = Verlust aller gespeicherten Credentials

View file

@ -55,7 +55,7 @@ class BankTransaction
'status' => 'bt.status', 'status' => 'bt.status',
]; ];
$sortCol = $sortMap[$filter['sort'] ?? ''] ?? 'bt.wertstellung'; $sortCol = $sortMap[$filter['sort'] ?? ''] ?? 'bt.wertstellung';
$sortDir = ($filter['dir'] ?? 'desc') === 'asc' ? 'ASC' : 'DESC'; $sortDir = ($filter['dir'] ?? 'asc') === 'desc' ? 'DESC' : 'ASC';
$whereStr = implode(' AND ', $where); $whereStr = implode(' AND ', $where);
$offset = ($page - 1) * $perPage; $offset = ($page - 1) * $perPage;

View file

@ -86,8 +86,8 @@ $months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','D
</select> </select>
<select name="dir" style="width:110px"> <select name="dir" style="width:110px">
<option value="desc"<?= ($filter['dir'] ?? 'desc') === 'desc' ? ' selected' : '' ?>>↓ Absteigend</option> <option value="desc"<?= ($filter['dir'] ?? 'asc') === 'desc' ? ' selected' : '' ?>>↓ Absteigend</option>
<option value="asc"<?= ($filter['dir'] ?? 'desc') === 'asc' ? ' selected' : '' ?>>↑ Aufsteigend</option> <option value="asc"<?= ($filter['dir'] ?? 'asc') === 'asc' ? ' selected' : '' ?>>↑ Aufsteigend</option>
</select> </select>
<button type="submit" class="btn btn-secondary btn-sm">Filtern</button> <button type="submit" class="btn btn-secondary btn-sm">Filtern</button>

View file

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
use Steuerkram\Database;
use Steuerkram\Models\BankTransaction;
/**
* Test 104: „Bank buchen“ sortiert standardmäßig älteste zuerst (Issue #219)
*
* Ohne ausdrückliche Sortierrichtung liefert BankTransaction::all() die
* Umsätze aufsteigend nach Wertstellung, damit datumsgetreu gebucht wird.
* Mit dir=desc bleibt die absteigende Sortierung weiterhin wählbar.
*/
class BankSortierungAeltesteZuerstTest extends TestCase
{
protected string $testName = 'Test 104: Bank buchen sortiert standardmäßig älteste zuerst';
public function run(): void
{
Database::execute(
'INSERT INTO bank_import_batches (source_id, filename, row_count) VALUES (NULL, ?, 3)',
['test-104.csv']
);
$batchId = (int)Database::lastInsertId();
$marker = 'T104-' . bin2hex(random_bytes(4));
foreach (['2026-03-15', '2026-01-05', '2026-02-10'] as $datum) {
Database::execute(
'INSERT INTO bank_transactions (batch_id, buchungstag, wertstellung, buchungstext, wert)
VALUES (?, ?, ?, ?, -10.00)',
[$batchId, $datum, $datum, "$marker $datum"]
);
}
$daten = static fn(array $result): array => array_column($result['rows'], 'wertstellung');
$standard = BankTransaction::all(['search' => $marker]);
$this->assertEquals(
['2026-01-05', '2026-02-10', '2026-03-15'],
$daten($standard),
'Ohne dir-Parameter: älteste Wertstellung zuerst'
);
$absteigend = BankTransaction::all(['search' => $marker, 'dir' => 'desc']);
$this->assertEquals(
['2026-03-15', '2026-02-10', '2026-01-05'],
$daten($absteigend),
'dir=desc: neueste Wertstellung zuerst'
);
$ungueltig = BankTransaction::all(['search' => $marker, 'dir' => 'quatsch']);
$this->assertEquals(
['2026-01-05', '2026-02-10', '2026-03-15'],
$daten($ungueltig),
'Ungültiger dir-Wert fällt auf aufsteigend zurück'
);
$view = (string)file_get_contents(dirname(__DIR__) . '/src/Views/bank/index.php');
$this->assert(
str_contains($view, "(\$filter['dir'] ?? 'asc') === 'asc'"),
'Filterauswahl zeigt „Aufsteigend“ als Vorbelegung'
);
$this->assert(
!str_contains($view, "(\$filter['dir'] ?? 'desc')"),
'Filterauswahl belegt nicht mehr „Absteigend“ vor'
);
}
}

View file

@ -0,0 +1,18 @@
// @ts-check
// „Bank buchen“ sortiert standardmäßig älteste zuerst (Issue #219): Ohne
// Parameter ist im Filter „Aufsteigend“ vorbelegt, „Absteigend“ bleibt wählbar.
const { test, expect } = require('@playwright/test')
const { gotoOk } = require('../helpers/app')
test.describe('Bank buchen – Sortierrichtung', () => {
test('ohne Parameter ist „Aufsteigend“ vorbelegt', async ({ page }) => {
await gotoOk(page, '/bank')
await expect(page.locator('select[name="dir"]')).toHaveValue('asc')
})
test('dir=desc bleibt ausgewählt', async ({ page }) => {
await gotoOk(page, '/bank?dir=desc')
await expect(page.locator('select[name="dir"]')).toHaveValue('desc')
})
})