feat: Phase 1 – Konten, Kategorien, Transaktionen, EZB-Währungsumrechnung
Backend: - Modelle + Migration 0002: accounts, account_identifiers, category_groups, categories, transactions, exchange_rates (alle owner-gescoped). - EZB-Kursdienst (90-Tage/Historie) + Backfill-Skript; Umrechnung EUR<->CHF zum Buchungsdatum mit lokalem Cache und manueller Übersteuerung; EUR-Pivot (amount_eur_cents) eingefroren. - Kontostände + Netto-Vermögen (Umrechnung in Hauptwährung). - Router: accounts (inkl. IBAN-Identifikatoren), categories (Gruppen+Kategorien), transactions (Ausgabe/Einnahme/Transfer, cross-currency), rates. - Gruppierte Standard-Kategorien je Nutzer beim Anlegen. - pytest-Suite: Währung, Salden, Cross-Currency-Transfer, Nutzer-Isolation. Frontend: - Seiten Konten, Kategorien, Buchungen; Icon-Chip, Geldbetrag mit Umrechnung, Icon-/Farbwähler, Modal; erweitertes SVG-Icon-Set (Konto-/Kategorie-Icons). - Währungs-/Datumsformatierung (EUR de-DE, CHF de-CH), i18n DE/EN erweitert. - Live-Umrechnungs-Vorschau im Transfer-Formular. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
def _seed_recent_rates(rate: str = "0.95") -> None:
|
||||
from app.database import SessionLocal
|
||||
from app.models import ExchangeRate
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
today = date.today()
|
||||
for delta in range(0, 7):
|
||||
day = today - timedelta(days=delta)
|
||||
if db.get(ExchangeRate, (day, "CHF")) is None:
|
||||
db.add(ExchangeRate(date=day, currency="CHF", rate_per_eur=Decimal(rate)))
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _mk_account(admin, **kw):
|
||||
body = {"name": "A", "type": "girokonto", "currency": "EUR", "opening_balance_cents": 0}
|
||||
body.update(kw)
|
||||
r = admin.post("/api/accounts", json=body)
|
||||
assert r.status_code == 201, r.text
|
||||
return r.json()
|
||||
|
||||
|
||||
def test_starter_categories_present(admin):
|
||||
groups = admin.get("/api/categories").json()
|
||||
assert len(groups) >= 6
|
||||
names = {c["name"] for g in groups for c in g["categories"]}
|
||||
assert {"Gehalt", "Lebensmittel", "Sparen & Investments"} <= names
|
||||
|
||||
|
||||
def test_currency_validation(admin):
|
||||
r = admin.post("/api/accounts", json={"name": "X", "currency": "USD"})
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_identifiers_stored_and_deduped(admin):
|
||||
acc = _mk_account(
|
||||
admin,
|
||||
name="Mit IBAN",
|
||||
identifiers=[
|
||||
{"kind": "iban", "value": "DE12 3456"},
|
||||
{"kind": "iban", "value": "de123456"}, # normiert identisch -> dedupliziert
|
||||
{"kind": "card_no", "value": "1234"},
|
||||
],
|
||||
)
|
||||
kinds = sorted((i["kind"], i["value"]) for i in acc["identifiers"])
|
||||
assert len(acc["identifiers"]) == 2
|
||||
assert ("card_no", "1234") in kinds
|
||||
|
||||
|
||||
def test_balance_and_cross_currency_transfer(admin):
|
||||
_seed_recent_rates("0.95")
|
||||
d = date.today()
|
||||
eur = _mk_account(admin, name="Giro", currency="EUR", opening_balance_cents=100000)
|
||||
chf = _mk_account(admin, name="Spar", type="sparkonto", currency="CHF", opening_balance_cents=0)
|
||||
|
||||
admin.post("/api/transactions", json={"type": "income", "date": str(d), "account_id": eur["id"], "amount_cents": 50000})
|
||||
admin.post("/api/transactions", json={"type": "expense", "date": str(d), "account_id": eur["id"], "amount_cents": 20000})
|
||||
assert admin.get(f"/api/accounts/{eur['id']}").json()["balance_cents"] == 130000
|
||||
|
||||
# Transfer 100 EUR -> CHF (0.95) = 95 CHF gutgeschrieben.
|
||||
r = admin.post("/api/transactions", json={
|
||||
"type": "transfer", "date": str(d), "account_id": eur["id"],
|
||||
"to_account_id": chf["id"], "amount_cents": 10000,
|
||||
})
|
||||
assert r.status_code == 201, r.text
|
||||
assert r.json()["to_amount_cents"] == 9500
|
||||
assert admin.get(f"/api/accounts/{eur['id']}").json()["balance_cents"] == 120000
|
||||
assert admin.get(f"/api/accounts/{chf['id']}").json()["balance_cents"] == 9500
|
||||
|
||||
|
||||
def test_transfer_requires_other_account(admin):
|
||||
eur = _mk_account(admin, name="Solo", currency="EUR")
|
||||
d = date.today()
|
||||
r = admin.post("/api/transactions", json={
|
||||
"type": "transfer", "date": str(d), "account_id": eur["id"],
|
||||
"to_account_id": eur["id"], "amount_cents": 100,
|
||||
})
|
||||
assert r.status_code == 400
|
||||
Reference in New Issue
Block a user