""" Access to the results table (`dbo.CV_ChargeSummary`, written by src/ecw_bot/core/results_db.py): SELECTs, and one DELETE — a whole run, the Super Admin's Delete run (2026-10-28). Always parameterized; one short connection per call. """ from datetime import date, datetime, timedelta from ecw_bot.core import results_db # started_by since 2026-10-22 (EEWA-33): a table without it is reported as # "needs tools/sql/create_tables.sql" (web/app.py, SQLSTATE 42S22). _SELECT_COLUMNS = ", ".join(["id", "run_at", "run_mode", "started_by"] + [c for _, c in results_db.COLUMNS] + ["details_json"]) _RUN_COUNTS = ( "run_at, MAX(run_mode) AS run_mode, COUNT(*) AS claims, " "SUM(CASE WHEN category = 'Pass' THEN 1 ELSE 0 END) AS pass_count, " "SUM(CASE WHEN category = 'Fail' THEN 1 ELSE 0 END) AS fail_count, " "SUM(CASE WHEN category = 'Review' THEN 1 ELSE 0 END) AS review_count") # The same counts per day a run STARTED: every row of a run shares its # run_at, so a run never splits over two days (portal, 2026-10-19). _DAY = "CAST(run_at AS DATE)" _DAY_COUNTS = ( f"{_DAY} AS day, COUNT(*) AS claims, COUNT(DISTINCT run_at) AS runs, " "SUM(CASE WHEN category = 'Pass' THEN 1 ELSE 0 END) AS pass_count, " "SUM(CASE WHEN category = 'Fail' THEN 1 ELSE 0 END) AS fail_count, " "SUM(CASE WHEN category = 'Review' THEN 1 ELSE 0 END) AS review_count") def _coerce(row: dict) -> dict: """ run_at as a datetime whatever the driver: ODBC Driver 17/18 return DATETIME2 as datetime, the legacy {SQL Server} driver as text ("2026-10-08 15:10:00.0000000"), which the page cannot compare or format (review, 2026-10-12). """ value = row.get("run_at") if isinstance(value, str): try: row["run_at"] = datetime.fromisoformat(value.strip()[:26]) except ValueError: pass # The same for a day (DATE), which that driver returns as "2026-10-16". value = row.get("day") if isinstance(value, str): try: row["day"] = date.fromisoformat(value.strip()[:10]) except ValueError: pass elif isinstance(value, datetime): row["day"] = value.date() return row class SqlResultsRepo: def __init__(self, conn_str: str, connect=None): self.conn_str = conn_str self._connect = connect def _open(self): connect = self._connect if connect is None: import pyodbc connect = pyodbc.connect return connect(self.conn_str, timeout=10) def _query(self, sql: str, params=()) -> list: conn = self._open() try: cur = conn.cursor().execute(sql, list(params)) cols = [d[0] for d in cur.description] return [_coerce(dict(zip(cols, row))) for row in cur.fetchall()] finally: conn.close() def delete_run(self, run_at) -> int: """Every row of the run that started at `run_at` (every row of a run shares it); how many went.""" conn = self._open() try: cur = conn.cursor().execute( f"DELETE FROM {results_db.TABLE} WHERE run_at = ?", [run_at]) gone = cur.rowcount conn.commit() return gone finally: conn.close() def runs(self) -> list: """Every run, newest first, with its claim count per Category.""" return self._query( f"SELECT TOP 200 {_RUN_COUNTS} FROM {results_db.TABLE} " f"GROUP BY run_at ORDER BY run_at DESC") def results(self, run_at, category=None, claim_no=None) -> list: """One run's rows, in S.No order, optionally filtered.""" sql = (f"SELECT {_SELECT_COLUMNS} FROM {results_db.TABLE} " f"WHERE run_at = ?") params = [run_at] if category: sql += " AND category = ?" params.append(category) if claim_no: sql += " AND claim_no LIKE ?" params.append(f"%{claim_no}%") sql += " ORDER BY s_no, id" return self._query(sql, params) def days(self, limit: int = 90) -> list: """The last `limit` days with a run, newest first: claims checked (every row — a claim checked twice counts twice), runs, and the count per Category.""" return self._query( f"SELECT TOP {int(limit)} {_DAY_COUNTS} FROM {results_db.TABLE} " f"GROUP BY {_DAY} ORDER BY {_DAY} DESC") def day_results(self, day) -> list: """Every row of every run that started on `day`, in run order then S.No. The page counts and filters them itself: its tiles need the whole day whatever filter is on.""" start = datetime(day.year, day.month, day.day) return self._query( f"SELECT {_SELECT_COLUMNS} FROM {results_db.TABLE} " f"WHERE run_at >= ? AND run_at < ? ORDER BY run_at, s_no, id", [start, start + timedelta(days=1)]) def claim_history(self, claim_no) -> list: """Every saved check of the claims matching `claim_no`, newest first (at most 200).""" return self._query( f"SELECT TOP 200 {_SELECT_COLUMNS} FROM {results_db.TABLE} " f"WHERE claim_no LIKE ? ORDER BY run_at DESC, s_no, id", [f"%{claim_no}%"]) # ── Run events (2026-10-30): the Runs page on a server Portal ───────── def run_ids(self) -> list: """Every Run with events, newest first (at most 500).""" return [r["run_id"] for r in self._query( "SELECT TOP 500 run_id FROM dbo.CV_RunEvent " "GROUP BY run_id ORDER BY run_id DESC")] def run_event_lines(self, run_ids) -> list: """(run_id, JSON line) for those Runs, in the order written.""" run_ids = list(run_ids)[:500] if not run_ids: return [] marks = ", ".join("?" * len(run_ids)) return [(r["run_id"], r["body"]) for r in self._query( f"SELECT run_id, body FROM dbo.CV_RunEvent " f"WHERE run_id IN ({marks}) ORDER BY run_id, id", run_ids)]