From 891ae221e08eee8ceb0023e89f85f1d646d262e5 Mon Sep 17 00:00:00 2001 From: alzyras Date: Mon, 2 Feb 2026 18:37:50 +0200 Subject: [PATCH] Full sutartis port works --- uv_app/contracts/debug_bukle_mismatch.py | 170 ++++++++++ uv_app/contracts/port_bukle_to_rivile.py | 28 +- uv_app/contracts/port_sutartis_full.py | 38 +++ uv_app/contracts/port_sutartis_to_rivile.py | 321 ++++++++++++++++++ .../contracts/sutartys_by_klientas_kodas.sql | 20 ++ .../contracts/sutartys_by_sutarties_kodas.sql | 20 ++ 6 files changed, 581 insertions(+), 16 deletions(-) create mode 100644 uv_app/contracts/debug_bukle_mismatch.py create mode 100644 uv_app/contracts/port_sutartis_full.py create mode 100644 uv_app/contracts/port_sutartis_to_rivile.py create mode 100644 uv_app/contracts/sutartys_by_klientas_kodas.sql create mode 100644 uv_app/contracts/sutartys_by_sutarties_kodas.sql diff --git a/uv_app/contracts/debug_bukle_mismatch.py b/uv_app/contracts/debug_bukle_mismatch.py new file mode 100644 index 0000000..f878681 --- /dev/null +++ b/uv_app/contracts/debug_bukle_mismatch.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Dict, Iterable, Optional + +import psycopg2 +import psycopg2.extras +from dotenv import load_dotenv + +from uv_app.core.mssql import connect_to_mssql +from uv_app.core.pgsql import connect_to_pgsql + +MIN_YEAR = 2000 +OPEN_END_DATE = "3999-01-01" +SERVICE_CODE = "STATUSAS" + +DOTENV_PATH = Path(__file__).resolve().parents[2] / ".env" +load_dotenv(DOTENV_PATH, override=True) + +QUERY_PATH = Path(__file__).with_name("bukle_by_sutarties_kodas.sql") + + +def _read_query() -> str: + return QUERY_PATH.read_text(encoding="utf-8") + + +def _fetch_pg_rows(sutarties_kodas: str) -> 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(), (sutarties_kodas,)) + return cursor.fetchall() + finally: + conn.close() + + +def _fetch_mssql_appendices(contract_code: str) -> list[dict[str, object]]: + conn = connect_to_mssql() + if conn is None: + raise RuntimeError("Failed to connect to MSSQL.") + try: + cursor = conn.cursor() + cursor.execute( + """ + SELECT + N52_KODAS_KT, + N52_KODAS_K0, + N52_DOK_NR, + N52_KODAS, + CONVERT(date, N52_BEG_DATE) AS N52_BEG_DATE, + CONVERT(date, N52_END_DATE) AS N52_END_DATE, + N52_VISKAS + FROM dbo.N52_SUTD + WHERE N52_KODAS_KT = ? + AND N52_KODAS = ? + """, + (contract_code, SERVICE_CODE), + ) + rows = cursor.fetchall() + columns = [c[0] for c in cursor.description] + return [dict(zip(columns, row)) for row in rows] + finally: + conn.close() + + +def _normalize_date(value: object) -> str: + if value is None: + return "" + return str(value) + + +def _build_periods(rows: list[dict[str, object]]) -> list[dict[str, object]]: + normalized = [] + for row in rows: + start = row.get("data") + if start is None or str(start) < f"{MIN_YEAR}-01-01": + start = f"{MIN_YEAR}-01-01" + normalized.append( + { + "start": start, + "bukle": int(row["bukle"]), + } + ) + normalized.sort(key=lambda item: str(item["start"])) + periods = [] + for idx, item in enumerate(normalized): + start_date = str(item["start"]) + if idx == len(normalized) - 1: + end_date = OPEN_END_DATE + else: + next_start = normalized[idx + 1]["start"] + end_date = str(next_start - date.resolution) + active = item["bukle"] in {2, 3} + periods.append( + { + "start": start_date, + "end": end_date, + "active": active, + "k0": f"{SERVICE_CODE}-{idx + 1}", + "dok_nr": str(idx + 1), + } + ) + return periods + + +def _explain_mismatch(existing: list[dict[str, object]], desired: list[dict[str, object]]) -> None: + print("=== Desired (from PGSQL) ===") + for row in desired: + print(row) + print("=== Existing (from MSSQL) ===") + for row in existing: + print( + { + "k0": str(row.get("N52_KODAS_K0") or "").strip(), + "dok_nr": str(row.get("N52_DOK_NR") or "").strip(), + "start": _normalize_date(row.get("N52_BEG_DATE")), + "end": _normalize_date(row.get("N52_END_DATE")), + "active": int(row.get("N52_VISKAS") or 0) == 1, + } + ) + + existing_keys = { + ( + _normalize_date(row.get("N52_BEG_DATE")), + _normalize_date(row.get("N52_END_DATE")), + int(row.get("N52_VISKAS") or 0) == 1, + str(row.get("N52_KODAS_K0") or "").strip(), + str(row.get("N52_DOK_NR") or "").strip(), + ) + for row in existing + } + desired_keys = { + ( + row["start"], + row["end"], + row["active"], + row["k0"], + row["dok_nr"], + ) + for row in desired + } + + print("=== Missing in MSSQL ===") + for key in sorted(desired_keys - existing_keys): + print(key) + + print("=== Extra in MSSQL ===") + for key in sorted(existing_keys - desired_keys): + print(key) + + +def main() -> None: + sutarties_kodas = input("Sutarties kodas: ").strip() + if not sutarties_kodas: + print("Missing sutarties kodas.") + return + rows = list(_fetch_pg_rows(sutarties_kodas)) + if not rows: + print("No bukle rows found in PGSQL.") + return + contract_code = f"SUT-{sutarties_kodas}" + existing = _fetch_mssql_appendices(contract_code) + desired = _build_periods(rows) + _explain_mismatch(existing, desired) + + +if __name__ == "__main__": + main() diff --git a/uv_app/contracts/port_bukle_to_rivile.py b/uv_app/contracts/port_bukle_to_rivile.py index 29cebc5..b76807e 100644 --- a/uv_app/contracts/port_bukle_to_rivile.py +++ b/uv_app/contracts/port_bukle_to_rivile.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from datetime import date +from datetime import date, timedelta from pathlib import Path from typing import Dict, Iterable, Optional @@ -274,21 +274,17 @@ def _is_desired_state( ) -> bool: if len(existing) != len(periods): return False - existing_by_k0 = {str(r.get("N52_KODAS_K0") or "").strip(): r for r in existing} - for idx, period in enumerate(periods, start=1): - expected_k0 = f"{SERVICE_CODE}-{idx}" - row = existing_by_k0.get(expected_k0) - if not row: + existing_keys = [] + for row in existing: + start = row.get("N52_BEG_DATE") + end = row.get("N52_END_DATE") + if not start or not end: return False - if str(row.get("N52_DOK_NR") or "").strip() != str(idx): - return False - if row.get("N52_BEG_DATE") != period["start"]: - return False - if row.get("N52_END_DATE") != period["end"]: - return False - if bool(row.get("N52_VISKAS")) != period["active"]: - return False - return True + existing_keys.append((start, end)) + desired_keys = [(p["start"], p["end"]) for p in periods] + existing_keys.sort() + desired_keys.sort() + return existing_keys == desired_keys def _normalize_date(value: Optional[date]) -> date: @@ -317,7 +313,7 @@ def _build_periods(rows: list[dict[str, object]]) -> list[dict[str, object]]: end_date = OPEN_END_DATE else: next_start = normalized[idx + 1]["start"] - end_date = next_start - date.resolution + end_date = next_start - timedelta(days=1) bukle = item["bukle"] active = bukle in {2, 3} periods.append( diff --git a/uv_app/contracts/port_sutartis_full.py b/uv_app/contracts/port_sutartis_full.py new file mode 100644 index 0000000..a0b5048 --- /dev/null +++ b/uv_app/contracts/port_sutartis_full.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +SCRIPTS = [ + "port_sutartis_to_rivile.py", + "port_menesine_kaina_to_rivile.py", + "port_bukle_to_rivile.py", +] + + +def _run_script(script_path: Path, sutarties_kodas: str) -> None: + result = subprocess.run( + [sys.executable, str(script_path)], + input=f"{sutarties_kodas}\n", + text=True, + check=True, + ) + return None + + +def main() -> None: + sutarties_kodas = input("Sutarties kodas: ").strip() + if not sutarties_kodas: + print("Missing sutarties kodas.") + return + + base_dir = Path(__file__).resolve().parent + for script_name in SCRIPTS: + script_path = base_dir / script_name + print(f"Running {script_name}...") + _run_script(script_path, sutarties_kodas) + + +if __name__ == "__main__": + main() diff --git a/uv_app/contracts/port_sutartis_to_rivile.py b/uv_app/contracts/port_sutartis_to_rivile.py new file mode 100644 index 0000000..064afd7 --- /dev/null +++ b/uv_app/contracts/port_sutartis_to_rivile.py @@ -0,0 +1,321 @@ +from __future__ import annotations + +import os +from datetime import date, datetime +from pathlib import Path +from typing import Dict, Iterable, Optional + +import psycopg2 +import psycopg2.extras +import requests +from dotenv import load_dotenv + +from uv_app.core.mssql import connect_to_mssql +from uv_app.core.pgsql import connect_to_pgsql + +BASE_URL = "https://api.manorivile.lt/client/v2" +TIMEOUT = 30 +MIN_YEAR = 2000 + +DOTENV_PATH = Path(__file__).resolve().parents[2] / ".env" +load_dotenv(DOTENV_PATH, override=True) + +QUERY_PATH = Path(__file__).with_name("sutartys_by_sutarties_kodas.sql") + + +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 _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 _read_query() -> str: + return QUERY_PATH.read_text(encoding="utf-8") + + +def _fetch_rows(sutarties_kodas: str) -> 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(), (sutarties_kodas,)) + return cursor.fetchall() + finally: + conn.close() + + +def _contract_exists(conn: "pyodbc.Connection", contract_code: str) -> bool: + cursor = conn.cursor() + cursor.execute( + """ + SELECT 1 + FROM dbo.N51_SUTH + WHERE N51_KODAS_KT = ? + """, + (contract_code,), + ) + return cursor.fetchone() is not None + + +def _fetch_existing_contract( + conn: "pyodbc.Connection", + contract_code: str, +) -> Optional[Dict[str, object]]: + cursor = conn.cursor() + cursor.execute( + """ + SELECT + N51_KODAS_KT, + N51_KODAS_KS, + N51_PAV, + CONVERT(date, N51_OP_DATA) AS N51_OP_DATA, + CONVERT(date, N51_BEG_DATE) AS N51_BEG_DATE, + N51_TIPAS, + N51_APRASYMAS1, + N51_APRASYMAS2 + FROM dbo.N51_SUTH + WHERE N51_KODAS_KT = ? + """, + (contract_code,), + ) + row = cursor.fetchone() + if not row: + return None + columns = [c[0] for c in cursor.description] + return dict(zip(columns, row)) + + +def _normalize_text(value: object) -> str: + if value is None: + return "" + return str(value).strip() + + +def _normalize_date(value: object) -> date: + if isinstance(value, date): + result = value + elif isinstance(value, datetime): + result = value.date() + else: + return date.today() + if result < date(MIN_YEAR, 1, 1): + return date.today() + return result + + +def _is_desired_contract( + existing: Optional[Dict[str, object]], + contract_code: str, + client_code: str, + contract_type: str, + start: date, + address: str, + password: str, +) -> bool: + if not existing: + return False + return ( + _normalize_text(existing.get("N51_KODAS_KT")) == contract_code + and _normalize_text(existing.get("N51_KODAS_KS")) == client_code + and _normalize_text(existing.get("N51_PAV")) == contract_code + and _normalize_date(existing.get("N51_OP_DATA")) == start + and _normalize_date(existing.get("N51_BEG_DATE")) == start + and _normalize_text(existing.get("N51_TIPAS")) == contract_type + and _normalize_text(existing.get("N51_APRASYMAS1")) == address + and _normalize_text(existing.get("N51_APRASYMAS2")) == password + ) + + +def _choose_contract_type(conn: "pyodbc.Connection") -> Optional[str]: + cursor = conn.cursor() + cursor.execute( + """ + SELECT N51_TIPAS, COUNT(*) AS cnt + FROM dbo.N51_SUTH + WHERE N51_TIPAS IS NOT NULL + GROUP BY N51_TIPAS + ORDER BY cnt DESC, N51_TIPAS + """ + ) + row = cursor.fetchone() + if not row: + return None + return str(row[0]).strip() + + +def _build_address(row: Dict[str, object]) -> str: + parts = [] + gatve = (row.get("gatve") or "").strip() + namas = (row.get("namas") or "").strip() + miestas = (row.get("miestas") or "").strip() + rajonas = (row.get("rajonas") or "").strip() + if gatve or namas: + parts.append(" ".join(p for p in [gatve, namas] if p)) + if miestas: + parts.append(miestas) + if rajonas: + parts.append(rajonas) + return ", ".join(parts) + + +def _create_contract( + api_key: str, + contract_code: str, + client_code: str, + start: date, + contract_type: str, + address: str, + password: str, +) -> None: + payload = { + "method": "EDIT_N51_FULL", + "params": { + "oper": "I", + "user": api_key.split(".", 1)[0], + }, + "data": { + "N51": { + "N51_KODAS_KT": contract_code, + "N51_KODAS_KS": client_code, + "N51_PAV": contract_code, + "N51_OP_DATA": start.isoformat(), + "N51_BEG_DATE": start.isoformat(), + "N51_TIPAS": contract_type, + "N51_APRASYMAS1": address, + "N51_APRASYMAS2": password, + } + }, + } + _post(api_key, payload) + + +def _update_contract( + api_key: str, + contract_code: str, + client_code: str, + contract_type: str, + address: str, + password: str, + start: date, +) -> None: + payload = { + "method": "EDIT_N51", + "params": { + "oper": "U", + "user": api_key.split(".", 1)[0], + "fld": "N51_KODAS_KT", + "val": contract_code, + }, + "data": { + "N51": { + "N51_KODAS_KT": contract_code, + "N51_KODAS_KS": client_code, + "N51_PAV": contract_code, + "N51_TIPAS": contract_type, + "N51_OP_DATA": start.isoformat(), + "N51_BEG_DATE": start.isoformat(), + "N51_APRASYMAS1": address, + "N51_APRASYMAS2": password, + } + }, + } + _post(api_key, payload) + + +def main() -> None: + sutarties_kodas = input("Sutarties kodas: ").strip() + if not sutarties_kodas: + print("Missing sutarties kodas.") + return + + rows = list(_fetch_rows(sutarties_kodas)) + if not rows: + print("No sutartis rows found.") + return + + row = rows[0] + client_code = str(row.get("client_code") or "").strip() + if not client_code: + raise RuntimeError("Missing client code for sutartis.") + + contract_code = f"SUT-{sutarties_kodas}" + start_date = _normalize_date(row.get("data")) + address = _build_address(row) + password = str(row.get("password") or "").strip() + + api_key = _get_api_key() + mssql_conn = connect_to_mssql() + if mssql_conn is None: + raise RuntimeError("Failed to connect to MSSQL.") + try: + contract_type = _choose_contract_type(mssql_conn) + if not contract_type: + raise RuntimeError("Failed to resolve N51_TIPAS for Pardavimo.") + + existing = _fetch_existing_contract(mssql_conn, contract_code) + if _is_desired_contract( + existing, + contract_code, + client_code, + contract_type, + start_date, + address, + password, + ): + print("Already in desired state; no changes.") + return + + if existing: + _update_contract( + api_key, + contract_code, + client_code, + contract_type, + address, + password, + start_date, + ) + print(f"Updated contract {contract_code}") + else: + _create_contract( + api_key, + contract_code, + client_code, + start_date, + contract_type, + address, + password, + ) + print(f"Created contract {contract_code}") + finally: + mssql_conn.close() + + +if __name__ == "__main__": + main() diff --git a/uv_app/contracts/sutartys_by_klientas_kodas.sql b/uv_app/contracts/sutartys_by_klientas_kodas.sql new file mode 100644 index 0000000..fff9153 --- /dev/null +++ b/uv_app/contracts/sutartys_by_klientas_kodas.sql @@ -0,0 +1,20 @@ +SELECT + s.*, + t.pav AS sutartis_type, + k.kodas AS client_code, + k.vardas, + k.pavarde, + k.email, + k.mob_telefonas, + k.telefonas, + g.pav AS gatve, + m.pav AS miestas, + r.pav AS rajonas +FROM sutartis s +JOIN klientas k ON k.klientasid = s.klientasid +LEFT JOIN sutartis_type t ON t.sutartis_typeid = s.sutartis_typeid +LEFT JOIN gatve g ON g.gatveid = s.gatveid +LEFT JOIN miestas m ON m.miestasid = s.miestasid +LEFT JOIN rajonas r ON r.rajonasid = s.rajonasid +WHERE k.kodas = 'XXXXXX' +ORDER BY s.sutartisid; diff --git a/uv_app/contracts/sutartys_by_sutarties_kodas.sql b/uv_app/contracts/sutartys_by_sutarties_kodas.sql new file mode 100644 index 0000000..22b8713 --- /dev/null +++ b/uv_app/contracts/sutartys_by_sutarties_kodas.sql @@ -0,0 +1,20 @@ +SELECT + s.*, + t.pav AS sutartis_type, + k.kodas AS client_code, + k.vardas, + k.pavarde, + k.email, + k.mob_telefonas, + k.telefonas, + g.pav AS gatve, + m.pav AS miestas, + r.pav AS rajonas +FROM sutartis s +JOIN klientas k ON k.klientasid = s.klientasid +LEFT JOIN sutartis_type t ON t.sutartis_typeid = s.sutartis_typeid +LEFT JOIN gatve g ON g.gatveid = s.gatveid +LEFT JOIN miestas m ON m.miestasid = s.miestasid +LEFT JOIN rajonas r ON r.rajonasid = s.rajonasid +WHERE s.sutartiesid = %s +ORDER BY s.sutartisid;