Adiciona mapper de campos, endpoint GET activation-preview, modal FOSS/OpenPanel/Odoo e canManageBilling para staff activar conta. Co-authored-by: Cursor <cursoragent@cursor.com>
243 lines
8.4 KiB
Python
243 lines
8.4 KiB
Python
"""Mapeamento activação cliente — Spec 043 activation-field-mapping.md."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Any
|
||
|
||
DEFAULT_PLAN_CODE = "ligbox-mail-business"
|
||
|
||
PLAN_DEFAULTS: dict[str, dict[str, int]] = {
|
||
"ligbox-mail-starter": {"seats": 10, "mail_gb": 20, "files_gb": 100},
|
||
"ligbox-mail-business": {"seats": 25, "mail_gb": 30, "files_gb": 200},
|
||
"ligbox-mail-enterprise": {"seats": 50, "mail_gb": 50, "files_gb": 500},
|
||
"ligbox-mail-custom": {"seats": 25, "mail_gb": 30, "files_gb": 200},
|
||
"ligbox-site-cms": {"seats": 1, "mail_gb": 0, "files_gb": 10},
|
||
}
|
||
|
||
ACTIVATABLE_STATES = frozenset(
|
||
{"awaiting_billing_validation", "provisioning", "policy_pending"}
|
||
)
|
||
|
||
|
||
def _digits_only(value: str | None) -> str:
|
||
if not value:
|
||
return ""
|
||
return re.sub(r"\D", "", str(value))
|
||
|
||
|
||
def normalize_company_profile(
|
||
profile: dict[str, Any] | None,
|
||
*,
|
||
domain: str,
|
||
) -> dict[str, Any]:
|
||
"""Unifica manager_* e address vindos do webhook wizard."""
|
||
p = dict(profile or {})
|
||
addr = p.get("address")
|
||
if not isinstance(addr, dict):
|
||
addr = {}
|
||
p["address"] = {
|
||
"country": addr.get("country") or "BR",
|
||
"city": addr.get("city") or "",
|
||
"postal_code": addr.get("postal_code") or "",
|
||
"street": addr.get("street") or "",
|
||
"state": addr.get("state") or "",
|
||
}
|
||
p.setdefault("domain", domain.strip().lower())
|
||
if not p.get("manager_email"):
|
||
p["manager_email"] = f"admin@{domain.strip().lower()}"
|
||
if not p.get("manager_name"):
|
||
p["manager_name"] = p.get("trade_name") or p.get("legal_name") or ""
|
||
return p
|
||
|
||
|
||
def split_manager_name(name: str | None) -> tuple[str, str]:
|
||
parts = (name or "").strip().split(None, 1)
|
||
if not parts:
|
||
return ("", "")
|
||
if len(parts) == 1:
|
||
return (parts[0], "")
|
||
return (parts[0], parts[1])
|
||
|
||
|
||
def derive_openpanel_username(domain: str, profile: dict[str, Any] | None = None) -> str:
|
||
"""Sugestão username hub — Spec 024 (7 chars alfanum + sufixo)."""
|
||
existing = (profile or {}).get("openpanel_username")
|
||
if existing:
|
||
return str(existing).strip().lower()
|
||
label = re.sub(r"[^a-z0-9]", "", domain.split(".")[0].lower()) or "ligbox"
|
||
if len(label) <= 7:
|
||
return f"{label}x"[:16]
|
||
return f"{label[:7]}x"[:16]
|
||
|
||
|
||
def resolve_plan_config(plan_code: str | None, config: dict[str, Any] | None = None) -> tuple[str, dict[str, int]]:
|
||
code = (plan_code or DEFAULT_PLAN_CODE).strip() or DEFAULT_PLAN_CODE
|
||
defaults = dict(PLAN_DEFAULTS.get(code, PLAN_DEFAULTS[DEFAULT_PLAN_CODE]))
|
||
if config:
|
||
for key in ("seats", "mail_gb", "files_gb"):
|
||
if config.get(key) is not None:
|
||
defaults[key] = int(config[key])
|
||
return code, defaults
|
||
|
||
|
||
def map_foss_client(profile: dict[str, Any], domain: str) -> dict[str, Any]:
|
||
first, last = split_manager_name(profile.get("manager_name"))
|
||
email = (profile.get("manager_email") or profile.get("email_billing") or "").strip().lower()
|
||
addr = profile.get("address") or {}
|
||
return {
|
||
"email": email,
|
||
"first_name": first,
|
||
"last_name": last,
|
||
"company": profile.get("legal_name") or profile.get("trade_name") or domain,
|
||
"company_vat": _digits_only(profile.get("tax_id")),
|
||
"phone": profile.get("phone_mobile") or profile.get("contact_phone") or profile.get("phone_landline") or "",
|
||
"address_1": addr.get("street") or "",
|
||
"city": addr.get("city") or "",
|
||
"state": addr.get("state") or "",
|
||
"postcode": addr.get("postal_code") or "",
|
||
"country": addr.get("country") or "BR",
|
||
"currency": "BRL",
|
||
"status": "active",
|
||
"custom_ligbox_domain": domain,
|
||
}
|
||
|
||
|
||
def map_foss_order(
|
||
*,
|
||
domain: str,
|
||
profile: dict[str, Any],
|
||
plan_code: str,
|
||
config: dict[str, int],
|
||
foss_client_id: str | int | None = None,
|
||
) -> dict[str, Any]:
|
||
return {
|
||
"client_id": foss_client_id,
|
||
"product_slug": plan_code,
|
||
"period": "1M",
|
||
"domain": domain,
|
||
"manager_email": profile.get("manager_email"),
|
||
"manager_name": profile.get("manager_name"),
|
||
"config": {
|
||
"seats": config.get("seats"),
|
||
"mail_gb": config.get("mail_gb"),
|
||
"files_gb": config.get("files_gb"),
|
||
},
|
||
}
|
||
|
||
|
||
def map_openpanel(
|
||
*,
|
||
domain: str,
|
||
profile: dict[str, Any],
|
||
plan_code: str,
|
||
config: dict[str, int],
|
||
) -> dict[str, Any]:
|
||
username = derive_openpanel_username(domain, profile)
|
||
return {
|
||
"username": username,
|
||
"email": profile.get("manager_email"),
|
||
"plan_name": plan_code,
|
||
"domain": domain,
|
||
"metadata": {
|
||
"bundle_type": "ligbox_mail" if plan_code.startswith("ligbox-mail") else "ligbox_hosting",
|
||
"max_seats": config.get("seats"),
|
||
"mail_gb_per_seat": config.get("mail_gb"),
|
||
"files_gb_per_seat": config.get("files_gb"),
|
||
"easydmarc": True,
|
||
"wizard_domain": domain,
|
||
"manager_name": profile.get("manager_name"),
|
||
"manager_email": profile.get("manager_email"),
|
||
},
|
||
}
|
||
|
||
|
||
def map_odoo_partner(profile: dict[str, Any], domain: str, foss_client_id: str | int | None = None) -> dict[str, Any]:
|
||
addr = profile.get("address") or {}
|
||
return {
|
||
"name": profile.get("legal_name") or profile.get("trade_name") or domain,
|
||
"display_name": profile.get("trade_name") or profile.get("legal_name") or domain,
|
||
"vat": profile.get("tax_id") or "",
|
||
"email": profile.get("email_billing") or profile.get("manager_email") or "",
|
||
"phone": profile.get("phone_mobile") or profile.get("contact_phone") or "",
|
||
"street": addr.get("street") or "",
|
||
"city": addr.get("city") or "",
|
||
"zip": addr.get("postal_code") or "",
|
||
"country_code": addr.get("country") or "BR",
|
||
"company_type": "company",
|
||
"customer_rank": 1,
|
||
"ref": domain,
|
||
"comment": {
|
||
"manager_email": profile.get("manager_email"),
|
||
"foss_client_id": foss_client_id,
|
||
"domain": domain,
|
||
},
|
||
}
|
||
|
||
|
||
def map_wizard_mail_bundle(
|
||
*,
|
||
domain: str,
|
||
profile: dict[str, Any],
|
||
config: dict[str, int],
|
||
foss_order_id: str | int | None = None,
|
||
) -> dict[str, Any]:
|
||
return {
|
||
"domain": domain,
|
||
"admin_email": profile.get("manager_email"),
|
||
"admin_name": profile.get("manager_name"),
|
||
"seats": config.get("seats"),
|
||
"mail_gb_per_seat": config.get("mail_gb"),
|
||
"files_gb_per_seat": config.get("files_gb"),
|
||
"foss_order_id": foss_order_id,
|
||
}
|
||
|
||
|
||
def build_activation_preview(
|
||
account: dict[str, Any],
|
||
*,
|
||
plan_code: str | None = None,
|
||
config: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
domain = str(account.get("domain") or "").strip().lower()
|
||
profile = normalize_company_profile(account.get("company_profile"), domain=domain)
|
||
code, cfg = resolve_plan_config(plan_code or account.get("plan_code"), config)
|
||
foss_id = account.get("external_customer_id")
|
||
billing_state = account.get("billing_state") or "awaiting_billing_validation"
|
||
can_activate = billing_state in ACTIVATABLE_STATES and not account.get("recurrence_active")
|
||
|
||
return {
|
||
"billing_account_id": account.get("id"),
|
||
"domain": domain,
|
||
"billing_state": billing_state,
|
||
"can_activate": can_activate,
|
||
"plan_code": code,
|
||
"config": cfg,
|
||
"company_profile": profile,
|
||
"mapped": {
|
||
"foss_client": map_foss_client(profile, domain),
|
||
"foss_order": map_foss_order(
|
||
domain=domain,
|
||
profile=profile,
|
||
plan_code=code,
|
||
config=cfg,
|
||
foss_client_id=foss_id,
|
||
),
|
||
"openpanel": map_openpanel(domain=domain, profile=profile, plan_code=code, config=cfg),
|
||
"odoo_partner": map_odoo_partner(profile, domain, foss_client_id=foss_id),
|
||
"wizard_mail_bundle": map_wizard_mail_bundle(
|
||
domain=domain,
|
||
profile=profile,
|
||
config=cfg,
|
||
),
|
||
},
|
||
"existing_ids": {
|
||
"foss_client_id": foss_id,
|
||
"external_subscription_id": account.get("external_subscription_id"),
|
||
"openpanel_username": profile.get("openpanel_username"),
|
||
},
|
||
"notes": (
|
||
"Preview read-only — Fase 1 Spec 043. "
|
||
"POST /activate implementado nas Fases 2–5."
|
||
),
|
||
}
|