from __future__ import annotations import argparse import os from datetime import date, datetime from pathlib import Path from typing import Dict, Iterable import psycopg2 import psycopg2.extras from dotenv import load_dotenv import requests from uv_app.core.mssql import connect_to_mssql from uv_app.core.pgsql import connect_to_pgsql DOTENV_PATH = Path(__file__).resolve().parents[2] / ".env" load_dotenv(DOTENV_PATH, override=True) QUERY_PATH = Path(__file__).with_name("multiple_contract_user_cleanup.sql") BASE_URL = "https://api.manorivile.lt/client/v2" TIMEOUT = 30 def _read_query() -> str: return QUERY_PATH.read_text(encoding="utf-8") def _fetch_rows() -> Iterable[Dict[str, object]]: conn = connect_to_pgsql() if conn is None: raise RuntimeError("Failed to connect to PostgreSQL.") try: with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor: cursor.execute(_read_query()) return cursor.fetchall() finally: conn.close() def _normalize_date(value: object) -> date: if isinstance(value, date): return value if isinstance(value, datetime): return value.date() return date.min def _pick_primary_sutartis(rows: list[Dict[str, object]]) -> Dict[str, object]: rows_sorted = sorted( rows, key=lambda r: ( _normalize_date(r.get("data")), str(r.get("sutartiesid") or ""), ), ) return rows_sorted[0] def _parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument( "--delete-rivile", action="store_true", help="Delete duplicate Rivile clients.", ) return parser.parse_args() def _get_api_key() -> str: api_key = os.getenv("RIVILE_API_KEY", "").strip() if not api_key: raise RuntimeError("Missing RIVILE_API_KEY environment variable.") return api_key def _post(api_key: str, payload: dict) -> dict: headers = { "ApiKey": api_key, "Content-Type": "application/json", "Accept": "application/json", } response = requests.post( BASE_URL, json=payload, headers=headers, timeout=TIMEOUT, ) if response.status_code != 200: raise RuntimeError(f"Rivile HTTP {response.status_code}: {response.text}") data = response.json() if "errorMessage" in data: raise RuntimeError(f"Rivile API error: {data}") return data def _delete_rivile_client(api_key: str, client_code: str) -> None: payload = { "method": "EDIT_N08", "params": {"oper": "D", "user": api_key.split(".", 1)[0]}, "data": {"N08": {"N08_KODAS_KS": client_code}}, } _post(api_key, payload) def _update_rivile_client_code(api_key: str, old_code: str, new_code: str) -> None: payload = { "method": "EDIT_N08", "params": { "oper": "U", "user": api_key.split(".", 1)[0], "fld": "N08_KODAS_KS", "val": old_code, }, "data": {"N08": {"N08_KODAS_KS": new_code}}, } _post(api_key, payload) def main() -> None: args = _parse_args() rows = list(_fetch_rows()) if not rows: print("No multi-contract clients found.") return by_client: dict[int, list[Dict[str, object]]] = {} for row in rows: by_client.setdefault(int(row["klientasid"]), []).append(row) mssql_conn = connect_to_mssql() if mssql_conn is None: raise RuntimeError("Failed to connect to MSSQL.") processed = 0 api_key = _get_api_key() if args.delete_rivile else "" try: for klientasid, items in by_client.items(): processed += 1 primary = _pick_primary_sutartis(items) target_code = str(primary.get("sutartiesid") or "").strip() current_code = str(primary.get("kodas") or "").strip() sutarties_ids = [ str(item.get("sutartiesid") or "").strip() for item in items ] sutarties_ids = [s for s in sutarties_ids if s] print("=" * 80) print(f"klientasid={klientasid}") print(f" PG klientas.kodas={current_code}") print(f" PG sutartiesid list={sutarties_ids}") print(f" earliest sutartiesid={target_code}") actions: list[str] = [] if not target_code: print(" -> Skipping: missing sutartiesid.") print(" -> Actions: skip (missing sutartiesid)") continue # Rivile (MSSQL) checks cursor = mssql_conn.cursor() placeholders = ",".join("?" for _ in sutarties_ids) or "?" params = sutarties_ids or [target_code] cursor.execute( f""" SELECT N08_KODAS_KS FROM dbo.N08_KLIJ WHERE N08_KODAS_KS IN ({placeholders}) """, params, ) rivile_clients = [str(row[0]).strip() for row in cursor.fetchall()] print(f" Rivile clients found={rivile_clients}") if len(rivile_clients) > 1: print(" -> ERROR: multiple Rivile clients for one klientasid.") elif not rivile_clients: print(" -> WARNING: no Rivile client found for these codes.") if target_code in rivile_clients: print(" -> OK: earliest sutartiesid is present in Rivile.") duplicates = [c for c in rivile_clients if c != target_code] if duplicates: print(f" -> Duplicate Rivile clients to delete: {duplicates}") if args.delete_rivile: for dup in duplicates: _delete_rivile_client(api_key, dup) print(f" Deleted Rivile client {dup}") actions.append(f"delete {dup}") else: print(" (dry-run: not deleted)") actions.append(f"would delete {duplicates}") else: print(" -> WARNING: earliest sutartiesid not found in Rivile.") if args.delete_rivile: if rivile_clients: keep_code = rivile_clients[0] print( f" -> Renaming Rivile client {keep_code} -> {target_code}" ) _update_rivile_client_code(api_key, keep_code, target_code) actions.append(f"rename {keep_code} -> {target_code}") duplicates = [c for c in rivile_clients if c != keep_code] if duplicates: print( f" -> Deleting remaining duplicates: {duplicates}" ) for dup in duplicates: _delete_rivile_client(api_key, dup) print(f" Deleted Rivile client {dup}") actions.append(f"delete {dup}") else: print(" -> Cannot update: no Rivile client to rename.") actions.append("no rivile client to rename") elif rivile_clients: print( " -> (dry-run) Would rename one client to earliest and delete the rest." ) actions.append("would rename one client to earliest and delete the rest") if current_code == target_code: print(f" -> PG klientas.kodas already matches earliest sutartiesid.") else: print(f" -> Needs update: {current_code} -> {target_code}") actions.append(f"pg differs {current_code}->{target_code} (no pg write)") if not actions: actions = ["no changes"] print(f" -> Actions: {', '.join(actions)}") print(f"Processed klientasid with multiple contracts: {processed}") finally: mssql_conn.close() print(f"Processed klientasid with multiple contracts: {processed}") if __name__ == "__main__": main()