"""Orquestração activação cliente — Spec 043 Fases 2–5.""" from __future__ import annotations import json import logging import secrets from datetime import datetime, timezone from typing import Any from app import billing_store from app.activation_mapper import ( ACTIVATABLE_STATES, build_activation_preview, map_foss_client, map_odoo_partner, map_openpanel, map_wizard_mail_bundle, normalize_company_profile, resolve_plan_config, ) from app.vm123 import foss_client, odoo_client, openpanel_client, wizard_client log = logging.getLogger(__name__) class ActivationError(RuntimeError): pass def _now() -> str: return datetime.now(timezone.utc).isoformat() def activate_billing_account( conn, account_id: int, *, plan_code: str | None = None, config: dict[str, Any] | None = None, provision_mail: bool = True, notes: str | None = None, activated_by: str, skip_openpanel: bool = False, ) -> dict[str, Any]: """Sequência FOSS → OpenPanel → Odoo → wizard mail-bundle.""" acc = billing_store.get_account(conn, account_id, mask=False) if not acc: raise ActivationError("conta não encontrada") if acc.get("recurrence_active") or acc.get("billing_state") == "billing_active": raise ActivationError("conta já activa") if acc.get("billing_state") not in ACTIVATABLE_STATES: raise ActivationError(f"estado {acc.get('billing_state')} não permite activação") preview = build_activation_preview(acc, plan_code=plan_code, config=config) domain = preview["domain"] profile = preview["company_profile"] code = preview["plan_code"] cfg = preview["config"] steps: dict[str, Any] = {} partial_errors: list[str] = [] billing_store.patch_account( conn, account_id, billing_state="provisioning", plan_code=code, activated_by=activated_by, activated_at=_now(), ) foss_client_id = acc.get("external_customer_id") foss_order_id = acc.get("external_subscription_id") # —— Fase 2: FOSS client + order —— try: if foss_client_id: steps["foss"] = { "client_id": foss_client_id, "created": False, "reused": True, } else: mapped_client = map_foss_client(profile, domain) foss_step = foss_client.create_client(mapped=mapped_client) foss_client_id = foss_step.get("client_id") steps["foss"] = { "client_id": foss_client_id, "created": bool(foss_step.get("created")), "reused": bool(foss_step.get("reused")), } if foss_client_id: billing_store.patch_account( conn, account_id, external_customer_id=str(foss_client_id), ) except Exception as exc: log.warning("FOSS client failed domain=%s: %s", domain, exc) steps["foss"] = {"error": str(exc)} partial_errors.append(f"FOSS client: {exc}") if foss_client_id and not foss_order_id: try: order_step = foss_client.create_order( client_id=foss_client_id, product_slug=code, domain=domain, config=cfg, manager_email=profile.get("manager_email"), manager_name=profile.get("manager_name"), activate=True, ) foss_order_id = order_step.get("order_id") steps["foss"]["order_id"] = foss_order_id steps["foss"]["order_created"] = True if foss_order_id: billing_store.patch_account( conn, account_id, external_subscription_id=str(foss_order_id), ) except Exception as exc: log.warning("FOSS order failed domain=%s: %s", domain, exc) steps.setdefault("foss", {})["order_error"] = str(exc) partial_errors.append(f"FOSS order: {exc}") elif foss_order_id: steps.setdefault("foss", {})["order_id"] = foss_order_id steps["foss"]["order_created"] = False # —— Fase 3: OpenPanel (fallback directo se módulo FOSS não provisionou) —— op_mapped = map_openpanel(domain=domain, profile=profile, plan_code=code, config=cfg) username = op_mapped["username"] if skip_openpanel or not openpanel_client.bridge_configured(): steps["openpanel"] = { "skipped": True, "reason": "bridge não configurado" if not openpanel_client.bridge_configured() else "skip_openpanel", "username": username, } elif profile.get("openpanel_username"): steps["openpanel"] = { "username": profile.get("openpanel_username"), "domain": domain, "created": False, "reused": True, } else: try: panel_pwd = secrets.token_urlsafe(12) op_step = openpanel_client.provision_user_safe( username=username, password=panel_pwd, email=str(profile.get("manager_email") or ""), domain=domain, plan_name=code, metadata=op_mapped.get("metadata"), ) steps["openpanel"] = { "username": username, "domain": domain, "success": True, "created": bool(op_step.get("created", True)), "reused": bool(op_step.get("reused")), } billing_store.merge_company_profile( conn, account_id, { "openpanel_username": username, "openpanel_domain": domain, }, ) except Exception as exc: log.warning("OpenPanel failed domain=%s: %s", domain, exc) steps["openpanel"] = {"username": username, "error": str(exc)} partial_errors.append(f"OpenPanel: {exc}") # —— Fase 4: Odoo partner —— odoo_partner_id = acc.get("odoo_partner_id") try: partner_mapped = map_odoo_partner(profile, domain, foss_client_id=foss_client_id) if odoo_partner_id: steps["odoo"] = { "partner_id": int(odoo_partner_id), "created": False, "reused": True, } else: odoo_step = odoo_client.upsert_customer_partner(mapped=partner_mapped) odoo_partner_id = odoo_step.get("partner_id") steps["odoo"] = { "partner_id": odoo_partner_id, "created": bool(odoo_step.get("created")), "updated": bool(odoo_step.get("updated")), } if odoo_partner_id: billing_store.patch_account( conn, account_id, odoo_partner_id=str(odoo_partner_id), ) except Exception as exc: log.warning("Odoo partner failed domain=%s: %s", domain, exc) steps["odoo"] = {"error": str(exc)} partial_errors.append(f"Odoo: {exc}") # —— Fase 5: Wizard mail-bundle —— if provision_mail and code.startswith("ligbox-mail"): try: mail_payload = map_wizard_mail_bundle( domain=domain, profile=profile, config=cfg, foss_order_id=foss_order_id, ) wizard_step = wizard_client.provision_mail_bundle(mail_payload) steps["wizard"] = wizard_step if wizard_step.get("skipped"): steps["wizard"]["provisioned"] = False else: steps["wizard"]["provisioned"] = bool(wizard_step.get("provisioned")) except Exception as exc: log.warning("Wizard mail-bundle failed domain=%s: %s", domain, exc) steps["wizard"] = {"error": str(exc), "provisioned": False} partial_errors.append(f"Wizard: {exc}") else: steps["wizard"] = {"skipped": True, "reason": "provision_mail=false ou plano não-mail"} ok = not partial_errors final_state = "billing_active" if ok else "provisioning" billing_store.patch_account( conn, account_id, billing_state=final_state, recurrence_active=ok, plan_code=code, ) billing_store.save_provision_log( conn, account_id, { "activated_at": _now(), "activated_by": activated_by, "notes": notes, "steps": steps, "partial_errors": partial_errors, "ok": ok, }, ) out = { "ok": ok, "billing_account_id": account_id, "domain": domain, "billing_state": final_state, "steps": steps, "activated_at": _now(), "activated_by": activated_by, } if partial_errors: out["message"] = " — ".join(partial_errors) return out