_set_identifiers glich die Liste per clear()+neu-Anlegen ab; SQLAlchemy fügte dabei die (unveränderten) Identifikatoren vor dem Löschen ein und verletzte so UNIQUE(account_id, kind, value_norm) -> Internal Server Error beim Speichern. Jetzt als Diff: unveränderte behalten, nur fehlende hinzufügen / überzählige entfernen. Betrifft alle Identifikator-Arten (IBAN/Nummer/Text). Regressionstest: Umbenennen mit unverändertem Identifikator + Entfernen (76 grün). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
4.0 KiB
Python
105 lines
4.0 KiB
Python
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_update_account_keeps_identifier(admin):
|
|
"""Umbenennen mit unverändertem Identifikator darf nicht an der UNIQUE-
|
|
Bedingung scheitern (Regression: früheres clear()+neu-Anlegen)."""
|
|
acc = _mk_account(admin, name="Revolut CHF",
|
|
identifiers=[{"kind": "text", "value": "Revolut**5064*"}])
|
|
r = admin.patch(f"/api/accounts/{acc['id']}", json={
|
|
"name": "Revolut CHF neu",
|
|
"identifiers": [{"kind": "text", "value": "Revolut**5064*"}],
|
|
})
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["name"] == "Revolut CHF neu"
|
|
idents = r.json()["identifiers"]
|
|
assert len(idents) == 1 and idents[0]["value"] == "Revolut**5064*"
|
|
|
|
# Identifikator entfernen klappt ebenfalls.
|
|
r2 = admin.patch(f"/api/accounts/{acc['id']}", json={"name": "Ohne", "identifiers": []})
|
|
assert r2.status_code == 200, r2.text
|
|
assert r2.json()["identifiers"] == []
|
|
|
|
|
|
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
|