From 1f03483e5d2e24db264e96427968f8af84f456ac Mon Sep 17 00:00:00 2001 From: Guido Date: Thu, 20 Aug 2026 20:00:09 +0200 Subject: [PATCH] fix: Konto umbenennen scheitert nicht mehr an Identifikator-UNIQUE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _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 --- backend/app/routers/accounts.py | 31 ++++++++++++++++++++++--------- backend/tests/test_accounts.py | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/backend/app/routers/accounts.py b/backend/app/routers/accounts.py index 6a483c6..e057cc8 100644 --- a/backend/app/routers/accounts.py +++ b/backend/app/routers/accounts.py @@ -50,18 +50,31 @@ def to_out(db: Session, acc: Account, base: str, on: date) -> AccountOut: def _set_identifiers(acc: Account, items) -> None: - """Ersetzt die Identifikator-Liste (dedupliziert nach Art + Normalform).""" - acc.identifiers.clear() - seen: set[tuple[str, str]] = set() + """Gleicht die Identifikator-Liste auf den Ziel-Zustand ab (dedupliziert nach + Art + Normalform). Bewusst als Diff (unveränderte behalten, nur fehlende + hinzufügen / überzählige entfernen): ein einfaches clear()+neu-Anlegen würde + beim Speichern zuerst einfügen und dann löschen und so die UNIQUE-Bedingung + (account_id, kind, value_norm) verletzen (Fehler beim Umbenennen).""" + desired: dict[tuple[str, str], tuple] = {} for it in items: norm = normalize_identifier(it.value, it.kind) - key = (it.kind.value, norm) - if not norm or key in seen: + if not norm: continue - seen.add(key) - acc.identifiers.append( - AccountIdentifier(kind=it.kind, value=it.value.strip(), value_norm=norm) - ) + key = (it.kind.value, norm) + desired.setdefault(key, (it.kind, it.value.strip(), norm)) + + keep: set[tuple[str, str]] = set() + for existing in list(acc.identifiers): + key = (existing.kind.value, existing.value_norm) + if key in desired: + keep.add(key) + existing.value = desired[key][1] # Original-Schreibweise ggf. angleichen + else: + acc.identifiers.remove(existing) + + for key, (kind, value, norm) in desired.items(): + if key not in keep: + acc.identifiers.append(AccountIdentifier(kind=kind, value=value, value_norm=norm)) @router.get("", response_model=list[AccountOut]) diff --git a/backend/tests/test_accounts.py b/backend/tests/test_accounts.py index 27175ac..4df286f 100644 --- a/backend/tests/test_accounts.py +++ b/backend/tests/test_accounts.py @@ -53,6 +53,26 @@ def test_identifiers_stored_and_deduped(admin): 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()