From c302a914111a7e01d3935b712613998ebfe3f2ae Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Thu, 26 Feb 2026 17:09:50 +0000 Subject: [PATCH] feat: auto-update fall_id when KVNR is entered on a case When a KVNR is entered on a case that has a random-suffix or legacy Nachname-based fall_id, the fall_id is automatically rebuilt using the new KVNR. Co-Authored-By: Claude Opus 4.6 --- backend/app/api/cases.py | 11 +++++++++-- backend/app/services/import_service.py | 23 +++++++++++++++++++++++ backend/tests/test_import.py | 23 +++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/backend/app/api/cases.py b/backend/app/api/cases.py index 08268ad..fc34994 100644 --- a/backend/app/api/cases.py +++ b/backend/app/api/cases.py @@ -24,6 +24,7 @@ from app.schemas.case import ( from app.schemas.disclosure import DisclosureRequestCreate, DisclosureRequestResponse from app.config import get_settings from app.services.audit_service import log_action +from app.services.import_service import has_random_suffix from app.services.disclosure_service import ( create_disclosure_request, has_active_disclosure, has_pending_request, ) @@ -433,6 +434,12 @@ def set_case_kvnr( old_kvnr = case.kvnr new_kvnr = payload.get("kvnr") case.kvnr = new_kvnr + + # Auto-update fall_id if it currently has a non-KVNR suffix + old_fall_id = case.fall_id + if new_kvnr and case.fall_id and has_random_suffix(case.fall_id): + case.fall_id = f"{case.jahr}-{case.kw:02d}-{case.fallgruppe}-{new_kvnr}" + case.updated_by = user.id db.commit() db.refresh(case) @@ -443,8 +450,8 @@ def set_case_kvnr( action="kvnr_updated", entity_type="case", entity_id=case.id, - old_values={"kvnr": old_kvnr}, - new_values={"kvnr": new_kvnr}, + old_values={"kvnr": old_kvnr, "fall_id": old_fall_id}, + new_values={"kvnr": new_kvnr, "fall_id": case.fall_id}, ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent"), ) diff --git a/backend/app/services/import_service.py b/backend/app/services/import_service.py index 5f4eba7..8359dd7 100644 --- a/backend/app/services/import_service.py +++ b/backend/app/services/import_service.py @@ -9,6 +9,7 @@ Handles: import logging import random +import re import string from sqlalchemy.orm import Session @@ -41,6 +42,28 @@ def generate_fall_id(parsed: ParsedCase) -> str: return f"{parsed.jahr}-{parsed.kw:02d}-{parsed.fallgruppe}-{suffix}" +# KVNR format: letter followed by 9 digits (e.g. A123456789) +KVNR_PATTERN = re.compile(r'^[A-Z]\d{9}$') + + +def has_random_suffix(fall_id: str | None) -> bool: + """Check if a fall_id ends with a non-KVNR suffix. + + Returns True for random suffixes (6-char alphanumeric) and legacy + Nachname-based suffixes. Returns False for KVNR suffixes. + """ + if not fall_id: + return False + parts = fall_id.rsplit("-", 1) + if len(parts) < 2: + return False + suffix = parts[1] + # If it matches KVNR pattern, it's NOT a random suffix + if KVNR_PATTERN.match(suffix): + return False + return True + + def check_duplicate(db: Session, parsed: ParsedCase) -> bool: """Check if a case already exists in the database. diff --git a/backend/tests/test_import.py b/backend/tests/test_import.py index 7cd443a..ec88673 100644 --- a/backend/tests/test_import.py +++ b/backend/tests/test_import.py @@ -17,6 +17,7 @@ from app.services.import_service import ( confirm_import, generate_fall_id, generate_random_suffix, + has_random_suffix, preview_import, ) @@ -354,3 +355,25 @@ class TestPreviewImportMocked: assert result.rows[0].row_number == 1 assert result.rows[1].row_number == 2 assert result.rows[2].row_number == 3 + + +# ── has_random_suffix tests ─────────────────────────────────────────── + + +class TestHasRandomSuffix: + def test_kvnr_suffix(self): + """Fall_id with KVNR is NOT random.""" + assert has_random_suffix("2026-06-onko-A123456789") is False + + def test_random_suffix(self): + """Fall_id with 6-char random suffix IS random.""" + assert has_random_suffix("2026-06-onko-X7K9M2") is True + + def test_legacy_nachname_suffix(self): + """Fall_id with legacy Nachname suffix IS treated as non-KVNR.""" + assert has_random_suffix("2020-32-onko-Bartl-Zimmermann") is True + assert has_random_suffix("2026-06-kardio-Tonn") is True + + def test_empty_fall_id(self): + assert has_random_suffix("") is False + assert has_random_suffix(None) is False