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 | sí | Módulos de cada aplicación, por empresa |
cfg_menu |
UUID | BIGINT | sí | Páginas del módulo (máx. 3 niveles, padre_id) |
cfg_menu_acciones |
UUID | BIGINT | sí | 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_aplicacionesconempresa_id— descartado: duplicaría la fila ERP/CRM por empresa;rel_empresa_aplicacionexpresa 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.