"""Create deduplicated procurement cases for externally detected internet changes.""" from __future__ import annotations import json import logging from typing import Any, Mapping, Optional from app.core.config import settings from app.core.database import execute_query, execute_query_single logger = logging.getLogger(__name__) RELEVANT_FIELDS = { "address", "service_address", "monthly_cost", "sales_price", "technology", "connection_type", "circuit_number", "provider_reference", "provider", "vendor_id", "speed_mbps", "download_mbps", "upload_mbps", "status", "sla_subscription_id", "sla_price", "sla_status", "ip_range", "cidr", "contract_number", "range_added", "range_removed", "range_monthly_cost", "range_sales_price", } FIELD_LABELS = { "address": "Adresse", "service_address": "Serviceadresse", "monthly_cost": "Indkøbspris", "sales_price": "Salgspris", "technology": "Teknologi", "connection_type": "Forbindelsestype", "circuit_number": "Kredsløbsnummer", "provider_reference": "Leverandørreference", "provider": "Leverandør", "vendor_id": "Leverandør", "speed_mbps": "Hastighed", "download_mbps": "Download", "upload_mbps": "Upload", "status": "Status", "sla_subscription_id": "SLA-aftale", "sla_price": "SLA-pris", "sla_status": "SLA-status", "ip_range": "IP-range", "cidr": "IP-range", "contract_number": "Kontraktnummer", "range_added": "Nyt IP-range", "range_removed": "Fjernet IP-range", "range_monthly_cost": "IP-range indkøbspris", "range_sales_price": "IP-range salgspris", } def filter_relevant_changes(changes: Mapping[str, Any] | None) -> dict[str, dict[str, Any]]: filtered: dict[str, dict[str, Any]] = {} for field, raw in (changes or {}).items(): if field not in RELEVANT_FIELDS: continue change = raw if isinstance(raw, Mapping) else {"from": None, "to": raw} before, after = change.get("from"), change.get("to") if str(before or "").strip() == str(after or "").strip(): continue filtered[field] = {"from": before, "to": after} return filtered def _procurement_customer_id() -> int: configured = getattr(settings, "PROCUREMENT_CASE_CUSTOMER_ID", None) if configured: row = execute_query_single("SELECT id FROM customers WHERE id=%s AND is_active=true", (configured,)) if row: return int(row["id"]) row = execute_query_single( """SELECT id FROM customers WHERE is_active=true AND LOWER(name) LIKE %s ORDER BY CASE WHEN LOWER(name) LIKE %s THEN 0 ELSE 1 END, id LIMIT 1""", ("%bmc%", "%bmc networks%"), ) if not row: raise ValueError("BMC's interne indkøbskunde blev ikke fundet") return int(row["id"]) def _economy_group_id() -> Optional[int]: row = execute_query_single( """SELECT id FROM groups WHERE LOWER(name) LIKE ANY(%s) ORDER BY id LIMIT 1""", (["%økonomi%", "%okonomi%", "%economic%"],), ) return int(row["id"]) if row else None def _render_description( *, connection_id: int, connection_name: str, reference: str, provider: str, source_label: str, source_url: Optional[str], changes: Mapping[str, Mapping[str, Any]], ) -> str: lines = [ "Automatisk oprettet efter en ekstern ændring af en internetforbindelse.", "", f"Forbindelse: {connection_name}", f"Reference: {reference or '-'}", f"Leverandør: {provider or '-'}", f"Kilde: {source_label}", f"Link til forbindelse: /economy/internet-connections/{connection_id}", ] if source_url: lines.append(f"Link til kilde: {source_url}") lines.extend(["", "Registrerede ændringer:"]) for field, change in changes.items(): lines.append(f"- {FIELD_LABELS.get(field, field)}: {change.get('from')} → {change.get('to')}") return "\n".join(lines) def ensure_external_change_case( *, connection_id: int, source_type: str, source_key: str, source_label: str, changes: Mapping[str, Any], connection_name: str = "Internetforbindelse", reference: str = "", provider: str = "", owner_customer_id: Optional[int] = None, source_url: Optional[str] = None, ) -> dict[str, Any]: relevant = filter_relevant_changes(changes) if not relevant: return {"case_id": None, "created": False, "changes": {}, "error": None} source_type = str(source_type or "external").strip().lower() source_key = str(source_key or "").strip() if not source_key: return {"case_id": None, "created": False, "changes": relevant, "error": "Kilden mangler en stabil nøgle"} try: existing = execute_query_single( """SELECT id, sag_id, changes FROM internet_connection_change_cases WHERE connection_id=%s AND source_type=%s AND source_key=%s""", (connection_id, source_type, source_key), ) merged = dict((existing or {}).get("changes") or {}) merged.update(relevant) case_customer_id = int(owner_customer_id) if owner_customer_id else _procurement_customer_id() title = f"Internetændring {reference or connection_name} · {source_label}"[:255] description = _render_description( connection_id=connection_id, connection_name=connection_name, reference=reference, provider=provider, source_label=source_label, source_url=source_url, changes=merged, ) case_id = int(existing["sag_id"]) if existing and existing.get("sag_id") else None created = False if case_id: execute_query( "UPDATE sag_sager SET beskrivelse=%s, updated_at=NOW() WHERE id=%s AND deleted_at IS NULL", (description, case_id), fetch=False, ) else: row = execute_query_single( """INSERT INTO sag_sager (titel, beskrivelse, type, status, customer_id, assigned_group_id, created_by_user_id) VALUES (%s,%s,'indkøb','åben',%s,%s,1) RETURNING id""", (title, description, case_customer_id, _economy_group_id()), ) case_id = int(row["id"]) created = True if existing: execute_query( """UPDATE internet_connection_change_cases SET sag_id=%s, source_label=%s, source_url=%s, changes=%s::jsonb, last_error=NULL, updated_at=NOW() WHERE id=%s""", (case_id, source_label, source_url, json.dumps(merged, ensure_ascii=False), existing["id"]), fetch=False, ) else: execute_query( """INSERT INTO internet_connection_change_cases (connection_id,source_type,source_key,source_label,source_url,sag_id,changes) VALUES (%s,%s,%s,%s,%s,%s,%s::jsonb)""", (connection_id, source_type, source_key, source_label, source_url, case_id, json.dumps(merged, ensure_ascii=False)), fetch=False, ) return {"case_id": case_id, "created": created, "changes": merged, "error": None} except Exception as exc: logger.warning("Could not create internet change case for connection %s: %s", connection_id, exc) # Keep the import operational, but persist a visible control item whenever # the audit table itself is available. try: execute_query( """INSERT INTO internet_connection_change_cases (connection_id,source_type,source_key,source_label,source_url,changes,last_error) VALUES (%s,%s,%s,%s,%s,%s::jsonb,%s) ON CONFLICT (connection_id,source_type,source_key) DO UPDATE SET changes=internet_connection_change_cases.changes || EXCLUDED.changes, last_error=EXCLUDED.last_error, updated_at=NOW()""", (connection_id, source_type, source_key, source_label, source_url, json.dumps(relevant, ensure_ascii=False), str(exc)), fetch=False, ) except Exception: logger.exception("Could not persist failed internet change-case audit") return {"case_id": None, "created": False, "changes": relevant, "error": str(exc)}