"""Cliente FOSSBilling Admin API.""" from __future__ import annotations import os import secrets from typing import Any import httpx from app.vm123.role_map import FOSS_GROUP_BY_ROLE FOSS_BASE = os.getenv("FOSSBILLING_URL", "https://financeiro.ligbox.com.br").rstrip("/") FOSS_ADMIN_USER = os.getenv("FOSS_ADMIN_USER", "admin") FOSS_ADMIN_API_KEY = os.getenv("FOSS_ADMIN_API_KEY", os.getenv("FOSS_API_KEY", "")) FOSS_PUBLIC_ADMIN = os.getenv("FOSS_PUBLIC_ADMIN_URL", f"{FOSS_BASE}/admin") class FossConfigError(RuntimeError): pass def _configured() -> bool: return bool(FOSS_ADMIN_API_KEY) def _auth(): if not _configured(): raise FossConfigError("FOSS_ADMIN_API_KEY não configurado no Desk") return (FOSS_ADMIN_USER, FOSS_ADMIN_API_KEY) def _post(path: str, payload: dict) -> dict[str, Any]: url = f"{FOSS_BASE}/api/admin/{path.lstrip('/')}" with httpx.Client(timeout=20.0) as client: res = client.post(url, json=payload, auth=_auth()) if res.status_code >= 400: raise RuntimeError(f"FOSS {path} HTTP {res.status_code}: {res.text[:300]}") try: return res.json() except Exception: return {"raw": res.text} def _extract_id(data: dict[str, Any]) -> int | str | None: if data.get("id") is not None: return data["id"] result = data.get("result") if isinstance(result, dict) and result.get("id") is not None: return result["id"] if result is not None and not isinstance(result, dict): return result return None def _list_items(data: dict[str, Any]) -> list[dict[str, Any]]: result = data.get("result") if isinstance(result, dict) and isinstance(result.get("list"), list): return result["list"] if isinstance(data.get("list"), list): return data["list"] return [] def find_client_by_email(email: str) -> dict[str, Any] | None: data = _post("client/get_list", {"per_page": 50, "search": email.strip()}) items = _list_items(data) if not items: return None needle = email.strip().lower() for item in items: if str(item.get("email", "")).lower() == needle: return item return items[0] if items else None def find_client_by_domain(domain: str) -> dict[str, Any] | None: dom = domain.strip().lower() data = _post("client/get_list", {"per_page": 100}) items = _list_items(data) for item in items: for field in ("company", "company_vat", "email"): val = str(item.get(field, "")).lower() if dom in val: return item return None def find_product_by_slug(slug: str) -> dict[str, Any] | None: needle = slug.strip().lower() data = _post("product/get_list", {"per_page": 100}) for item in _list_items(data): if str(item.get("slug", "")).lower() == needle: return item title_slug = str(item.get("title", "")).lower().replace(" ", "-") if title_slug == needle: return item return None def create_client(*, mapped: dict[str, Any], password: str | None = None) -> dict[str, Any]: """Cria cliente FOSS — idempotente por email (Spec 043 §4.1).""" email = str(mapped.get("email") or "").strip().lower() if not email: raise ValueError("email obrigatório para FOSS client") existing = find_client_by_email(email) if existing: cid = existing.get("id") return { "client_id": cid, "email": email, "created": False, "reused": True, } pwd = password or secrets.token_urlsafe(14) payload: dict[str, Any] = { "email": email, "first_name": mapped.get("first_name") or "", "last_name": mapped.get("last_name") or "", "company": mapped.get("company") or "", "company_vat": mapped.get("company_vat") or "", "phone": mapped.get("phone") or "", "address_1": mapped.get("address_1") or "", "city": mapped.get("city") or "", "state": mapped.get("state") or "", "postcode": mapped.get("postcode") or "", "country": mapped.get("country") or "BR", "currency": mapped.get("currency") or "BRL", "status": mapped.get("status") or "active", "password": pwd, "password_confirm": pwd, } result = _post("client/create", payload) cid = _extract_id(result) return { "client_id": cid, "email": email, "created": True, "reused": False, "admin_url": FOSS_PUBLIC_ADMIN, } def _split_domain(domain: str) -> tuple[str, str]: parts = domain.strip().lower().split(".") if len(parts) < 2: return (parts[0], "com.br") return (parts[0], ".".join(parts[1:])) def create_order( *, client_id: int | str, product_slug: str, domain: str, period: str = "1M", activate: bool = True, config: dict[str, Any] | None = None, manager_email: str | None = None, manager_name: str | None = None, ) -> dict[str, Any]: """Cria e activa pedido hosting FOSS (Spec 043 §4.2).""" product = find_product_by_slug(product_slug) if not product: raise RuntimeError(f"produto FOSS não encontrado: {product_slug}") product_id = product.get("id") sld, tld = _split_domain(domain) payload: dict[str, Any] = { "client_id": client_id, "product_id": product_id, "period": period, "currency": "BRL", "activate": 1 if activate else 0, "config": { "domain": { "action": "owndomain", "owndomain_sld": sld, "owndomain_tld": tld, }, }, } if config: for key, val in config.items(): if val is not None: payload["config"][key] = val if manager_email: payload["config"]["manager_email"] = manager_email if manager_name: payload["config"]["manager_name"] = manager_name payload["config"]["domain_name"] = domain result = _post("order/create", payload) order_id = _extract_id(result) return { "order_id": order_id, "client_id": client_id, "product_id": product_id, "product_slug": product_slug, "domain": domain, "activated": activate, "created": True, } def staff_group_name_for_role(desk_role: str) -> str | None: return FOSS_GROUP_BY_ROLE.get(desk_role) def create_staff(*, email: str, name: str, desk_role: str, password: str | None = None) -> dict[str, Any]: """Cria staff FOSS — grupo staff deve existir no Admin (manual v1).""" group_name = staff_group_name_for_role(desk_role) if not group_name: return {"skipped": True, "reason": f"role {desk_role} sem grupo FOSS"} pwd = password or secrets.token_urlsafe(14) payload: dict[str, Any] = { "email": email.strip().lower(), "name": name, "password": pwd, "status": "active", "admin_group_id": group_name, } try: result = _post("staff/create", payload) except RuntimeError as exc: if "admin_group" in str(exc).lower() or "group" in str(exc).lower(): return {"skipped": True, "reason": str(exc), "group": group_name} raise return { "foss_staff_id": result.get("id") or result.get("result"), "email": email, "group": group_name, "admin_url": FOSS_PUBLIC_ADMIN, "created": True, }