From 387b3c0e789bd5967d6f6aa76e3375444aa91dc9 Mon Sep 17 00:00:00 2001 From: Ernesto Herrera Date: Fri, 17 Jul 2026 13:20:50 -0600 Subject: [PATCH] =?UTF-8?q?feat(core,crm):=20auto-sync=20de=20tenants=20Wo?= =?UTF-8?q?rkspace=E2=86=92CRM=20+=20mostrar=20tenant=20de=20la=20compa?= =?UTF-8?q?=C3=B1=C3=ADa=20activa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- backend/api/v1/modules/core/auth/routes.py | 84 +++++++++++++++++-- .../components/sidebar/team-switcher.svelte | 15 +++- frontend/src/lib/stores/company.svelte.ts | 2 + 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/backend/api/v1/modules/core/auth/routes.py b/backend/api/v1/modules/core/auth/routes.py index e5ee398..50d6869 100644 --- a/backend/api/v1/modules/core/auth/routes.py +++ b/backend/api/v1/modules/core/auth/routes.py @@ -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) diff --git a/frontend/src/lib/components/sidebar/team-switcher.svelte b/frontend/src/lib/components/sidebar/team-switcher.svelte index 547c9c1..f4cd37f 100644 --- a/frontend/src/lib/components/sidebar/team-switcher.svelte +++ b/frontend/src/lib/components/sidebar/team-switcher.svelte @@ -145,9 +145,18 @@ > Tenant {#if userTenants.length === 0} - - Sin tenant asignado - + {#if companyStore.activeCompany?.tenant_name} + +
+ +
+ {companyStore.activeCompany.tenant_name} +
+ {:else} + + Sin tenant asignado + + {/if} {:else} {#each userTenants as tenant (tenant.id)}