169 lines
8.1 KiB
Python
169 lines
8.1 KiB
Python
|
|
"""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)}
|