Saltar a contenido

0006. Aplicaciones, módulos y menú (catálogo de navegación)

Estado: Propuesto Fecha: 2026-09-08 Autor: Duval Alcivar Módulo(s) afectado(s): svc-identidad (core_sigfa), frontend (React 18)

Contexto

La plataforma se organiza en aplicaciones (ERP, CRM, futuros), dentro de cada aplicación hay módulos (rrhh, inventario, contabilidad) y dentro de cada módulo un menú de páginas en hasta 3 niveles. El ADR transversal 0013 ya define el modelo de datos de esa navegación (cat_aplicaciones, cfg_modulos, cfg_menu, cfg_menu_acciones) con dueño svc-identidad.

Este ADR consolida ese modelo como parte del catálogo de identidad, define la relación empresa ↔ aplicación (rel_empresa_aplicacion) y referencia el catálogo inicial (ADRs 0013/0014/0015).

Decisión

Adoptar el modelo del ADR transversal 0013 tal cual (una sola fuente de verdad: core_sigfa, esquema core.identidad). Este ADR agrega las piezas que faltaban y deja claro el orden: los catálogos globales (ADR 0003), la empresa (ADR 0004), y aquí qué aplicaciones/módulos/menú ve cada empresa.

cat_aplicaciones (ERP, CRM)                    ← global (sin empresa_id)
   │  rel_empresa_aplicacion (empresa ↔ app)
   │
cfg_modulos (rrhh, inventario, contabilidad)   ← empresa_id + aplicacion_id (RLS)
   │
cfg_menu (nómina → registro, vacaciones)       ← modulo_id + padre_id (self, 3 niveles)
   │
cfg_menu_acciones (crear / editar / ver / eliminar)

1. Tablas adoptadas (del ADR transversal 0013)

Tabla PK empresa_id RLS Descripción
cat_aplicaciones UUID no Catálogo global de aplicaciones (ERP, CRM)
cfg_modulos UUID BIGINT Módulos de cada aplicación, por empresa
cfg_menu UUID BIGINT Páginas del módulo (máx. 3 niveles, padre_id)
cfg_menu_acciones UUID BIGINT Acciones por página (crear/editar/eliminar)

El DDL completado (SQL PG + SQLAlchemy) está en el ADR transversal 0013; aquí solo se referencia para no duplicarlo. Cada tabla usa la auditoría estándar (created_at, updated_at, deleted_at, created_by, updated_by) y is_activo / is_visible.

2. rel_empresa_aplicacion — qué aplicaciones usa cada empresa

La empresa decide qué aplicaciones contrata (solo ERP, solo CRM, ambas). No viaja en el JWT: se consulta en el panel al entrar.

Campo Tipo Notas
id UUID PK
empresa_id BIGINT NOT NULL FK → mae_empresas (ADR 0004)
aplicacion_id UUID NOT NULL FK → cat_aplicaciones
is_activo BOOLEAN DEFAULT TRUE
created_at / updated_at TIMESTAMPTZ default now()
deleted_at TIMESTAMPTZ NULL
created_by / updated_by UUID

UNIQUE (empresa_id, aplicacion_id). Si no hay fila, la empresa no usa esa aplicación (no aparece en su panel).

CREATE TABLE core.identidad.rel_empresa_aplicacion (
    id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    empresa_id    BIGINT NOT NULL,
    aplicacion_id UUID NOT NULL,
    is_activo     BOOLEAN DEFAULT TRUE,
    created_at    TIMESTAMPTZ DEFAULT now(),
    updated_at    TIMESTAMPTZ DEFAULT now(),
    deleted_at    TIMESTAMPTZ NULL,
    created_by    UUID,
    updated_by    UUID,

    CONSTRAINT uq_rel_empresa_aplicacion UNIQUE (empresa_id, aplicacion_id),
    CONSTRAINT fk_rel_empresa_aplicacion_empresa
        FOREIGN KEY (empresa_id) REFERENCES core.identidad.mae_empresas(id),
    CONSTRAINT fk_rel_empresa_aplicacion_aplicacion
        FOREIGN KEY (aplicacion_id) REFERENCES core.identidad.cat_aplicaciones(id)
);

ALTER TABLE core.identidad.rel_empresa_aplicacion ENABLE ROW LEVEL SECURITY;
CREATE POLICY aislamiento_empresa ON core.identidad.rel_empresa_aplicacion
    USING (empresa_id = NULLIF(current_setting('app.current_empresa_id', true), '')::BIGINT);
class RelEmpresaAplicacion(Base):
    __tablename__ = "rel_empresa_aplicacion"
    __table_args__ = (
        UniqueConstraint("empresa_id", "aplicacion_id", name="uq_rel_empresa_aplicacion"),
        {"schema": "core.identidad"},
    )
    id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
    empresa_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("core.identidad.mae_empresas.id"), nullable=False)
    aplicacion_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("core.identidad.cat_aplicaciones.id"), nullable=False)
    is_activo: Mapped[bool] = mapped_column(Boolean, default=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
    deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
    created_by: Mapped[uuid.UUID | None] = mapped_column(as_uuid=True)
    updated_by: Mapped[uuid.UUID | None] = mapped_column(as_uuid=True)

3. Flujo de datos en el panel

1. Login → usuario elige la EMPRESA (ADR 0010)
2. svc-identidad resuelve, en UNA sola consulta, las aplicaciones de esa
   empresa (rel_empresa_aplicacion) Y sus módulos (cfg_modulos)
3. Frontend muestra UNA sola pantalla con todas las aplicaciones y sus
   módulos agrupados:

     CORE
       permisos, tickets, ...
     ERP
       produccion, rrhh, ...
     CRM
       promotores, ventas, ...

4. Al entrar a un módulo → GET /api/v1/navegacion/menu?modulo_id=... → árbol de
   páginas (3 niveles con submenús) — ver ADR 0012

Las aplicaciones y sus módulos se cargan juntos, en una sola pantalla posterior al login: no hay un paso "elegir aplicación" intermedio. El módulo es el punto de entrada; la página se elige dentro del módulo.

4. Catálogo inicial (resuelto)

  • El catálogo inicial de módulos y acciones por aplicación (plantilla core/erp/crm y clon por empresa) está en el ADR 0013; el catálogo de permisos derivado de esas acciones en el 0014 y la asignación fina por usuario en el 0015.

Alternativas descartadas

  • Una sola tabla auto-referenciada (módulo = nivel 1) — descartado: módulo y página son entidades distintas (el módulo agrupa, la página navega y se audita); separarlos permite la grilla de módulos sin mezclar niveles.
  • cat_aplicaciones con empresa_id — descartado: duplicaría la fila ERP/CRM por empresa; rel_empresa_aplicacion expresa lo mismo sin redundancia.
  • Módulos/menú en la BD de cada producto — descartado: fragmenta la navegación y obliga a consultar varias bases para armar el login.

Consecuencias

  • La navegación se resuelve en un solo servicio (svc-identidad) sin cruzar bases.
  • La relación usuario → páginas del menú y el filtro del panel por permisos están en el ADR 0007 (permisos individuales) y su resolución en ejecución en el ADR 0012 (navegación por permisos).
  • El catálogo inicial de módulos/acciones (catálogo seed) está en el ADR 0013; su clon transaccional al alta de empresa en el ADR 0016.