docs: Plan-Haekchen nach Umsetzung aller 17 Tasks gesetzt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
140
Plan.md
140
Plan.md
@@ -34,7 +34,7 @@
|
|||||||
**Interfaces:**
|
**Interfaces:**
|
||||||
- Produces: `app.config.get_settings() -> Settings` mit Feldern `database_url: str`, `api_key: str`, `gui_user: str`, `gui_password_hash: str`, `session_secret: str`, `inbox_dir: Path`, `uploads_dir: Path`, `warn_threshold: Decimal`, `horizon_days: int` (Default 548 = 18 Monate).
|
- Produces: `app.config.get_settings() -> Settings` mit Feldern `database_url: str`, `api_key: str`, `gui_user: str`, `gui_password_hash: str`, `session_secret: str`, `inbox_dir: Path`, `uploads_dir: Path`, `warn_threshold: Decimal`, `horizon_days: int` (Default 548 = 18 Monate).
|
||||||
|
|
||||||
- [ ] **Step 1: Dateien anlegen**
|
- [x] **Step 1: Dateien anlegen**
|
||||||
|
|
||||||
`finance/requirements.txt`:
|
`finance/requirements.txt`:
|
||||||
```
|
```
|
||||||
@@ -122,7 +122,7 @@ def test_defaults():
|
|||||||
assert s.gui_user == "admin"
|
assert s.gui_user == "admin"
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: venv anlegen, Dependencies installieren, Test ausführen**
|
- [x] **Step 2: venv anlegen, Dependencies installieren, Test ausführen**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/wlfb/bin/finance
|
cd /home/wlfb/bin/finance
|
||||||
@@ -131,7 +131,7 @@ python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
|
|||||||
```
|
```
|
||||||
Erwartet: `1 passed`.
|
Erwartet: `1 passed`.
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
- [x] **Step 3: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/wlfb/bin && git add finance && git commit -m "feat: Projekt-Geruest finance/ mit Config und Test-Setup"
|
cd /home/wlfb/bin && git add finance && git commit -m "feat: Projekt-Geruest finance/ mit Config und Test-Setup"
|
||||||
@@ -150,7 +150,7 @@ cd /home/wlfb/bin && git add finance && git commit -m "feat: Projekt-Geruest fin
|
|||||||
- Produces: ORM-Klassen `Account, Category, CategoryRule, Statement, Transaction, RecurringItem, PlannedItem, Loan, Scenario, ScenarioLoan, ScenarioModifier, ProjectionPoint, ProjectionResult` (Import aus `app.models.tables`), `app.db.get_session()` (FastAPI-Dependency), `app.db.Base`.
|
- Produces: ORM-Klassen `Account, Category, CategoryRule, Statement, Transaction, RecurringItem, PlannedItem, Loan, Scenario, ScenarioLoan, ScenarioModifier, ProjectionPoint, ProjectionResult` (Import aus `app.models.tables`), `app.db.get_session()` (FastAPI-Dependency), `app.db.Base`.
|
||||||
- Statement.status ∈ {"draft", "confirmed", "error"}; Transaction.status ∈ {"draft", "confirmed"}; RecurringItem.rhythm ∈ {"monthly", "quarterly", "yearly"}; Loan.repayment_type ∈ {"annuity", "bullet"}; ScenarioModifier.target_type ∈ {"category", "recurring"}, kind ∈ {"percent", "absolute", "remove"}.
|
- Statement.status ∈ {"draft", "confirmed", "error"}; Transaction.status ∈ {"draft", "confirmed"}; RecurringItem.rhythm ∈ {"monthly", "quarterly", "yearly"}; Loan.repayment_type ∈ {"annuity", "bullet"}; ScenarioModifier.target_type ∈ {"category", "recurring"}, kind ∈ {"percent", "absolute", "remove"}.
|
||||||
|
|
||||||
- [ ] **Step 1: Failing Test schreiben**
|
- [x] **Step 1: Failing Test schreiben**
|
||||||
|
|
||||||
`finance/tests/conftest.py`:
|
`finance/tests/conftest.py`:
|
||||||
```python
|
```python
|
||||||
@@ -201,9 +201,9 @@ def test_account_transaction_roundtrip(db):
|
|||||||
assert db.query(Transaction).one().amount == Decimal("-50.00")
|
assert db.query(Transaction).one().amount == Decimal("-50.00")
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Test ausführen — erwartet FAIL** (`ModuleNotFoundError: app.db`)
|
- [x] **Step 2: Test ausführen — erwartet FAIL** (`ModuleNotFoundError: app.db`)
|
||||||
|
|
||||||
- [ ] **Step 3: Implementieren**
|
- [x] **Step 3: Implementieren**
|
||||||
|
|
||||||
`finance/app/db.py`:
|
`finance/app/db.py`:
|
||||||
```python
|
```python
|
||||||
@@ -378,9 +378,9 @@ class ProjectionResult(Base):
|
|||||||
from app.models import tables # noqa: F401
|
from app.models import tables # noqa: F401
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Test ausführen — erwartet PASS** (`.venv/bin/python -m pytest -q`)
|
- [x] **Step 4: Test ausführen — erwartet PASS** (`.venv/bin/python -m pytest -q`)
|
||||||
|
|
||||||
- [ ] **Step 5: Alembic initialisieren**
|
- [x] **Step 5: Alembic initialisieren**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/wlfb/bin/finance
|
cd /home/wlfb/bin/finance
|
||||||
@@ -403,7 +403,7 @@ rm mig_test.sqlite
|
|||||||
```
|
```
|
||||||
Erwartet: Migration in `alembic/versions/` erzeugt, `upgrade head` läuft fehlerfrei.
|
Erwartet: Migration in `alembic/versions/` erzeugt, `upgrade head` läuft fehlerfrei.
|
||||||
|
|
||||||
- [ ] **Step 6: Commit** — `git add finance && git commit -m "feat: Datenmodell (SQLAlchemy) und Alembic-Migration"`
|
- [x] **Step 6: Commit** — `git add finance && git commit -m "feat: Datenmodell (SQLAlchemy) und Alembic-Migration"`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -417,7 +417,7 @@ Erwartet: Migration in `alembic/versions/` erzeugt, `upgrade head` läuft fehler
|
|||||||
**Interfaces:**
|
**Interfaces:**
|
||||||
- Produces: `add_months(d: date, months: int) -> date`; `annuity_payment(principal: Decimal, annual_rate_pct: Decimal, term_months: int) -> Decimal`; `loan_schedule(principal, annual_rate_pct, term_months, payout_date, repayment_type) -> list[Installment]` mit `Installment(due: date, payment: Decimal, interest: Decimal, principal: Decimal, remaining: Decimal)`; `payment` ist negativ (Ausgabe).
|
- Produces: `add_months(d: date, months: int) -> date`; `annuity_payment(principal: Decimal, annual_rate_pct: Decimal, term_months: int) -> Decimal`; `loan_schedule(principal, annual_rate_pct, term_months, payout_date, repayment_type) -> list[Installment]` mit `Installment(due: date, payment: Decimal, interest: Decimal, principal: Decimal, remaining: Decimal)`; `payment` ist negativ (Ausgabe).
|
||||||
|
|
||||||
- [ ] **Step 1: Failing Tests schreiben**
|
- [x] **Step 1: Failing Tests schreiben**
|
||||||
|
|
||||||
`finance/tests/test_loans.py`:
|
`finance/tests/test_loans.py`:
|
||||||
```python
|
```python
|
||||||
@@ -452,9 +452,9 @@ def test_bullet_loan():
|
|||||||
assert plan[-1].remaining == Decimal("0.00")
|
assert plan[-1].remaining == Decimal("0.00")
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Ausführen — erwartet FAIL** (Modul fehlt)
|
- [x] **Step 2: Ausführen — erwartet FAIL** (Modul fehlt)
|
||||||
|
|
||||||
- [ ] **Step 3: Implementieren**
|
- [x] **Step 3: Implementieren**
|
||||||
|
|
||||||
`finance/app/engine/loans.py`:
|
`finance/app/engine/loans.py`:
|
||||||
```python
|
```python
|
||||||
@@ -516,9 +516,9 @@ def loan_schedule(principal: Decimal, annual_rate_pct: Decimal, term_months: int
|
|||||||
return plan
|
return plan
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Ausführen — erwartet PASS**
|
- [x] **Step 4: Ausführen — erwartet PASS**
|
||||||
|
|
||||||
- [ ] **Step 5: Commit** — `feat: Kredit-Mathematik (Annuitaet, endfaellig)`
|
- [x] **Step 5: Commit** — `feat: Kredit-Mathematik (Annuitaet, endfaellig)`
|
||||||
|
|
||||||
### Task 4: Wiederholungs-Termine (Recurrence)
|
### Task 4: Wiederholungs-Termine (Recurrence)
|
||||||
|
|
||||||
@@ -528,7 +528,7 @@ def loan_schedule(principal: Decimal, annual_rate_pct: Decimal, term_months: int
|
|||||||
**Interfaces:**
|
**Interfaces:**
|
||||||
- Produces: `occurrences(rhythm: str, due_day: int, window_start: date, window_end: date, item_start: date | None = None, item_end: date | None = None) -> list[date]`.
|
- Produces: `occurrences(rhythm: str, due_day: int, window_start: date, window_end: date, item_start: date | None = None, item_end: date | None = None) -> list[date]`.
|
||||||
|
|
||||||
- [ ] **Step 1: Failing Tests**
|
- [x] **Step 1: Failing Tests**
|
||||||
|
|
||||||
`finance/tests/test_recurrence.py`:
|
`finance/tests/test_recurrence.py`:
|
||||||
```python
|
```python
|
||||||
@@ -553,9 +553,9 @@ def test_yearly():
|
|||||||
assert got == [date(2026, 1, 1), date(2027, 1, 1), date(2028, 1, 1)]
|
assert got == [date(2026, 1, 1), date(2027, 1, 1), date(2028, 1, 1)]
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Ausführen — FAIL**
|
- [x] **Step 2: Ausführen — FAIL**
|
||||||
|
|
||||||
- [ ] **Step 3: Implementieren**
|
- [x] **Step 3: Implementieren**
|
||||||
|
|
||||||
`finance/app/engine/recurrence.py`:
|
`finance/app/engine/recurrence.py`:
|
||||||
```python
|
```python
|
||||||
@@ -586,7 +586,7 @@ def occurrences(rhythm: str, due_day: int, window_start: date, window_end: date,
|
|||||||
return out
|
return out
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Recurrence-Terminberechnung`
|
- [x] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Recurrence-Terminberechnung`
|
||||||
|
|
||||||
### Task 5: Projektion, Modifikatoren, Szenario-Zahlungsstrom
|
### Task 5: Projektion, Modifikatoren, Szenario-Zahlungsstrom
|
||||||
|
|
||||||
@@ -599,7 +599,7 @@ def occurrences(rhythm: str, due_day: int, window_start: date, window_end: date,
|
|||||||
- `scenario.PlainRecurring(name, amount, rhythm, due_day, start_date, end_date, category_id, id)` / `PlainPlanned(name, amount, due, category_id)` / `PlainModifier(target_type, target_id, kind, value)` (Dataclasses).
|
- `scenario.PlainRecurring(name, amount, rhythm, due_day, start_date, end_date, category_id, id)` / `PlainPlanned(name, amount, due, category_id)` / `PlainModifier(target_type, target_id, kind, value)` (Dataclasses).
|
||||||
- `scenario.build_cashflows(recurring: list[PlainRecurring], planned: list[PlainPlanned], loan_schedules: list[list[Installment]], loan_payouts: list[tuple[date, Decimal]], modifiers: list[PlainModifier], window_start: date, window_end: date) -> list[tuple[date, Decimal]]`.
|
- `scenario.build_cashflows(recurring: list[PlainRecurring], planned: list[PlainPlanned], loan_schedules: list[list[Installment]], loan_payouts: list[tuple[date, Decimal]], modifiers: list[PlainModifier], window_start: date, window_end: date) -> list[tuple[date, Decimal]]`.
|
||||||
|
|
||||||
- [ ] **Step 1: Failing Tests**
|
- [x] **Step 1: Failing Tests**
|
||||||
|
|
||||||
`finance/tests/test_projection.py`:
|
`finance/tests/test_projection.py`:
|
||||||
```python
|
```python
|
||||||
@@ -666,9 +666,9 @@ def test_remove_modifier_and_planned_and_loan():
|
|||||||
assert all(a != Decimal("-50") for _, a in flows) # Abo entfernt
|
assert all(a != Decimal("-50") for _, a in flows) # Abo entfernt
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Ausführen — FAIL**
|
- [x] **Step 2: Ausführen — FAIL**
|
||||||
|
|
||||||
- [ ] **Step 3: Implementieren**
|
- [x] **Step 3: Implementieren**
|
||||||
|
|
||||||
`finance/app/engine/projection.py`:
|
`finance/app/engine/projection.py`:
|
||||||
```python
|
```python
|
||||||
@@ -799,7 +799,7 @@ def build_cashflows(recurring: list[PlainRecurring], planned: list[PlainPlanned]
|
|||||||
return sorted(flows)
|
return sorted(flows)
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Szenario-Engine (Projektion, Modifikatoren, Zahlungsstrom)`
|
- [x] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Szenario-Engine (Projektion, Modifikatoren, Zahlungsstrom)`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -816,7 +816,7 @@ def build_cashflows(recurring: list[PlainRecurring], planned: list[PlainPlanned]
|
|||||||
- `detect.detect_bank(text: str) -> str | None` mit Rückgabe `"vr" | "hvb" | "dkb" | None`.
|
- `detect.detect_bank(text: str) -> str | None` mit Rückgabe `"vr" | "hvb" | "dkb" | None`.
|
||||||
- `validate.balance_difference(p: ParsedStatement) -> Decimal` (0 = plausibel); `validate.dedup_hash(account_id: int, booking_date: date, amount: Decimal, purpose: str) -> str`.
|
- `validate.balance_difference(p: ParsedStatement) -> Decimal` (0 = plausibel); `validate.dedup_hash(account_id: int, booking_date: date, amount: Decimal, purpose: str) -> str`.
|
||||||
|
|
||||||
- [ ] **Step 1: Failing Tests**
|
- [x] **Step 1: Failing Tests**
|
||||||
|
|
||||||
`finance/tests/test_parser_base.py`:
|
`finance/tests/test_parser_base.py`:
|
||||||
```python
|
```python
|
||||||
@@ -870,9 +870,9 @@ def test_dedup_hash_normalizes_whitespace():
|
|||||||
assert a == b and len(a) == 64
|
assert a == b and len(a) == 64
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Ausführen — FAIL**
|
- [x] **Step 2: Ausführen — FAIL**
|
||||||
|
|
||||||
- [ ] **Step 3: Implementieren**
|
- [x] **Step 3: Implementieren**
|
||||||
|
|
||||||
`finance/app/parsers/base.py`:
|
`finance/app/parsers/base.py`:
|
||||||
```python
|
```python
|
||||||
@@ -959,7 +959,7 @@ def dedup_hash(account_id: int, booking_date: date, amount: Decimal, purpose: st
|
|||||||
return hashlib.sha256(raw.encode()).hexdigest()
|
return hashlib.sha256(raw.encode()).hexdigest()
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Parser-Basis, Bank-Erkennung, Saldo-/Duplikat-Pruefung`
|
- [x] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Parser-Basis, Bank-Erkennung, Saldo-/Duplikat-Pruefung`
|
||||||
|
|
||||||
### Task 7: Bank-Parser VR, HVB, DKB (fixture-getrieben)
|
### Task 7: Bank-Parser VR, HVB, DKB (fixture-getrieben)
|
||||||
|
|
||||||
@@ -971,7 +971,7 @@ def dedup_hash(account_id: int, booking_date: date, amount: Decimal, purpose: st
|
|||||||
|
|
||||||
**Wichtig:** Die echten PDF-Layouts liegen erst mit den Fixtures vor (Nutzer legt pro Bank mindestens ein echtes PDF unter `finance/tests/fixtures/` ab: `vr_beispiel.pdf`, `hvb_beispiel.pdf`, `dkb_beispiel.pdf` — gitignored). Die Fixture-Tests skippen, solange die Datei fehlt. Die Regex-Konstanten am Dateianfang jedes Parsers sind der einzige anzupassende Teil; Arbeitsschleife pro Bank: Test laufen lassen → bei Fehlschlag mit dem Debug-Skript den Text ansehen → Regex anpassen → wiederholen, bis die Saldo-Prüfung aufgeht.
|
**Wichtig:** Die echten PDF-Layouts liegen erst mit den Fixtures vor (Nutzer legt pro Bank mindestens ein echtes PDF unter `finance/tests/fixtures/` ab: `vr_beispiel.pdf`, `hvb_beispiel.pdf`, `dkb_beispiel.pdf` — gitignored). Die Fixture-Tests skippen, solange die Datei fehlt. Die Regex-Konstanten am Dateianfang jedes Parsers sind der einzige anzupassende Teil; Arbeitsschleife pro Bank: Test laufen lassen → bei Fehlschlag mit dem Debug-Skript den Text ansehen → Regex anpassen → wiederholen, bis die Saldo-Prüfung aufgeht.
|
||||||
|
|
||||||
- [ ] **Step 1: Fixture-README und Tests schreiben**
|
- [x] **Step 1: Fixture-README und Tests schreiben**
|
||||||
|
|
||||||
`finance/tests/fixtures/README.md`:
|
`finance/tests/fixtures/README.md`:
|
||||||
```markdown
|
```markdown
|
||||||
@@ -1014,7 +1014,7 @@ def test_parse_fixture(filename, bank):
|
|||||||
assert balance_difference(stmt) == Decimal("0")
|
assert balance_difference(stmt) == Decimal("0")
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Parser-Gerüst implementieren (gemeinsames Muster)**
|
- [x] **Step 2: Parser-Gerüst implementieren (gemeinsames Muster)**
|
||||||
|
|
||||||
`finance/app/parsers/vr.py` (HVB/DKB analog — gleiche Struktur, eigene Konstanten; Dateien `hvb.py`/`dkb.py` identisch aufbauen, nur `BANK = "hvb"` bzw. `"dkb"`):
|
`finance/app/parsers/vr.py` (HVB/DKB analog — gleiche Struktur, eigene Konstanten; Dateien `hvb.py`/`dkb.py` identisch aufbauen, nur `BANK = "hvb"` bzw. `"dkb"`):
|
||||||
```python
|
```python
|
||||||
@@ -1115,7 +1115,7 @@ def parse_pdf(path: Path) -> ParsedStatement:
|
|||||||
return PARSERS[bank](path)
|
return PARSERS[bank](path)
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Debug-Hilfsskript anlegen** — `finance/scripts/dump_pdf_text.py`:
|
- [x] **Step 3: Debug-Hilfsskript anlegen** — `finance/scripts/dump_pdf_text.py`:
|
||||||
```python
|
```python
|
||||||
"""PDF-Text anzeigen, um Parser-Regexe abzugleichen: python scripts/dump_pdf_text.py <pdf>"""
|
"""PDF-Text anzeigen, um Parser-Regexe abzugleichen: python scripts/dump_pdf_text.py <pdf>"""
|
||||||
import sys
|
import sys
|
||||||
@@ -1128,9 +1128,9 @@ with pdfplumber.open(sys.argv[1]) as pdf:
|
|||||||
print(page.extract_text() or "(kein Text)")
|
print(page.extract_text() or "(kein Text)")
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Fixture-Abgleich je Bank** — Für jede vorhandene Fixture: `.venv/bin/python -m pytest tests/test_bank_parsers.py -v` ausführen; bei FAIL mit `dump_pdf_text.py` den echten Text ansehen und **nur die RE_\*-Konstanten** (und falls nötig die Salden-Begriffe, z. B. „Kontostand am…") des jeweiligen Parsers anpassen, bis der Test inkl. `balance_difference == 0` grün ist. Fehlt eine Fixture noch, bleibt der Test geskippt — dann diesen Zustand committen und den Abgleich nachholen, sobald die Datei da ist.
|
- [x] **Step 4: Fixture-Abgleich je Bank** — Für jede vorhandene Fixture: `.venv/bin/python -m pytest tests/test_bank_parsers.py -v` ausführen; bei FAIL mit `dump_pdf_text.py` den echten Text ansehen und **nur die RE_\*-Konstanten** (und falls nötig die Salden-Begriffe, z. B. „Kontostand am…") des jeweiligen Parsers anpassen, bis der Test inkl. `balance_difference == 0` grün ist. Fehlt eine Fixture noch, bleibt der Test geskippt — dann diesen Zustand committen und den Abgleich nachholen, sobald die Datei da ist.
|
||||||
|
|
||||||
- [ ] **Step 5: Alle Tests ausführen — PASS/SKIP** · **Step 6: Commit** — `feat: PDF-Parser VR/HVB/DKB mit Registry und Fixture-Tests`
|
- [x] **Step 5: Alle Tests ausführen — PASS/SKIP** · **Step 6: Commit** — `feat: PDF-Parser VR/HVB/DKB mit Registry und Fixture-Tests`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1145,7 +1145,7 @@ with pdfplumber.open(sys.argv[1]) as pdf:
|
|||||||
- Produces: `app.main.app` (FastAPI); `auth.hash_password(pw: str) -> str` (Format `salt$hex`, PBKDF2-SHA256); `auth.verify_password(pw: str, stored: str) -> bool`; Dependency `auth.require_auth` (akzeptiert `Authorization: Bearer <FB_API_KEY>` **oder** gültiges Session-Cookie `fb_session`, sonst 401); Routen `POST /login` (Form: `username`, `password`; setzt Cookie, Redirect `/`), `GET /login` (Formular), `POST /logout`.
|
- Produces: `app.main.app` (FastAPI); `auth.hash_password(pw: str) -> str` (Format `salt$hex`, PBKDF2-SHA256); `auth.verify_password(pw: str, stored: str) -> bool`; Dependency `auth.require_auth` (akzeptiert `Authorization: Bearer <FB_API_KEY>` **oder** gültiges Session-Cookie `fb_session`, sonst 401); Routen `POST /login` (Form: `username`, `password`; setzt Cookie, Redirect `/`), `GET /login` (Formular), `POST /logout`.
|
||||||
- Consumes: `get_settings`, `get_session`.
|
- Consumes: `get_settings`, `get_session`.
|
||||||
|
|
||||||
- [ ] **Step 1: conftest erweitern + Failing Tests**
|
- [x] **Step 1: conftest erweitern + Failing Tests**
|
||||||
|
|
||||||
In `finance/tests/conftest.py` ergänzen:
|
In `finance/tests/conftest.py` ergänzen:
|
||||||
```python
|
```python
|
||||||
@@ -1188,9 +1188,9 @@ def test_login_flow(client):
|
|||||||
assert client.get("/api/accounts").status_code == 200 # Cookie reicht
|
assert client.get("/api/accounts").status_code == 200 # Cookie reicht
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Ausführen — FAIL**
|
- [x] **Step 2: Ausführen — FAIL**
|
||||||
|
|
||||||
- [ ] **Step 3: Implementieren**
|
- [x] **Step 3: Implementieren**
|
||||||
|
|
||||||
`finance/app/auth.py`:
|
`finance/app/auth.py`:
|
||||||
```python
|
```python
|
||||||
@@ -1294,7 +1294,7 @@ def _placeholder_accounts(): # wird in Task 9 durch Router ersetzt
|
|||||||
return []
|
return []
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: FastAPI-App mit API-Key- und Session-Auth`
|
- [x] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: FastAPI-App mit API-Key- und Session-Auth`
|
||||||
|
|
||||||
### Task 9: CRUD — Konten, Buchungen, Kategorien, Regeln
|
### Task 9: CRUD — Konten, Buchungen, Kategorien, Regeln
|
||||||
|
|
||||||
@@ -1310,7 +1310,7 @@ def _placeholder_accounts(): # wird in Task 9 durch Router ersetzt
|
|||||||
- `services.categorize.apply_rules(session, transactions) -> int` (setzt `category_id` per Regel, Rückgabe: Anzahl kategorisiert; Regel-Match: `rule.pattern.lower() in (purpose + " " + counterparty).lower()`, Regeln nach `priority` aufsteigend, erster Treffer gewinnt).
|
- `services.categorize.apply_rules(session, transactions) -> int` (setzt `category_id` per Regel, Rückgabe: Anzahl kategorisiert; Regel-Match: `rule.pattern.lower() in (purpose + " " + counterparty).lower()`, Regeln nach `priority` aufsteigend, erster Treffer gewinnt).
|
||||||
- `services.balances.account_balance(session, account) -> Decimal` (Endsaldo des neuesten bestätigten Statements + Summe bestätigter Buchungen mit `booking_date > period_end`; ohne Statement: Summe aller bestätigten Buchungen). `services.balances.total_balance(session) -> Decimal`.
|
- `services.balances.account_balance(session, account) -> Decimal` (Endsaldo des neuesten bestätigten Statements + Summe bestätigter Buchungen mit `booking_date > period_end`; ohne Statement: Summe aller bestätigten Buchungen). `services.balances.total_balance(session) -> Decimal`.
|
||||||
|
|
||||||
- [ ] **Step 1: Failing Tests** — `finance/tests/test_crud_api.py`:
|
- [x] **Step 1: Failing Tests** — `finance/tests/test_crud_api.py`:
|
||||||
```python
|
```python
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -1364,9 +1364,9 @@ def test_rules_categorize(client):
|
|||||||
assert txs[0]["category_id"] == cat["id"]
|
assert txs[0]["category_id"] == cat["id"]
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Ausführen — FAIL**
|
- [x] **Step 2: Ausführen — FAIL**
|
||||||
|
|
||||||
- [ ] **Step 3: Implementieren** — Router mit Pydantic-Schemas (`BaseModel` je Endpoint, `model_config = ConfigDict(from_attributes=True)`), Services:
|
- [x] **Step 3: Implementieren** — Router mit Pydantic-Schemas (`BaseModel` je Endpoint, `model_config = ConfigDict(from_attributes=True)`), Services:
|
||||||
|
|
||||||
`finance/app/services/categorize.py`:
|
`finance/app/services/categorize.py`:
|
||||||
```python
|
```python
|
||||||
@@ -1475,7 +1475,7 @@ def create_account(data: AccountIn, session: Session = Depends(get_session)):
|
|||||||
```
|
```
|
||||||
In `main.py`: Platzhalter-Route `_placeholder_accounts` löschen, stattdessen `app.include_router(accounts.router)` etc.
|
In `main.py`: Platzhalter-Route `_placeholder_accounts` löschen, stattdessen `app.include_router(accounts.router)` etc.
|
||||||
|
|
||||||
- [ ] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: CRUD-API Konten/Buchungen/Kategorien/Regeln`
|
- [x] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: CRUD-API Konten/Buchungen/Kategorien/Regeln`
|
||||||
|
|
||||||
### Task 10: Import-API (Upload → Vorschau → Bestätigen)
|
### Task 10: Import-API (Upload → Vorschau → Bestätigen)
|
||||||
|
|
||||||
@@ -1491,7 +1491,7 @@ In `main.py`: Platzhalter-Route `_placeholder_accounts` löschen, stattdessen `a
|
|||||||
- `POST /api/imports/{id}/confirm` → alle Drafts mit `is_duplicate=False` auf `confirmed`, Statement auf `confirmed`; `DELETE /api/imports/{id}` → Statement + zugehörige Drafts löschen.
|
- `POST /api/imports/{id}/confirm` → alle Drafts mit `is_duplicate=False` auf `confirmed`, Statement auf `confirmed`; `DELETE /api/imports/{id}` → Statement + zugehörige Drafts löschen.
|
||||||
- Consumes: `registry.parse_pdf`, `validate.*`, `categorize.apply_rules`.
|
- Consumes: `registry.parse_pdf`, `validate.*`, `categorize.apply_rules`.
|
||||||
|
|
||||||
- [ ] **Step 1: Failing Tests** — `finance/tests/test_import_api.py`: Da echte PDFs in Unit-Tests fehlen, wird `registry.parse_pdf` gemockt:
|
- [x] **Step 1: Failing Tests** — `finance/tests/test_import_api.py`: Da echte PDFs in Unit-Tests fehlen, wird `registry.parse_pdf` gemockt:
|
||||||
```python
|
```python
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -1543,9 +1543,9 @@ def test_upload_preview_confirm(client, fake_parse, tmp_path, monkeypatch):
|
|||||||
assert len(client.get("/api/transactions", headers=H).json()) == 1
|
assert len(client.get("/api/transactions", headers=H).json()) == 1
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Ausführen — FAIL** · **Step 3: Implementieren** gemäß Interface-Block (Importer-Service + Router; `parse_pdf` in `importer.py` importieren als `from app.parsers.registry import parse_pdf`, damit der Test-Monkeypatch greift).
|
- [x] **Step 2: Ausführen — FAIL** · **Step 3: Implementieren** gemäß Interface-Block (Importer-Service + Router; `parse_pdf` in `importer.py` importieren als `from app.parsers.registry import parse_pdf`, damit der Test-Monkeypatch greift).
|
||||||
|
|
||||||
- [ ] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Import-API mit Vorschau, Saldo-Pruefung und Duplikat-Schutz`
|
- [x] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Import-API mit Vorschau, Saldo-Pruefung und Duplikat-Schutz`
|
||||||
|
|
||||||
### Task 11: Planungs- & Szenario-API
|
### Task 11: Planungs- & Szenario-API
|
||||||
|
|
||||||
@@ -1559,7 +1559,7 @@ def test_upload_preview_confirm(client, fake_parse, tmp_path, monkeypatch):
|
|||||||
- Szenarien: `GET/POST /api/scenarios`, `PATCH/DELETE /api/scenarios/{id}`, `POST /api/scenarios/{id}/loans/{loan_id}`, `DELETE` dito, `POST /api/scenarios/{id}/modifiers` + `DELETE /api/scenarios/{id}/modifiers/{mod_id}`.
|
- Szenarien: `GET/POST /api/scenarios`, `PATCH/DELETE /api/scenarios/{id}`, `POST /api/scenarios/{id}/loans/{loan_id}`, `DELETE` dito, `POST /api/scenarios/{id}/modifiers` + `DELETE /api/scenarios/{id}/modifiers/{mod_id}`.
|
||||||
- `POST /api/scenarios/{id}/project?horizon_days=548` → `services.projection_service.run_projection(session, scenario, horizon_days, start_date) -> ProjectionResult`: Startsaldo = `total_balance`; ORM → Plain-Dataclasses; `build_cashflows` + `project` (threshold = `settings.warn_threshold`); alte `ProjectionPoint`s des Szenarios löschen, neue Serie + `ProjectionResult` (upsert) speichern; Response `{low_point_date, low_point_balance, below_zero_date, below_threshold_date, series: [{day, balance}, ...]}`. `start_date` Parameter default: heutiges Datum (`date.today()` nur hier, nicht in der Engine).
|
- `POST /api/scenarios/{id}/project?horizon_days=548` → `services.projection_service.run_projection(session, scenario, horizon_days, start_date) -> ProjectionResult`: Startsaldo = `total_balance`; ORM → Plain-Dataclasses; `build_cashflows` + `project` (threshold = `settings.warn_threshold`); alte `ProjectionPoint`s des Szenarios löschen, neue Serie + `ProjectionResult` (upsert) speichern; Response `{low_point_date, low_point_balance, below_zero_date, below_threshold_date, series: [{day, balance}, ...]}`. `start_date` Parameter default: heutiges Datum (`date.today()` nur hier, nicht in der Engine).
|
||||||
|
|
||||||
- [ ] **Step 1: Failing Test** — `finance/tests/test_planning_api.py`:
|
- [x] **Step 1: Failing Test** — `finance/tests/test_planning_api.py`:
|
||||||
```python
|
```python
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -1610,7 +1610,7 @@ def test_loan_in_scenario_keeps_balance_positive(client, db):
|
|||||||
assert Decimal(body["low_point_balance"]) > Decimal("0")
|
assert Decimal(body["low_point_balance"]) > Decimal("0")
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Ausführen — FAIL** · **Step 3: Implementieren** gemäß Interface-Block. Kern von `projection_service.run_projection`:
|
- [x] **Step 2: Ausführen — FAIL** · **Step 3: Implementieren** gemäß Interface-Block. Kern von `projection_service.run_projection`:
|
||||||
```python
|
```python
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -1672,7 +1672,7 @@ def run_projection(session: Session, scenario: Scenario, horizon_days: int,
|
|||||||
return result
|
return result
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Planungs- und Szenario-API mit Projektion und Vorschlaegen`
|
- [x] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Planungs- und Szenario-API mit Projektion und Vorschlaegen`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1688,14 +1688,14 @@ def run_projection(session: Session, scenario: Scenario, horizon_days: int,
|
|||||||
- Produces: HTML-Routen `GET /` (Übersicht), `GET /buchungen` — beide leiten ohne gültige Session auf `/login` um (302), API-Key gilt hier nicht. GUI-Formulare posten auf die bestehenden `/api/...`-Endpunkte (Cookie-Auth greift dort).
|
- Produces: HTML-Routen `GET /` (Übersicht), `GET /buchungen` — beide leiten ohne gültige Session auf `/login` um (302), API-Key gilt hier nicht. GUI-Formulare posten auf die bestehenden `/api/...`-Endpunkte (Cookie-Auth greift dort).
|
||||||
- Login-Formular aus Task 8 wird durch Template `login.html` ersetzt (gleiche Routen).
|
- Login-Formular aus Task 8 wird durch Template `login.html` ersetzt (gleiche Routen).
|
||||||
|
|
||||||
- [ ] **Step 1: htmx vendoren**
|
- [x] **Step 1: htmx vendoren**
|
||||||
```bash
|
```bash
|
||||||
mkdir -p /home/wlfb/bin/finance/app/static
|
mkdir -p /home/wlfb/bin/finance/app/static
|
||||||
curl -fsSL https://unpkg.com/htmx.org@2.0.6/dist/htmx.min.js \
|
curl -fsSL https://unpkg.com/htmx.org@2.0.6/dist/htmx.min.js \
|
||||||
-o /home/wlfb/bin/finance/app/static/htmx.min.js
|
-o /home/wlfb/bin/finance/app/static/htmx.min.js
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Failing Test** — `finance/tests/test_gui.py`:
|
- [x] **Step 2: Failing Test** — `finance/tests/test_gui.py`:
|
||||||
```python
|
```python
|
||||||
def test_pages_require_login(client):
|
def test_pages_require_login(client):
|
||||||
for path in ("/", "/buchungen", "/import", "/planung"):
|
for path in ("/", "/buchungen", "/import", "/planung"):
|
||||||
@@ -1713,7 +1713,7 @@ def test_pages_render_after_login(client):
|
|||||||
```
|
```
|
||||||
(`/import` und `/planung` kommen in Task 13 — Test deckt sie schon ab; bis dahin dürfen die Routen minimal existieren und nur das Template-Gerüst rendern.)
|
(`/import` und `/planung` kommen in Task 13 — Test deckt sie schon ab; bis dahin dürfen die Routen minimal existieren und nur das Template-Gerüst rendern.)
|
||||||
|
|
||||||
- [ ] **Step 3: Implementieren** — `base.html` mit Navigation (Übersicht / Import / Buchungen / Planung / Grafana-Link auf `http://<host>:8097`, Logout-Button), `<script src="/static/htmx.min.js"></script>`, `style.css` schlicht (max-width, Tabellenstil, rote Negativbeträge `.neg { color: #b00 }`). `index.html`: Gesamtsaldo + Tabelle Konto→Saldo (`services.balances`), Liste der nächsten 10 anstehenden Posten (occurrences der RecurringItems + PlannedItems der nächsten 30 Tage), Warnbox falls neueste `ProjectionResult.below_threshold_date` gesetzt, `<iframe>` auf Grafana-Dashboard. `transactions.html`: Filterformular (GET-Parameter an eigene Route, serverseitig gefiltert über dieselbe Query-Logik wie `/api/transactions`), Tabelle, Inline-`<select>` je Zeile für Kategorie (HTMX `hx-patch` auf `/api/transactions/{id}`), Formular „Buchung manuell erfassen" (HTMX-Post auf `/api/transactions`), Button „Regel daraus erzeugen" (Formular vorbefüllt mit counterparty als Pattern, Post auf `/api/category-rules`). GUI-Router-Muster:
|
- [x] **Step 3: Implementieren** — `base.html` mit Navigation (Übersicht / Import / Buchungen / Planung / Grafana-Link auf `http://<host>:8097`, Logout-Button), `<script src="/static/htmx.min.js"></script>`, `style.css` schlicht (max-width, Tabellenstil, rote Negativbeträge `.neg { color: #b00 }`). `index.html`: Gesamtsaldo + Tabelle Konto→Saldo (`services.balances`), Liste der nächsten 10 anstehenden Posten (occurrences der RecurringItems + PlannedItems der nächsten 30 Tage), Warnbox falls neueste `ProjectionResult.below_threshold_date` gesetzt, `<iframe>` auf Grafana-Dashboard. `transactions.html`: Filterformular (GET-Parameter an eigene Route, serverseitig gefiltert über dieselbe Query-Logik wie `/api/transactions`), Tabelle, Inline-`<select>` je Zeile für Kategorie (HTMX `hx-patch` auf `/api/transactions/{id}`), Formular „Buchung manuell erfassen" (HTMX-Post auf `/api/transactions`), Button „Regel daraus erzeugen" (Formular vorbefüllt mit counterparty als Pattern, Post auf `/api/category-rules`). GUI-Router-Muster:
|
||||||
```python
|
```python
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
@@ -1744,7 +1744,7 @@ def index(request: Request, session: Session = Depends(get_session)):
|
|||||||
... # Salden + anstehende Posten laden, templates.TemplateResponse("index.html", {...})
|
... # Salden + anstehende Posten laden, templates.TemplateResponse("index.html", {...})
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Web-GUI Basis, Uebersicht, Buchungsliste`
|
- [x] **Step 4: Ausführen — PASS** · **Step 5: Commit** — `feat: Web-GUI Basis, Uebersicht, Buchungsliste`
|
||||||
|
|
||||||
### Task 13: Import-Seite (Drag-and-Drop) und Planungs-Seite
|
### Task 13: Import-Seite (Drag-and-Drop) und Planungs-Seite
|
||||||
|
|
||||||
@@ -1755,13 +1755,13 @@ def index(request: Request, session: Session = Depends(get_session)):
|
|||||||
**Interfaces:**
|
**Interfaces:**
|
||||||
- Consumes: Import-API (Task 10), Planungs-API (Task 11).
|
- Consumes: Import-API (Task 10), Planungs-API (Task 11).
|
||||||
|
|
||||||
- [ ] **Step 1: Implementieren**
|
- [x] **Step 1: Implementieren**
|
||||||
- `import.html`: Drop-Zone (`<div id="dropzone">`) mit kleinem Inline-Script: `dragover`/`drop`-Events, `FormData` + `fetch("/api/imports/upload", {method: "POST", body: fd})` (Cookie-Auth), danach `htmx.trigger("#imports", "refresh")`. Importliste als HTMX-Fragment (`hx-get`-Polling alle 5 s), Status-Badges (draft = „wartet auf Bestätigung", error = rot mit `error_message`, confirmed = grün). Je Draft: aufklappbare Vorschau (`_preview_table.html`: Buchungstabelle, Saldo-Status, Duplikat-Markierung) mit Buttons „Übernehmen" (`hx-post /api/imports/{id}/confirm`) und „Verwerfen" (`hx-delete`).
|
- `import.html`: Drop-Zone (`<div id="dropzone">`) mit kleinem Inline-Script: `dragover`/`drop`-Events, `FormData` + `fetch("/api/imports/upload", {method: "POST", body: fd})` (Cookie-Auth), danach `htmx.trigger("#imports", "refresh")`. Importliste als HTMX-Fragment (`hx-get`-Polling alle 5 s), Status-Badges (draft = „wartet auf Bestätigung", error = rot mit `error_message`, confirmed = grün). Je Draft: aufklappbare Vorschau (`_preview_table.html`: Buchungstabelle, Saldo-Status, Duplikat-Markierung) mit Buttons „Übernehmen" (`hx-post /api/imports/{id}/confirm`) und „Verwerfen" (`hx-delete`).
|
||||||
- `planning.html`: vier Abschnitte (wiederkehrende Posten inkl. „Vorschläge übernehmen"-Liste aus `/api/recurring/suggestions`, Einmalposten, Kredite mit Tilgungsplan-Aufklapper, Szenarien). Szenario-Abschnitt: Liste + Formular (Name, Checkboxen include_recurring/include_planned, Kredite zuordnen, Modifikatoren hinzufügen: Ziel-Dropdown Kategorie/Posten, Art percent/absolute/remove, Wert), Button „Durchrechnen" (`hx-post /api/scenarios/{id}/project`), Ergebnisanzeige (Tiefpunkt mit Datum, Unterschreitungs-Daten, Hinweis „Kurven in Grafana ansehen").
|
- `planning.html`: vier Abschnitte (wiederkehrende Posten inkl. „Vorschläge übernehmen"-Liste aus `/api/recurring/suggestions`, Einmalposten, Kredite mit Tilgungsplan-Aufklapper, Szenarien). Szenario-Abschnitt: Liste + Formular (Name, Checkboxen include_recurring/include_planned, Kredite zuordnen, Modifikatoren hinzufügen: Ziel-Dropdown Kategorie/Posten, Art percent/absolute/remove, Wert), Button „Durchrechnen" (`hx-post /api/scenarios/{id}/project`), Ergebnisanzeige (Tiefpunkt mit Datum, Unterschreitungs-Daten, Hinweis „Kurven in Grafana ansehen").
|
||||||
|
|
||||||
- [ ] **Step 2: Alle Tests ausführen — PASS** (Test aus Task 12 deckt die neuen Routen ab)
|
- [x] **Step 2: Alle Tests ausführen — PASS** (Test aus Task 12 deckt die neuen Routen ab)
|
||||||
|
|
||||||
- [ ] **Step 3: Manuelle Prüfung** — App lokal starten und durchklicken:
|
- [x] **Step 3: Manuelle Prüfung** — App lokal starten und durchklicken:
|
||||||
```bash
|
```bash
|
||||||
cd /home/wlfb/bin/finance
|
cd /home/wlfb/bin/finance
|
||||||
FB_API_KEY=dev FB_GUI_PASSWORD_HASH=$(.venv/bin/python -c \
|
FB_API_KEY=dev FB_GUI_PASSWORD_HASH=$(.venv/bin/python -c \
|
||||||
@@ -1770,7 +1770,7 @@ FB_API_KEY=dev FB_GUI_PASSWORD_HASH=$(.venv/bin/python -c \
|
|||||||
```
|
```
|
||||||
Login mit admin/dev, alle vier Seiten öffnen, eine Test-PDF in die Drop-Zone ziehen (Fehlerfall „Bank nicht erkannt" ist ok und muss sauber angezeigt werden).
|
Login mit admin/dev, alle vier Seiten öffnen, eine Test-PDF in die Drop-Zone ziehen (Fehlerfall „Bank nicht erkannt" ist ok und muss sauber angezeigt werden).
|
||||||
|
|
||||||
- [ ] **Step 4: Commit** — `feat: Import-Seite mit Drag-and-Drop und Planungs-Seite`
|
- [x] **Step 4: Commit** — `feat: Import-Seite mit Drag-and-Drop und Planungs-Seite`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1785,7 +1785,7 @@ Login mit admin/dev, alle vier Seiten öffnen, eine Test-PDF in die Drop-Zone zi
|
|||||||
**Interfaces:**
|
**Interfaces:**
|
||||||
- Produces: Views `v_balance_history(day, account, balance)` (Fensterfunktion, kumulierte bestätigte Buchungen je Konto), `v_balance_total(day, balance)`, `v_monthly_by_category(month, category, total)`, `v_projection(scenario, day, balance)`.
|
- Produces: Views `v_balance_history(day, account, balance)` (Fensterfunktion, kumulierte bestätigte Buchungen je Konto), `v_balance_total(day, balance)`, `v_monthly_by_category(month, category, total)`, `v_projection(scenario, day, balance)`.
|
||||||
|
|
||||||
- [ ] **Step 1: Views implementieren** — `finance/app/models/views.py`:
|
- [x] **Step 1: Views implementieren** — `finance/app/models/views.py`:
|
||||||
```python
|
```python
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
@@ -1823,7 +1823,7 @@ def create_views(engine: Engine) -> None:
|
|||||||
```
|
```
|
||||||
In `main.py` via Lifespan-Handler aufrufen (`create_views(get_engine())`).
|
In `main.py` via Lifespan-Handler aufrufen (`create_views(get_engine())`).
|
||||||
|
|
||||||
- [ ] **Step 2: Grafana-Provisionierung** — `datasources/postgres.yaml`:
|
- [x] **Step 2: Grafana-Provisionierung** — `datasources/postgres.yaml`:
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: 1
|
apiVersion: 1
|
||||||
datasources:
|
datasources:
|
||||||
@@ -1854,14 +1854,14 @@ providers:
|
|||||||
4. *Szenario-Vergleich* (timeseries, Threshold-Linien bei 0 und Warnschwelle): `SELECT day AS time, scenario AS metric, balance AS value FROM v_projection ORDER BY day`
|
4. *Szenario-Vergleich* (timeseries, Threshold-Linien bei 0 und Warnschwelle): `SELECT day AS time, scenario AS metric, balance AS value FROM v_projection ORDER BY day`
|
||||||
JSON per Hand aus einem minimalen Grafana-Dashboard-Export aufbauen (`schemaVersion` aktuell, `uid: "finanzen"`); endgültige Feinheiten (Stacking, Thresholds) beim Smoke-Test in Task 16 in der Grafana-UI justieren und JSON re-exportieren.
|
JSON per Hand aus einem minimalen Grafana-Dashboard-Export aufbauen (`schemaVersion` aktuell, `uid: "finanzen"`); endgültige Feinheiten (Stacking, Thresholds) beim Smoke-Test in Task 16 in der Grafana-UI justieren und JSON re-exportieren.
|
||||||
|
|
||||||
- [ ] **Step 3: Tests laufen lassen — PASS** (Views werden auf SQLite übersprungen) · **Step 4: Commit** — `feat: SQL-Views und Grafana-Provisionierung`
|
- [x] **Step 3: Tests laufen lassen — PASS** (Views werden auf SQLite übersprungen) · **Step 4: Commit** — `feat: SQL-Views und Grafana-Provisionierung`
|
||||||
|
|
||||||
### Task 15: Containerfile
|
### Task 15: Containerfile
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `finance/Containerfile`, `finance/entrypoint.sh`
|
- Create: `finance/Containerfile`, `finance/entrypoint.sh`
|
||||||
|
|
||||||
- [ ] **Step 1: Schreiben**
|
- [x] **Step 1: Schreiben**
|
||||||
|
|
||||||
`finance/Containerfile`:
|
`finance/Containerfile`:
|
||||||
```dockerfile
|
```dockerfile
|
||||||
@@ -1888,13 +1888,13 @@ alembic upgrade head
|
|||||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
|
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Build testen**
|
- [x] **Step 2: Build testen**
|
||||||
```bash
|
```bash
|
||||||
cd /home/wlfb/bin/finance && podman build -t localhost/finance-api:0.1.0 .
|
cd /home/wlfb/bin/finance && podman build -t localhost/finance-api:0.1.0 .
|
||||||
```
|
```
|
||||||
Erwartet: Build erfolgreich.
|
Erwartet: Build erfolgreich.
|
||||||
|
|
||||||
- [ ] **Step 3: Commit** — `feat: Containerfile und Entrypoint`
|
- [x] **Step 3: Commit** — `feat: Containerfile und Entrypoint`
|
||||||
|
|
||||||
### Task 16: Pod-Skript `create_pod_finance.sh`
|
### Task 16: Pod-Skript `create_pod_finance.sh`
|
||||||
|
|
||||||
@@ -1903,7 +1903,7 @@ Erwartet: Build erfolgreich.
|
|||||||
|
|
||||||
**Struktur — exakt dem Muster von `example_create_pod_langflow.sh` folgen** (Reihenfolge: Variablen → systemd-Stop → Verzeichnisse → Pod → Container → `podman generate systemd --name --new --files` → Pod stoppen/entfernen → `systemctl --user enable --now` → Readiness-Check):
|
**Struktur — exakt dem Muster von `example_create_pod_langflow.sh` folgen** (Reihenfolge: Variablen → systemd-Stop → Verzeichnisse → Pod → Container → `podman generate systemd --name --new --files` → Pod stoppen/entfernen → `systemctl --user enable --now` → Readiness-Check):
|
||||||
|
|
||||||
- [ ] **Step 1: Skript schreiben** — Eckdaten:
|
- [x] **Step 1: Skript schreiben** — Eckdaten:
|
||||||
```bash
|
```bash
|
||||||
POD_NAME='finance_pod'
|
POD_NAME='finance_pod'
|
||||||
DB_CTR_NAME='finance-db_ctr'
|
DB_CTR_NAME='finance-db_ctr'
|
||||||
@@ -1958,7 +1958,7 @@ podman exec "$DB_CTR_NAME" psql -U finance -d finance -c \
|
|||||||
- Grafana: `-e GF_SECURITY_ADMIN_PASSWORD="$GRAFANA_ADMIN_PASSWORD" -e FINANCE_READ_PASSWORD -v "$HOME/bin/finance/grafana/provisioning:/etc/grafana/provisioning:Z,ro" -v "$HOME/bin/finance/grafana/dashboards:/var/lib/grafana/dashboards:Z,ro" -v "$BIND_DIR/grafana-data:/var/lib/grafana:Z"`
|
- Grafana: `-e GF_SECURITY_ADMIN_PASSWORD="$GRAFANA_ADMIN_PASSWORD" -e FINANCE_READ_PASSWORD -v "$HOME/bin/finance/grafana/provisioning:/etc/grafana/provisioning:Z,ro" -v "$HOME/bin/finance/grafana/dashboards:/var/lib/grafana/dashboards:Z,ro" -v "$BIND_DIR/grafana-data:/var/lib/grafana:Z"`
|
||||||
4. **Readiness-Check** am Ende: curl auf `http://127.0.0.1:8096/login` und `http://127.0.0.1:8097/api/health`.
|
4. **Readiness-Check** am Ende: curl auf `http://127.0.0.1:8096/login` und `http://127.0.0.1:8097/api/health`.
|
||||||
|
|
||||||
- [ ] **Step 2: Ausführen und verifizieren**
|
- [x] **Step 2: Ausführen und verifizieren**
|
||||||
```bash
|
```bash
|
||||||
chmod +x /home/wlfb/bin/create_pod_finance.sh && /home/wlfb/bin/create_pod_finance.sh
|
chmod +x /home/wlfb/bin/create_pod_finance.sh && /home/wlfb/bin/create_pod_finance.sh
|
||||||
systemctl --user status pod-finance_pod.service --no-pager
|
systemctl --user status pod-finance_pod.service --no-pager
|
||||||
@@ -1966,14 +1966,14 @@ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8096/login # 200
|
|||||||
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8097/api/health # 200
|
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8097/api/health # 200
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: End-to-End-Smoke-Test** — Im Browser: GUI-Login mit dem ausgegebenen Passwort, ein Fixture-PDF per Drag-and-Drop importieren, Vorschau bestätigen, Buchungen prüfen; Grafana öffnen, Dashboard „Finanzen" zeigt Daten; ein Basis-Szenario anlegen und durchrechnen. API mit Key testen:
|
- [x] **Step 3: End-to-End-Smoke-Test** — Im Browser: GUI-Login mit dem ausgegebenen Passwort, ein Fixture-PDF per Drag-and-Drop importieren, Vorschau bestätigen, Buchungen prüfen; Grafana öffnen, Dashboard „Finanzen" zeigt Daten; ein Basis-Szenario anlegen und durchrechnen. API mit Key testen:
|
||||||
```bash
|
```bash
|
||||||
. /home/wlfb/bin/finance/.env
|
. /home/wlfb/bin/finance/.env
|
||||||
curl -s -H "Authorization: Bearer $FB_API_KEY" http://127.0.0.1:8096/api/accounts
|
curl -s -H "Authorization: Bearer $FB_API_KEY" http://127.0.0.1:8096/api/accounts
|
||||||
```
|
```
|
||||||
Grafana-Panel-Feinschliff (Task 14 Step 2) jetzt erledigen, JSON re-exportieren und einchecken.
|
Grafana-Panel-Feinschliff (Task 14 Step 2) jetzt erledigen, JSON re-exportieren und einchecken.
|
||||||
|
|
||||||
- [ ] **Step 4: Commit** — `feat: Pod-Skript finance_pod mit systemd-Service und Secrets-Bootstrap`
|
- [x] **Step 4: Commit** — `feat: Pod-Skript finance_pod mit systemd-Service und Secrets-Bootstrap`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1984,7 +1984,7 @@ Grafana-Panel-Feinschliff (Task 14 Step 2) jetzt erledigen, JSON re-exportieren
|
|||||||
**Files (alle im Repo `/home/wlfb/fb` — dort committen!):**
|
**Files (alle im Repo `/home/wlfb/fb` — dort committen!):**
|
||||||
- Create: `/home/wlfb/fb/CLAUDE.md`, `/home/wlfb/fb/.claude/skills/finanz-api/SKILL.md`, `/home/wlfb/fb/.claude/skills/finanzberatung/SKILL.md`, `/home/wlfb/fb/.claude/skills/auszug-import/SKILL.md`
|
- Create: `/home/wlfb/fb/CLAUDE.md`, `/home/wlfb/fb/.claude/skills/finanz-api/SKILL.md`, `/home/wlfb/fb/.claude/skills/finanzberatung/SKILL.md`, `/home/wlfb/fb/.claude/skills/auszug-import/SKILL.md`
|
||||||
|
|
||||||
- [ ] **Step 1: CLAUDE.md schreiben** — Inhalt:
|
- [x] **Step 1: CLAUDE.md schreiben** — Inhalt:
|
||||||
```markdown
|
```markdown
|
||||||
# Finanzberatungs-Umgebung
|
# Finanzberatungs-Umgebung
|
||||||
|
|
||||||
@@ -2006,7 +2006,7 @@ Beratung und Auswertung laufen auf Deutsch.
|
|||||||
- Für API-Nutzung den Skill `finanz-api` verwenden.
|
- Für API-Nutzung den Skill `finanz-api` verwenden.
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Skills schreiben** — jeweils `SKILL.md` mit Frontmatter (`name`, `description` mit „Use when …"-Trigger):
|
- [x] **Step 2: Skills schreiben** — jeweils `SKILL.md` mit Frontmatter (`name`, `description` mit „Use when …"-Trigger):
|
||||||
- `finanz-api`: Beschreibung aller Endpunkt-Gruppen (accounts, transactions, categories, category-rules, imports, recurring, planned, loans, scenarios, project) mit je einem konkreten curl-Beispiel, Muster:
|
- `finanz-api`: Beschreibung aller Endpunkt-Gruppen (accounts, transactions, categories, category-rules, imports, recurring, planned, loans, scenarios, project) mit je einem konkreten curl-Beispiel, Muster:
|
||||||
```bash
|
```bash
|
||||||
KEY=$(grep '^FB_API_KEY=' /home/wlfb/bin/finance/.env | cut -d= -f2)
|
KEY=$(grep '^FB_API_KEY=' /home/wlfb/bin/finance/.env | cut -d= -f2)
|
||||||
@@ -2015,7 +2015,7 @@ curl -s -H "Authorization: Bearer $KEY" http://127.0.0.1:8096/api/accounts | jq
|
|||||||
- `finanzberatung`: Workflow — (1) Lage erfassen: Konten/Salden, unkategorisierte Buchungen, gepflegte wiederkehrende Posten prüfen (ggf. `/api/recurring/suggestions` durchgehen), (2) Basis-Szenario anlegen/durchrechnen, (3) Fragestellung in Varianten übersetzen (Kredit-Parameter, Kürzungs-Modifikatoren je Kategorie), (4) Varianten durchrechnen, (5) Vergleichstabelle (Tiefpunkt, Unterschreitungsdaten, Gesamtkosten Kredit aus Tilgungsplan) und Empfehlung formulieren; Hinweis auf Grafana-Dashboard „Szenario-Vergleich" für den Nutzer.
|
- `finanzberatung`: Workflow — (1) Lage erfassen: Konten/Salden, unkategorisierte Buchungen, gepflegte wiederkehrende Posten prüfen (ggf. `/api/recurring/suggestions` durchgehen), (2) Basis-Szenario anlegen/durchrechnen, (3) Fragestellung in Varianten übersetzen (Kredit-Parameter, Kürzungs-Modifikatoren je Kategorie), (4) Varianten durchrechnen, (5) Vergleichstabelle (Tiefpunkt, Unterschreitungsdaten, Gesamtkosten Kredit aus Tilgungsplan) und Empfehlung formulieren; Hinweis auf Grafana-Dashboard „Szenario-Vergleich" für den Nutzer.
|
||||||
- `auszug-import`: Workflow — `GET /api/imports` auf `error`/`draft` prüfen; bei Drafts Vorschau dem Nutzer zusammenfassen und nach Bestätigung `confirm` aufrufen; bei Parser-Fehlern: PDF aus Inbox mit `finance/scripts/dump_pdf_text.py` ansehen, Regex-Konstanten im passenden Parser (`/home/wlfb/bin/finance/app/parsers/`) fixen, Tests laufen lassen, Image neu bauen + `create_pod_finance.sh` erneut ausführen.
|
- `auszug-import`: Workflow — `GET /api/imports` auf `error`/`draft` prüfen; bei Drafts Vorschau dem Nutzer zusammenfassen und nach Bestätigung `confirm` aufrufen; bei Parser-Fehlern: PDF aus Inbox mit `finance/scripts/dump_pdf_text.py` ansehen, Regex-Konstanten im passenden Parser (`/home/wlfb/bin/finance/app/parsers/`) fixen, Tests laufen lassen, Image neu bauen + `create_pod_finance.sh` erneut ausführen.
|
||||||
|
|
||||||
- [ ] **Step 3: Commit im fb-Repo**
|
- [x] **Step 3: Commit im fb-Repo**
|
||||||
```bash
|
```bash
|
||||||
cd /home/wlfb/fb && git add CLAUDE.md .claude/skills && \
|
cd /home/wlfb/fb && git add CLAUDE.md .claude/skills && \
|
||||||
git commit -m "feat: Beratungsumgebung (CLAUDE.md, Skills finanz-api/finanzberatung/auszug-import)"
|
git commit -m "feat: Beratungsumgebung (CLAUDE.md, Skills finanz-api/finanzberatung/auszug-import)"
|
||||||
@@ -2025,7 +2025,7 @@ cd /home/wlfb/fb && git add CLAUDE.md .claude/skills && \
|
|||||||
|
|
||||||
## Abschluss-Checkliste
|
## Abschluss-Checkliste
|
||||||
|
|
||||||
- [ ] Alle Tests grün: `cd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q` (Fixture-Tests ggf. SKIP, sobald PDFs da sind: PASS Pflicht)
|
- [x] Alle Tests grün: `cd /home/wlfb/bin/finance && .venv/bin/python -m pytest -q` (Fixture-Tests ggf. SKIP, sobald PDFs da sind: PASS Pflicht)
|
||||||
- [ ] `pod-finance_pod.service` aktiv und `enabled`; GUI, API (mit Key) und Grafana erreichbar (Smoke-Test aus Task 16 Step 3 komplett durchlaufen)
|
- [x] `pod-finance_pod.service` aktiv und `enabled`; GUI, API (mit Key) und Grafana erreichbar (Smoke-Test aus Task 16 Step 3 komplett durchlaufen)
|
||||||
- [ ] Kein Secret, keine PDF, keine echten Kontodaten in `git log -p` beider Repos
|
- [x] Kein Secret, keine PDF, keine echten Kontodaten in `git log -p` beider Repos
|
||||||
- [ ] Beide Repos committet (`~/bin`: Tool + Plan-Häkchen, `~/fb`: Umgebung)
|
- [x] Beide Repos committet (`~/bin`: Tool + Plan-Häkchen, `~/fb`: Umgebung)
|
||||||
|
|||||||
Reference in New Issue
Block a user