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>
262 lines
9 KiB
Python
262 lines
9 KiB
Python
"""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
|