feat(core,crm): auto-sync de tenants Workspace→CRM + mostrar tenant de la compañía activa

- assignable-tenants: para hub_admin, sincroniza automáticamente los tenants del
  Workspace (Hub GET /hub/tenants) a core.tenants con su mismo ID, y los devuelve.
  Así los tenants creados en el Workspace aparecen solos en el CRM para asignarles
  compañías (best-effort con el token KC de la sesión). CRM→Workspace ya lo hace
  Organizaciones (POST /hub/tenants).
- my-companies devuelve tenant_name/tenant_slug (join core.tenants).
- Switcher muestra el tenant de la compañía activa (antes "Sin tenant asignado").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ernesto Herrera
2026-07-17 13:20:50 -06:00
parent 706da34f4a
commit 387b3c0e78
3 changed files with 91 additions and 10 deletions

View File

@@ -489,8 +489,9 @@ async def get_my_companies(
rows = db.execute(
text(
"""
SELECT c.id, c.name, c.rfc, c.logo, c.tenant_id
SELECT c.id, c.name, c.rfc, c.logo, c.tenant_id, t.name, t.slug
FROM a76.company c
LEFT JOIN core.tenants t ON t.id = c.tenant_id
WHERE c.id IN (
SELECT company_id FROM core.user_tenants
WHERE keycloak_user_id = :uid AND is_active AND company_id IS NOT NULL
@@ -509,6 +510,8 @@ async def get_my_companies(
"id": int(r[0]),
"name": r[1] or "Empresa",
"tenant_id": int(r[4]),
"tenant_name": r[5],
"tenant_slug": r[6],
"rfc": r[2],
"logo": r[3],
"is_active": True,
@@ -523,23 +526,90 @@ class _CreateCompanyDTO(BaseModel):
rfc: Optional[str] = None
async def _sync_tenants_from_hub(request: Request, db: Session) -> None:
"""
Auto-sync Workspace→CRM: trae los tenants del Workspace (Hub GET /hub/tenants) y
los da de alta/actualiza en core.tenants con su MISMO ID del Workspace. Así los
tenants creados en el Workspace aparecen solos en el CRM para asignarles compañías.
Best-effort: usa el token KC de la sesión (valkey); si no está fresco o el Hub no
responde, no bloquea (se devuelven los tenants ya sincronizados).
"""
import httpx
from sqlalchemy import text as _text
from core.config import settings
from core import session_store
from api.v1.modules.core.tenants.models import Tenant, TenantType
sid = request.cookies.get("crm_sid") if request else None
kc_token = None
if sid:
sess = session_store.get_session(sid)
kc_token = (sess or {}).get("access_token")
if not kc_token:
return
try:
async with httpx.AsyncClient(timeout=8.0) as client:
r = await client.get(
f"{settings.HUB_URL}api/v1/hub/tenants",
headers={"Authorization": f"Bearer {kc_token}"},
)
if r.status_code != 200:
logger.info("sync-tenants: Hub devolvió %s — sin sincronizar", r.status_code)
return
payload = r.json()
items = payload.get("tenants", []) if isinstance(payload, dict) else (payload or [])
for t in items:
tid = t.get("id")
if tid is None:
continue
name = t.get("name") or t.get("display_name") or t.get("slug")
slug = t.get("slug") or f"tenant-{tid}"
existing = db.query(Tenant).filter(Tenant.id == int(tid)).first()
if existing:
if name and existing.name != name:
existing.name = name
else:
db.add(Tenant(
id=int(tid), name=name or slug, slug=slug,
keycloak_realm=slug, type=TenantType.SHARED, is_active=True,
))
db.commit()
db.execute(_text("SELECT setval('core.tenants_id_seq', (SELECT MAX(id) FROM core.tenants))"))
db.commit()
except Exception as exc:
logger.warning("sync-tenants desde Hub falló (no bloquea): %s", exc)
try:
db.rollback()
except Exception:
pass
@router.get("/assignable-tenants")
async def assignable_tenants(
request: Request,
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_core_db),
):
"""
Tenants disponibles para asignar una compañía nueva. El tenant lo crea el
Workspace; aquí solo se elige. hub_admin ve todos; un usuario con tenant ve el suyo.
Tenants disponibles para asignar una compañía. El tenant lo crea el Workspace;
aquí solo se elige. hub_admin ve TODOS (auto-sincronizados del Hub); un usuario
con tenant ve el suyo.
"""
from api.v1.modules.core.tenants.models import Tenant
from core.security import resolve_effective_tenant_id_from_user, is_hub_admin
q = db.query(Tenant).filter(Tenant.is_active == True) # noqa: E712
if is_hub_admin(current_user):
# Sincroniza automáticamente los tenants del Workspace antes de listar.
await _sync_tenants_from_hub(request, db)
rows = db.query(Tenant).filter(Tenant.is_active == True).order_by(Tenant.id).all() # noqa: E712
return [{"id": t.id, "name": t.name, "slug": t.slug} for t in rows]
tid = resolve_effective_tenant_id_from_user(current_user)
if tid and not is_hub_admin(current_user):
q = q.filter(Tenant.id == int(tid))
return [{"id": t.id, "name": t.name, "slug": t.slug} for t in q.order_by(Tenant.id).all()]
if tid:
t = db.query(Tenant).filter(Tenant.id == int(tid), Tenant.is_active == True).first() # noqa: E712
return [{"id": t.id, "name": t.name, "slug": t.slug}] if t else []
return []
@router.post("/companies", status_code=201)