ligbox-ops-platform/projects/ops-desk/api/app/client_activation.py
Ligbox Spec Hub c815a32bbc feat(desk): Spec 043 Fases 2–6 — activar conta FOSS/OpenPanel/Odoo
Orquestra create_client/order FOSS, bridge OpenPanel com metadata,
upsert res.partner Odoo, mail-bundle wizard e webhook order-activated.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 15:19:54 +00:00

262 lines
9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Orquestração activação cliente — Spec 043 Fases 25."""
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