"""Status-Engine: computes chain status based on KONZEPT.md section 6.""" from __future__ import annotations import sqlite3 from datetime import date, timedelta # Threshold: a Vorlage is considered "old" after this many days without activity VERSANDET_TAGE = 365 def compute_status( conn: sqlite3.Connection, ursprung_id: int, chain_typ: str, members: list[sqlite3.Row], ) -> dict: """Compute the overall status for a chain. Returns dict with keys: status, status_seit, vertagungen_count """ if chain_typ == "anfrage": return _status_anfrage(conn, ursprung_id, members) elif chain_typ == "antrag": return _status_antrag(conn, ursprung_id, members) return {"status": "unbekannt", "status_seit": None, "vertagungen_count": 0} def _status_anfrage( conn: sqlite3.Connection, ursprung_id: int, members: list[sqlite3.Row], ) -> dict: """Status logic for Anfragen (KONZEPT.md 6.1). angefragt: Keine Stellungnahme, <1 Jahr beantwortet: Stellungnahme + KI-Match >=0.7 + Kenntnisnahme offen: Stellungnahme da, aber keine Kenntnisnahme abgewiegelt: Stellungnahme + KI-Match <0.5 versandet: Keine Antwort, >1 Jahr zurückgezogen: Explizit zurückgezogen """ heute = date.today() ursprung_datum = _parse_date(members[0]["datum_eingang"]) # Check for Stellungnahme in chain members stellungnahmen = [m for m in members if m["typ"] == "stellungnahme"] has_stellungnahme = len(stellungnahmen) > 0 # Check for Kenntnisnahme in Beratungen beratungen = conn.execute(""" SELECT rolle, ergebnis, sitzung_datum FROM beratungen WHERE vorlage_id = ? ORDER BY sitzung_datum DESC """, (ursprung_id,)).fetchall() has_kenntnisnahme = any( b["rolle"] and "kenntnisnahme" in b["rolle"].lower() for b in beratungen ) # Check KI-Match score for Antwort ki_score = _get_ki_score(conn, ursprung_id, "antwort_match") # Check zurückgezogen if _is_zurueckgezogen(beratungen): return {"status": "zurückgezogen", "status_seit": _latest_date(beratungen), "vertagungen_count": 0} if has_stellungnahme: if ki_score is not None and ki_score < 0.5: return {"status": "abgewiegelt", "status_seit": _vorlage_date(stellungnahmen[0]), "vertagungen_count": 0} if has_kenntnisnahme and (ki_score is None or ki_score >= 0.7): return {"status": "beantwortet", "status_seit": _vorlage_date(stellungnahmen[0]), "vertagungen_count": 0} return {"status": "offen", "status_seit": _vorlage_date(stellungnahmen[0]), "vertagungen_count": 0} # No Stellungnahme if ursprung_datum and (heute - ursprung_datum).days > VERSANDET_TAGE: return {"status": "versandet", "status_seit": ursprung_datum, "vertagungen_count": 0} return {"status": "angefragt", "status_seit": ursprung_datum, "vertagungen_count": 0} def _status_antrag( conn: sqlite3.Connection, ursprung_id: int, members: list[sqlite3.Row], ) -> dict: """Status logic for Anträge (KONZEPT.md 6.2). eingereicht: Neu, noch keine Beratung in_beratung: Mindestens eine Beratung ohne Endbeschluss vertagt: Letzte Beratung = vertagt verwiesen: An anderen Ausschuss überwiesen beschlossen: Angenommen, <1 Jahr, kein Umsetzungsbericht umgesetzt: Umsetzungsbericht + KI-Match >=0.7 teilweise_umgesetzt: Umsetzungsbericht + KI-Match 0.4-0.7 abgelehnt: Beschluss = abgelehnt abgewiegelt: Beschlossen + Bericht + KI-Match <0.4 versandet: Beschlossen, >1 Jahr, kein Bericht zurückgezogen: Explizit zurückgezogen """ heute = date.today() ursprung_datum = _parse_date(members[0]["datum_eingang"]) beratungen = conn.execute(""" SELECT rolle, ergebnis, sitzung_datum FROM beratungen WHERE vorlage_id = ? ORDER BY sitzung_datum DESC NULLS LAST """, (ursprung_id,)).fetchall() # Count Vertagungen vertagungen = sum(1 for b in beratungen if b["ergebnis"] and "vertagt" in b["ergebnis"].lower()) # Check zurückgezogen if _is_zurueckgezogen(beratungen): return {"status": "zurückgezogen", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen} # Check for Berichte in chain berichte = [m for m in members if m["typ"] == "bericht"] has_bericht = len(berichte) > 0 # Determine beschluss from beratungen beschluss = _get_beschluss(beratungen) if beschluss == "abgelehnt": return {"status": "abgelehnt", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen} if beschluss == "angenommen": beschluss_datum = _latest_date(beratungen) if has_bericht: ki_score = _get_ki_score(conn, ursprung_id, "umsetzung_match") bericht_datum = _vorlage_date(berichte[-1]) if ki_score is not None: if ki_score >= 0.7: return {"status": "umgesetzt", "status_seit": bericht_datum, "vertagungen_count": vertagungen} elif ki_score >= 0.4: return {"status": "teilweise_umgesetzt", "status_seit": bericht_datum, "vertagungen_count": vertagungen} else: return {"status": "abgewiegelt", "status_seit": bericht_datum, "vertagungen_count": vertagungen} return {"status": "umgesetzt", "status_seit": bericht_datum, "vertagungen_count": vertagungen} # Angenommen but no Bericht if beschluss_datum and (heute - beschluss_datum).days > VERSANDET_TAGE: return {"status": "versandet", "status_seit": beschluss_datum, "vertagungen_count": vertagungen} return {"status": "beschlossen", "status_seit": beschluss_datum, "vertagungen_count": vertagungen} if beschluss == "verwiesen": return {"status": "verwiesen", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen} # No final decision yet if beratungen: last = beratungen[0] if last["ergebnis"] and "vertagt" in last["ergebnis"].lower(): return {"status": "vertagt", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen} return {"status": "in_beratung", "status_seit": _latest_date(beratungen), "vertagungen_count": vertagungen} # No beratungen at all return {"status": "eingereicht", "status_seit": ursprung_datum, "vertagungen_count": vertagungen} # --- Helpers --- def _parse_date(val: str | None) -> date | None: if not val: return None try: return date.fromisoformat(val) except (ValueError, TypeError): return None def _vorlage_date(member: sqlite3.Row) -> date | None: return _parse_date(member["datum_eingang"]) def _latest_date(beratungen: list[sqlite3.Row]) -> date | None: dates = [_parse_date(b["sitzung_datum"]) for b in beratungen if b["sitzung_datum"]] return max(dates) if dates else None def _get_ki_score(conn: sqlite3.Connection, vorlage_id: int, typ: str) -> float | None: row = conn.execute(""" SELECT score FROM ki_bewertungen WHERE vorlage_id = ? AND typ = ? ORDER BY erstellt_at DESC LIMIT 1 """, (vorlage_id, typ)).fetchone() return row["score"] if row else None def _is_zurueckgezogen(beratungen: list[sqlite3.Row]) -> bool: return any( b["ergebnis"] and "zurückgezogen" in b["ergebnis"].lower() for b in beratungen ) def _get_beschluss(beratungen: list[sqlite3.Row]) -> str | None: """Determine the final decision from Beratungen. Looks for Entscheidung-role beratungen with a result. """ for b in beratungen: ergebnis = (b["ergebnis"] or "").lower() rolle = (b["rolle"] or "").lower() if "abgelehnt" in ergebnis: return "abgelehnt" if "verwiesen" in ergebnis: return "verwiesen" if any(kw in ergebnis for kw in ("angenommen", "empfohlen", "beschlossen", "zugestimmt")): return "angenommen" # If rolle is Entscheidung and there's any ergebnis, it's likely a decision if "entscheidung" in rolle and ergebnis and "vertagt" not in ergebnis: return "angenommen" return None