Appearance
Feature Flag — modulo_portal_clientes
Módulo: Portal de Clientes — Arquitectura Tipo: Architecture Estado: Implementado Fecha: 2026-05-12
Descripción
modulo_portal_clientes es el flag que controla si el Portal de Clientes está habilitado para un tenant. Opera como submódulo de membresías: solo puede estar activo si modulo_membresias = 1.
El flag afecta tres capas de forma coordinada:
- Migraciones — las tablas
portal_*no se crean si el flag está en 0 - Seeds — las claves
portal.*endata_configno se insertan si el flag está en 0 - Servicio — el endpoint de configuración de gateway retorna 403 si el portal está deshabilitado
Ubicación del Flag
sql
-- Tabla: sistema (schema public, nivel EMPRESA)
-- Columna: modulos (JSONB)
{
"modulo_membresias": 1,
"modulo_portal_clientes": 1 -- 0 = deshabilitado, 1 = habilitado
}El flag existe en sistema.modulos con default backward-compatible:
- Si
modulo_membresias = 1→modulo_portal_clientesse inserta con valor1 - Si
modulo_membresias = 0→modulo_portal_clientesse inserta con valor0 - La migración es idempotente (solo inserta si la clave no existe)
Jerarquía de Módulos
modulo_ventas (prerequisito de membresías)
└─ modulo_ctacte (prerequisito de membresías)
└─ modulo_membresias
└─ modulo_portal_clientes ← este flagisPortalClientesEnabled() = isMembresiasEnabled() AND isModuleEnabled('modulo_portal_clientes')
Si membresías está deshabilitado, el portal está deshabilitado sin importar el valor del flag propio.
Guard de Migraciones
Archivo: migrations/MigrationTrait.php
php
protected function isPortalClientesEnabled(): bool
{
return $this->isMembresiasEnabled() && $this->isModuleEnabled('modulo_portal_clientes');
}Las migraciones del portal implementan shouldExecute():
php
public function shouldExecute(): bool
{
return $this->shouldRunOnLevel()
&& $this->isPortalClientesEnabled()
&& !$this->haveTable('portal_users');
}Si modulo_portal_clientes = 0, las migraciones hacen skip con log de confirmación. Las tablas portal_users, portal_payments, etc. no se crean.
Guard de Servicio
Archivo: Modules/Portal/Application/Services/PortalConfigService.php
El método guardGatewayAccess() verifica dos condiciones en orden:
php
private function guardGatewayAccess(): void
{
if (!$this->authService->hasPermission('config.gateway')) {
$this->logger->warning('AccessDenied', [
'reason' => 'PERMISSION_MISSING',
'permission' => 'config.gateway',
]);
throw new Forbidden('No tienes permiso para acceder a configuración de gateway');
}
if (!$this->authService->isPortalClientesEnabled()) {
$this->logger->warning('AccessDenied', [
'reason' => 'PORTAL_DISABLED',
'module' => 'modulo_portal_clientes',
]);
throw new Forbidden('Portal de clientes no está habilitado. Contacta a administración.');
}
}El logging diferenciado (PERMISSION_MISSING vs PORTAL_DISABLED) permite distinguir en auditoría si el acceso fue bloqueado por permisos del usuario o por configuración del módulo.
Degradación Graceful en Frontend ERP
useGatewayConfig() en bautista-app maneja el 403 sin lanzar error:
typescript
// Si el backend retorna 403 (portal deshabilitado o sin permiso):
// → isEnabled: false, config: null
// GatewayConfigSection NO se renderiza (return null)El operador sin permiso o sobre un tenant con portal deshabilitado simplemente no ve la sección. No hay mensajes de error visibles ni crashes.
Escenarios
| Escenario | membresias | portal flag | permiso | Resultado |
|---|---|---|---|---|
| Portal habilitado | 1 | 1 | sí | Config accesible, tablas creadas, seeds insertados |
| Portal deshabilitado | 1 | 0 | sí | Config retorna 403 (PORTAL_DISABLED), tablas no creadas |
| Sin membresías | 0 | cualquiera | sí | Portal deshabilitado (prerequisito no cumplido) |
| Sin permiso | 1 | 1 | no | Config retorna 403 (PERMISSION_MISSING) |
Seeds de DataConfig
Las claves portal.gateway.* y portal.recibo.* en data_config solo se insertan si isPortalClientesEnabled() retorna true:
php
// migrations/seeds/tenancy/DataConfig.php
if ($this->isPortalClientesEnabled()) {
// Inserta portal.gateway.nombre, portal.gateway.api_key, etc.
// Inserta portal.recibo.cuenta_bancaria, portal.recibo.caja_schema
}