bmc_hub/scripts/reset_and_rebuild_internet_connections.py

251 lines
9.9 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Reset internet connection data and rebuild from latest GlobalConnect extractions."""
from __future__ import annotations
import argparse
import json
from collections import defaultdict
from app.billing.backend.supplier_invoices import _sync_globalconnect_extraction_to_internet
from app.core.database import execute_query, execute_query_single, execute_update, init_db
def latest_globalconnect_extractions() -> list[dict]:
rows = execute_query(
"""
SELECT DISTINCT ON (e.file_id)
e.extraction_id,
e.file_id,
e.vendor_name,
e.document_id,
e.document_date,
e.created_at,
i.filename
FROM extractions e
JOIN incoming_files i ON i.file_id = e.file_id
WHERE LOWER(COALESCE(e.vendor_name, '')) LIKE '%%globalconnect%%'
ORDER BY e.file_id, e.created_at DESC, e.extraction_id DESC
"""
) or []
return [dict(row) for row in rows]
def snapshot_manual_allocations() -> list[dict]:
"""Keep deliberate CRM ownership across a destructive invoice rebuild.
Supplier imports must never infer a customer. A clean rebuild previously
also discarded customers that a user had already selected, which allowed a
later, shifted invoice address to become the new canonical address.
"""
rows = execute_query(
"""
SELECT DISTINCT ON (
regexp_replace(UPPER(COALESCE(circuit_number, '')), '[^A-Z0-9]', '', 'g')
)
regexp_replace(UPPER(COALESCE(circuit_number, '')), '[^A-Z0-9]', '', 'g') AS reference_key,
circuit_number,
customer_id,
address
FROM internet_connections_connections
WHERE deleted_at IS NULL
AND customer_id IS NOT NULL
AND BTRIM(COALESCE(circuit_number, '')) <> ''
ORDER BY
regexp_replace(UPPER(COALESCE(circuit_number, '')), '[^A-Z0-9]', '', 'g'),
updated_at DESC,
id DESC
"""
) or []
return [dict(row) for row in rows]
def restore_manual_allocations(allocations: list[dict]) -> int:
restored = 0
for allocation in allocations:
reference_key = str(allocation.get("reference_key") or "").strip()
if not reference_key:
continue
connection = execute_query_single(
"""
SELECT id
FROM internet_connections_connections
WHERE deleted_at IS NULL
AND regexp_replace(UPPER(COALESCE(circuit_number, '')), '[^A-Z0-9]', '', 'g') = %s
ORDER BY id DESC
LIMIT 1
""",
(reference_key,),
)
if not connection:
continue
connection_id = int(connection["id"])
execute_update(
"""
UPDATE internet_connections_connections
SET customer_id = %s,
address = COALESCE(NULLIF(BTRIM(%s), ''), address),
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""",
(allocation.get("customer_id"), allocation.get("address"), connection_id),
)
if str(allocation.get("address") or "").strip():
execute_update(
"""
UPDATE internet_connections_ip_ranges
SET service_address = %s,
updated_at = CURRENT_TIMESTAMP
WHERE connection_id = %s AND deleted_at IS NULL
""",
(allocation["address"], connection_id),
)
restored += 1
return restored
def reset_all_internet_data() -> dict:
summary: dict[str, int] = {}
counts = execute_query_single(
"""
SELECT
(SELECT COUNT(*) FROM internet_connections_connections WHERE deleted_at IS NULL) AS connections,
(SELECT COUNT(*) FROM internet_connections_ip_ranges WHERE deleted_at IS NULL) AS ip_ranges,
(SELECT COUNT(*) FROM internet_connections_ip_addresses WHERE deleted_at IS NULL) AS ip_addresses,
(SELECT COUNT(*) FROM internet_connections_pricing) AS pricing,
(SELECT COUNT(*) FROM internet_connections_history) AS history
"""
) or {}
summary["before_connections"] = int(counts.get("connections") or 0)
summary["before_ip_ranges"] = int(counts.get("ip_ranges") or 0)
summary["before_ip_addresses"] = int(counts.get("ip_addresses") or 0)
summary["before_pricing"] = int(counts.get("pricing") or 0)
summary["before_history"] = int(counts.get("history") or 0)
summary["deleted_ip_addresses"] = execute_update(
"""
UPDATE internet_connections_ip_addresses
SET deleted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP,
comment = CONCAT(
COALESCE(comment, ''),
CASE WHEN COALESCE(comment, '') = '' THEN '' ELSE E'\n' END,
'Nulstillet før ren genimport af internetforbindelser.'
)
WHERE deleted_at IS NULL
"""
)
summary["deleted_ip_ranges"] = execute_update(
"""
UPDATE internet_connections_ip_ranges
SET deleted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP,
description = CONCAT(
COALESCE(description, ''),
CASE WHEN COALESCE(description, '') = '' THEN '' ELSE E'\n' END,
'Nulstillet før ren genimport af internetforbindelser.'
)
WHERE deleted_at IS NULL
"""
)
summary["deleted_connections"] = execute_update(
"""
UPDATE internet_connections_connections
SET deleted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP,
status = 'inactive',
notes = CONCAT(
COALESCE(notes, ''),
CASE WHEN COALESCE(notes, '') = '' THEN '' ELSE E'\n' END,
'Nulstillet før ren genimport af internetforbindelser.'
)
WHERE deleted_at IS NULL
"""
)
summary["deleted_pricing"] = execute_update("DELETE FROM internet_connections_pricing")
summary["deleted_history"] = execute_update("DELETE FROM internet_connections_history")
return summary
def rebuild_from_globalconnect(manual_allocations: list[dict] | None = None) -> dict:
extractions = latest_globalconnect_extractions()
results = []
totals = defaultdict(int)
for extraction_stub in extractions:
extraction = execute_query_single(
"""
SELECT *
FROM extractions
WHERE extraction_id = %s
""",
(extraction_stub["extraction_id"],),
)
if not extraction:
continue
# A reset intentionally rebuilds previously processed invoices, so the
# normal idempotency guard must not skip their historical sync runs.
result = _sync_globalconnect_extraction_to_internet(dict(extraction), force=True)
result_summary = {
"file_id": extraction_stub["file_id"],
"extraction_id": extraction_stub["extraction_id"],
"filename": extraction_stub["filename"],
"document_id": extraction_stub["document_id"],
"connections_synced": int(result.get("connections_synced") or 0),
"connections_created": int(result.get("connections_created") or 0),
"connections_updated": int(result.get("connections_updated") or 0),
"ip_ranges_synced": int(result.get("ip_ranges_synced") or 0),
"skipped_connection_lines": int(result.get("skipped_connection_lines") or 0),
"skipped_ip_range_lines": int(result.get("skipped_orphan_ip_ranges") or 0),
"verification": result.get("verification") or {},
}
results.append(result_summary)
totals["files_processed"] += 1
totals["connections_synced"] += result_summary["connections_synced"]
totals["connections_created"] += result_summary["connections_created"]
totals["connections_updated"] += result_summary["connections_updated"]
totals["ip_ranges_synced"] += result_summary["ip_ranges_synced"]
totals["skipped_connection_lines"] += result_summary["skipped_connection_lines"]
totals["skipped_ip_range_lines"] += result_summary["skipped_ip_range_lines"]
restored_allocations = restore_manual_allocations(manual_allocations or [])
counts = execute_query_single(
"""
SELECT
(SELECT COUNT(*) FROM internet_connections_connections WHERE deleted_at IS NULL) AS active_connections,
(SELECT COUNT(*) FROM internet_connections_connections WHERE deleted_at IS NULL AND (address IS NULL OR BTRIM(address) = '')) AS missing_address_connections,
(SELECT COUNT(*) FROM internet_connections_ip_ranges WHERE deleted_at IS NULL) AS active_ip_ranges,
(SELECT COUNT(*) FROM internet_connections_ip_addresses WHERE deleted_at IS NULL) AS active_ip_addresses
"""
) or {}
return {
"totals": dict(totals),
"results": results,
"post_counts": {
"active_connections": int(counts.get("active_connections") or 0),
"missing_address_connections": int(counts.get("missing_address_connections") or 0),
"active_ip_ranges": int(counts.get("active_ip_ranges") or 0),
"active_ip_addresses": int(counts.get("active_ip_addresses") or 0),
},
"restored_manual_allocations": restored_allocations,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--skip-rebuild", action="store_true", help="Only reset internet data")
args = parser.parse_args()
init_db()
manual_allocations = snapshot_manual_allocations()
payload = {
"reset": reset_all_internet_data(),
"rebuild": None if args.skip_rebuild else rebuild_from_globalconnect(manual_allocations),
}
print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
return 0
if __name__ == "__main__":
raise SystemExit(main())