diff --git a/uv_app/infrastructure/create_paslauga_kaina.py b/uv_app/infrastructure/create_paslauga_kaina.py new file mode 100644 index 0000000..1d7da5f --- /dev/null +++ b/uv_app/infrastructure/create_paslauga_kaina.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Optional + +import requests +from dotenv import load_dotenv + +from uv_app.core.mssql import connect_to_mssql + +BASE_URL = "https://api.manorivile.lt/client/v2" +TIMEOUT = 30 + +DOTENV_PATH = Path(__file__).resolve().parents[2] / ".env" +load_dotenv(DOTENV_PATH, override=True) + +SERVICE_CODE = "KAINA" +SERVICE_NAME = "Kaina" +SERVICE_UOM = "VNT" + + +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 _service_exists(api_key: str, code: str) -> bool: + payload = { + "method": "GET_N17_LIST", + "params": { + "list": "H", + "fil": f"n17_kodas_ps='{code}'", + "limit": 1, + }, + } + data = _post(api_key, payload) + return bool(data.get("list") or data.get("N17")) + + +def _choose_service_ds() -> Optional[str]: + conn = connect_to_mssql() + if conn is None: + raise RuntimeError("Failed to connect to MSSQL.") + try: + cursor = conn.cursor() + cursor.execute( + """ + SELECT N17_KODAS_DS, COUNT(*) AS cnt + FROM dbo.N17_PROD + WHERE N17_TIPAS = 2 + AND N17_KODAS_DS IS NOT NULL + AND LTRIM(RTRIM(N17_KODAS_DS)) <> '' + GROUP BY N17_KODAS_DS + ORDER BY cnt DESC, N17_KODAS_DS + """ + ) + rows = cursor.fetchall() + finally: + conn.close() + + if not rows: + return None + + return str(rows[0][0]).strip() + + +def create_service_kaina() -> dict: + api_key = _get_api_key() + + service_ds = _choose_service_ds() + if not service_ds: + return {"status": "cancelled", "reason": "No N17_KODAS_DS selected"} + + n17 = { + "N17_KODAS_PS": SERVICE_CODE, + "N17_TIPAS": "2", + "N17_PAV": SERVICE_NAME, + "N17_KODAS_US": SERVICE_UOM, + "N17_KODAS_DS": service_ds, + } + + if _service_exists(api_key, SERVICE_CODE): + payload = { + "method": "EDIT_N17", + "params": { + "oper": "U", + "user": api_key.split(".", 1)[0], + "fld": "N17_KODAS_PS", + "val": SERVICE_CODE, + }, + "data": {"N17": n17}, + } + data = _post(api_key, payload) + return {"status": "updated", "code": SERVICE_CODE, "response": data} + + payload = { + "method": "EDIT_N17", + "params": {"oper": "I"}, + "data": {"N17": n17}, + } + data = _post(api_key, payload) + return {"status": "created", "code": SERVICE_CODE, "response": data} + + +def main() -> None: + result = create_service_kaina() + print(result) + + +if __name__ == "__main__": + main() diff --git a/uv_app/infrastructure/create_paslauga_statusas.py b/uv_app/infrastructure/create_paslauga_statusas.py new file mode 100644 index 0000000..89cbe11 --- /dev/null +++ b/uv_app/infrastructure/create_paslauga_statusas.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Optional + +import requests +from dotenv import load_dotenv + +from uv_app.core.mssql import connect_to_mssql + +BASE_URL = "https://api.manorivile.lt/client/v2" +TIMEOUT = 30 + +DOTENV_PATH = Path(__file__).resolve().parents[2] / ".env" +load_dotenv(DOTENV_PATH, override=True) + +SERVICE_CODE = "STATUSAS" +SERVICE_NAME = "Statusas" +SERVICE_UOM = "VNT" + + +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 _service_exists(api_key: str, code: str) -> bool: + payload = { + "method": "GET_N17_LIST", + "params": { + "list": "H", + "fil": f"n17_kodas_ps='{code}'", + "limit": 1, + }, + } + data = _post(api_key, payload) + return bool(data.get("list") or data.get("N17")) + + +def _choose_service_ds() -> Optional[str]: + conn = connect_to_mssql() + if conn is None: + raise RuntimeError("Failed to connect to MSSQL.") + try: + cursor = conn.cursor() + cursor.execute( + """ + SELECT N17_KODAS_DS, COUNT(*) AS cnt + FROM dbo.N17_PROD + WHERE N17_TIPAS = 2 + AND N17_KODAS_DS IS NOT NULL + AND LTRIM(RTRIM(N17_KODAS_DS)) <> '' + GROUP BY N17_KODAS_DS + ORDER BY cnt DESC, N17_KODAS_DS + """ + ) + rows = cursor.fetchall() + finally: + conn.close() + + if not rows: + return None + + return str(rows[0][0]).strip() + + +def create_service_statusas() -> dict: + api_key = _get_api_key() + service_ds = _choose_service_ds() + if not service_ds: + return {"status": "cancelled", "reason": "No N17_KODAS_DS selected"} + + n17 = { + "N17_KODAS_PS": SERVICE_CODE, + "N17_TIPAS": "2", + "N17_PAV": SERVICE_NAME, + "N17_KODAS_US": SERVICE_UOM, + "N17_KODAS_DS": service_ds, + } + + if _service_exists(api_key, SERVICE_CODE): + payload = { + "method": "EDIT_N17", + "params": { + "oper": "U", + "user": api_key.split(".", 1)[0], + "fld": "N17_KODAS_PS", + "val": SERVICE_CODE, + }, + "data": {"N17": n17}, + } + data = _post(api_key, payload) + return {"status": "updated", "code": SERVICE_CODE, "response": data} + + payload = { + "method": "EDIT_N17", + "params": {"oper": "I"}, + "data": {"N17": n17}, + } + data = _post(api_key, payload) + return {"status": "created", "code": SERVICE_CODE, "response": data} + + +def main() -> None: + result = create_service_statusas() + print(result) + + +if __name__ == "__main__": + main() diff --git a/uv_app/user/debug_client_101460.py b/uv_app/user/debug_client_101460.py new file mode 100644 index 0000000..25f7b99 --- /dev/null +++ b/uv_app/user/debug_client_101460.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import re +from pathlib import Path +from typing import Dict, Optional + +import psycopg2 +import psycopg2.extras + +from uv_app.core.mssql import connect_to_mssql +from uv_app.core.pgsql import connect_to_pgsql + +QUERY_PATH = Path(__file__).with_name("routeriai_query.sql") +CLIENT_CODE = "101460" + + +def _read_query() -> str: + return QUERY_PATH.read_text(encoding="utf-8") + + +def _clean_text(value: object) -> Optional[str]: + if value is None: + return None + text = value.decode(errors="ignore") if isinstance(value, bytes) else str(value) + text = text.replace("\u00a0", " ") + text = re.sub(r"[\x00-\x1f\x7f\u2028\u2029\u0085]", " ", text) + return re.sub(r"\s+", " ", text).strip() + + +def _format_diff_value(value: str) -> str: + return ( + value.replace("\r", "\\r") + .replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\u2028", "\\u2028") + .replace("\u2029", "\\u2029") + .replace("\u0085", "\\u0085") + ) + + +def _dump_string(label: str, value: object) -> None: + if value is None: + print(f"{label}: None") + return + text = value.decode(errors="ignore") if isinstance(value, bytes) else str(value) + print(f"{label} raw repr: {text!r}") + print(f"{label} raw escaped: {_format_diff_value(text)}") + cleaned = _clean_text(text) + print(f"{label} cleaned repr: {cleaned!r}") + print(f"{label} cleaned escaped: {_format_diff_value(cleaned or '')}") + codepoints = [f"U+{ord(ch):04X}" for ch in text] + print(f"{label} codepoints: {' '.join(codepoints)}") + + +def _fetch_pg_client() -> Optional[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()) + rows = cursor.fetchall() + finally: + conn.close() + for row in rows: + if str(row.get("client_code") or "").strip() == CLIENT_CODE: + return row + return None + + +def _fetch_mssql_client() -> Optional[Dict[str, object]]: + conn = connect_to_mssql() + if conn is None: + raise RuntimeError("Failed to connect to MSSQL.") + try: + query = """ + SELECT + N08_KODAS_KS, + N08_PAV + FROM dbo.N08_KLIJ + WHERE N08_KODAS_KS = ? + """ + cursor = conn.cursor() + cursor.execute(query, (CLIENT_CODE,)) + row = cursor.fetchone() + if not row: + return None + columns = [c[0] for c in cursor.description] + return dict(zip(columns, row)) + finally: + conn.close() + + +def main() -> None: + pg_row = _fetch_pg_client() + if not pg_row: + print("Client not found in PostgreSQL.") + else: + print("PostgreSQL data:") + _dump_string("PG name", pg_row.get("name")) + + mssql_row = _fetch_mssql_client() + if not mssql_row: + print("Client not found in MSSQL.") + else: + print("MSSQL data:") + _dump_string("MSSQL N08_PAV", mssql_row.get("N08_PAV")) + + +if __name__ == "__main__": + main() diff --git a/uv_app/user/insert_clients.py b/uv_app/user/insert_clients.py index 2bd4f63..d4d689e 100644 --- a/uv_app/user/insert_clients.py +++ b/uv_app/user/insert_clients.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import re from datetime import date, datetime from pathlib import Path from typing import Dict, Iterable, Optional, Tuple @@ -108,6 +109,19 @@ def _normalize_person_type(value: Optional[object]) -> str: return "1" +def _clean_text(value: object) -> Optional[str]: + if value is None: + return None + if isinstance(value, bytes): + text = value.decode(errors="ignore") + else: + text = str(value) + text = text.replace("\u00a0", " ") + # Drop control chars (including C1) and unicode line/paragraph separators. + text = re.sub(r"[\x00-\x1f\x7f-\x9f\u2028\u2029]", " ", text) + return re.sub(r"\s+", " ", text).strip() + + def _build_n08_payload(row: Dict[str, object]) -> Dict[str, object]: mobile = (row.get("mobile_phone") or "").strip() phone = (row.get("phone") or "").strip() @@ -129,9 +143,11 @@ def _build_n08_payload(row: Dict[str, object]) -> Dict[str, object]: if isinstance(creation_date, (date, datetime)): creation_date = creation_date.isoformat() + name = _clean_text(row.get("name")) + return { "N08_KODAS_KS": row.get("client_code"), - "N08_PAV": row.get("name"), + "N08_PAV": name, "N08_ADR": row.get("address"), "N08_E_MAIL": row.get("email"), "N08_ADD_DATE": creation_date, @@ -163,6 +179,17 @@ def _normalize_value(value: object) -> str: return str(value).strip() +def _format_diff_value(value: str) -> str: + return ( + value.replace("\r", "\\r") + .replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\u2028", "\\u2028") + .replace("\u2029", "\\u2029") + .replace("\u0085", "\\u0085") + ) + + def _diff_fields( existing: Optional[Dict[str, object]], desired: Dict[str, object], @@ -207,7 +234,9 @@ def _upsert_client( return print(f"Updating client: {client_code} ({index}/{total})") for field, (old, new) in changes.items(): - print(f" {field}: '{old}' -> '{new}'") + old_fmt = _format_diff_value(old) + new_fmt = _format_diff_value(new) + print(f" {field}: '{old_fmt}' -> '{new_fmt}'") payload = { "method": "EDIT_N08", "params": { @@ -221,7 +250,8 @@ def _upsert_client( else: print(f"Creating client: {client_code} ({index}/{total})") for field, (_, new) in changes.items(): - print(f" {field}: '' -> '{new}'") + new_fmt = _format_diff_value(new) + print(f" {field}: '' -> '{new_fmt}'") payload = { "method": "EDIT_N08_FULL", "params": {"oper": "I", "user": user}, diff --git a/uv_app/user/routeriai_query.sql b/uv_app/user/routeriai_query.sql index 779b8fc..b1bee72 100644 --- a/uv_app/user/routeriai_query.sql +++ b/uv_app/user/routeriai_query.sql @@ -1,6 +1,11 @@ SELECT k.kodas AS client_code, - trim(concat_ws(' ', k.vardas, k.pavarde)) AS name, + regexp_replace( + trim(concat_ws(' ', k.vardas, k.pavarde)), + '\s+', + ' ', + 'g' + ) AS name, trim(concat_ws(', ', concat_ws(' ', g.pav, k.namas, NULLIF(NULLIF(k.butas::text, '0'), '')), m.pav,