bmc_hub/app/modules/internet_connections/backend/provisioning_utils.py
Christian 3311b8e590 Add comprehensive tests for internet connections module, invoice parsing, and subscription provisioning
- Implement tests for the internet connections module, covering routes, IP range creation, and connection validation.
- Add tests for the Invoice2DataService to validate extraction from GlobalConnect invoices.
- Create tests for subscription network provisioning, ensuring proper handling of network items and IP allocations.
- Include validation checks for subtotal mismatches and ensure error handling for missing IP selections.
2026-07-09 23:44:30 +02:00

144 lines
5.3 KiB
Python

import json
import re
from typing import Any, Dict, List, Optional
NETWORK_KINDS = {"internet_access", "ip_allocation"}
def parse_product_attributes(raw: Any) -> Dict[str, Any]:
if raw is None:
return {}
if isinstance(raw, dict):
return raw
if isinstance(raw, str):
text = raw.strip()
if not text:
return {}
try:
parsed = json.loads(text)
except json.JSONDecodeError:
return {}
return parsed if isinstance(parsed, dict) else {}
return {}
def _parse_speed_from_text(text: str) -> Dict[str, Optional[int]]:
match = re.search(r"(\d+)\s*/\s*(\d+)\s*(?:mbit|mbps|gbit|gbps)?", text, re.IGNORECASE)
if not match:
return {"speed_mbps": None, "download_mbps": None, "upload_mbps": None}
download = int(match.group(1))
upload = int(match.group(2))
if re.search(r"(gbit|gbps)", text, re.IGNORECASE):
download *= 1000
upload *= 1000
return {
"speed_mbps": max(download, upload),
"download_mbps": download,
"upload_mbps": upload,
}
def _parse_prefix_from_text(text: str) -> Optional[int]:
match = re.search(r"/(\d{1,2})", text)
if not match:
return None
try:
prefix = int(match.group(1))
except ValueError:
return None
return prefix if 0 <= prefix <= 32 else None
def build_network_product_profile(product: Dict[str, Any], fallback_text: Optional[str] = None) -> Dict[str, Any]:
attributes = parse_product_attributes(product.get("attributes_json"))
network = attributes.get("network") if isinstance(attributes.get("network"), dict) else {}
text = " ".join(
part for part in [
str(product.get("name") or "").strip(),
str(product.get("product_name") or "").strip(),
str(product.get("description") or "").strip(),
str(fallback_text or "").strip(),
]
if part
)
lowered = text.lower()
kind = (
network.get("kind")
or attributes.get("network_kind")
or product.get("network_kind")
or product.get("type")
)
if kind not in NETWORK_KINDS:
if "/3" in lowered and "ip" in lowered:
kind = "ip_allocation"
elif re.search(r"/\d{1,2}", lowered) and "ip" in lowered:
kind = "ip_allocation"
elif "bmcnet" in lowered or "internet" in lowered or "fiber" in lowered:
kind = "internet_access"
else:
kind = None
speeds = _parse_speed_from_text(text)
speed_mbps = network.get("speed_mbps") or attributes.get("speed_mbps") or speeds["speed_mbps"]
download_mbps = network.get("download_mbps") or attributes.get("download_mbps") or speeds["download_mbps"]
upload_mbps = network.get("upload_mbps") or attributes.get("upload_mbps") or speeds["upload_mbps"]
ip_prefix_length = network.get("ip_prefix_length") or attributes.get("ip_prefix_length") or _parse_prefix_from_text(text)
connection_type = network.get("connection_type") or attributes.get("connection_type")
return {
"kind": kind,
"is_network_product": kind in NETWORK_KINDS,
"requires_provisioning": kind in NETWORK_KINDS,
"speed_mbps": int(speed_mbps) if speed_mbps is not None else None,
"download_mbps": int(download_mbps) if download_mbps is not None else None,
"upload_mbps": int(upload_mbps) if upload_mbps is not None else None,
"ip_prefix_length": int(ip_prefix_length) if ip_prefix_length is not None else None,
"connection_type": connection_type,
"attributes": attributes,
}
def summarize_subscription_network_requirements(line_items: List[Dict[str, Any]]) -> Dict[str, Any]:
internet_items: List[Dict[str, Any]] = []
ip_items: List[Dict[str, Any]] = []
for item in line_items or []:
profile = build_network_product_profile(item, fallback_text=item.get("description"))
if not profile["requires_provisioning"]:
continue
entry = {
"subscription_item_id": item.get("id"),
"line_no": item.get("line_no"),
"product_id": item.get("product_id"),
"product_name": item.get("product_name") or item.get("description"),
"description": item.get("description"),
"quantity": item.get("quantity"),
"unit_price": item.get("unit_price"),
"line_total": item.get("line_total"),
"network_kind": profile["kind"],
"speed_mbps": profile["speed_mbps"],
"download_mbps": profile["download_mbps"],
"upload_mbps": profile["upload_mbps"],
"ip_prefix_length": profile["ip_prefix_length"],
"connection_type": profile["connection_type"],
}
if profile["kind"] == "internet_access":
internet_items.append(entry)
elif profile["kind"] == "ip_allocation":
ip_items.append(entry)
primary_internet_item = internet_items[0] if internet_items else None
return {
"requires_provisioning": bool(internet_items or ip_items),
"internet_items": internet_items,
"ip_items": ip_items,
"primary_internet_item": primary_internet_item,
"required_ip_prefixes": [item["ip_prefix_length"] for item in ip_items if item.get("ip_prefix_length") is not None],
}