# Directiva: Multi-Tenant

## Propósito

Define las reglas obligatorias para implementar multi-tenancy en ERP Core y todas las verticales que lo extienden.

## Alcance

- Backend: Todas las consultas y operaciones
- Database: RLS Policies, schemas, índices
- Frontend: Contexto de tenant
- APIs: Headers y validación

---

## Principios

### 1. Aislamiento Total
```
OBLIGATORIO: Ningún tenant puede ver o modificar datos de otro tenant.
```

### 2. Tenant ID Obligatorio
```
OBLIGATORIO: Toda tabla debe tener columna `tenant_id UUID NOT NULL`.
```

### 3. Filtrado Automático
```
OBLIGATORIO: Toda consulta debe filtrar por tenant_id a nivel de aplicación Y base de datos (RLS).
```

---

## Implementación en Base de Datos

### Columna tenant_id
```sql
-- OBLIGATORIO en TODA tabla
CREATE TABLE {schema}.{tabla} (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id),
    -- ... otras columnas ...
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Índice obligatorio
CREATE INDEX idx_{tabla}_tenant_id ON {schema}.{tabla}(tenant_id);
```

### RLS Policy Estándar
```sql
-- Habilitar RLS
ALTER TABLE {schema}.{tabla} ENABLE ROW LEVEL SECURITY;

-- Policy de lectura
CREATE POLICY "tenant_isolation_select" ON {schema}.{tabla}
    FOR SELECT
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Policy de inserción
CREATE POLICY "tenant_isolation_insert" ON {schema}.{tabla}
    FOR INSERT
    WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Policy de actualización
CREATE POLICY "tenant_isolation_update" ON {schema}.{tabla}
    FOR UPDATE
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid)
    WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Policy de eliminación
CREATE POLICY "tenant_isolation_delete" ON {schema}.{tabla}
    FOR DELETE
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

### Contexto de Sesión
```sql
-- Establecer contexto al inicio de cada request
SET app.current_tenant_id = '{tenant_uuid}';

-- En transacciones
BEGIN;
SET LOCAL app.current_tenant_id = '{tenant_uuid}';
-- ... operaciones ...
COMMIT;
```

---

## Implementación en Backend

### Middleware de Tenant
```typescript
// middleware/tenant.middleware.ts
export const tenantMiddleware = async (req, res, next) => {
    const tenantId = req.headers['x-tenant-id'] || req.user?.tenantId;

    if (!tenantId) {
        return res.status(400).json({ error: 'Tenant ID required' });
    }

    // Validar que tenant existe y está activo
    const tenant = await tenantService.findById(tenantId);
    if (!tenant || !tenant.isActive) {
        return res.status(403).json({ error: 'Invalid or inactive tenant' });
    }

    req.tenantId = tenantId;

    // Establecer contexto en BD
    await dbConnection.query(`SET app.current_tenant_id = '${tenantId}'`);

    next();
};
```

### En Servicios
```typescript
// OBLIGATORIO: Siempre filtrar por tenantId
async findAll(tenantId: string): Promise<Entity[]> {
    return this.repository.find({
        where: { tenantId }
    });
}

// OBLIGATORIO: Validar tenantId en creación
async create(dto: CreateDto, tenantId: string): Promise<Entity> {
    const entity = this.repository.create({
        ...dto,
        tenantId
    });
    return this.repository.save(entity);
}
```

### En Controllers
```typescript
@Get()
async findAll(@Req() req: Request) {
    // tenantId viene del middleware
    return this.service.findAll(req.tenantId);
}
```

---

## Implementación en Frontend

### Contexto de Tenant
```typescript
// stores/tenant.store.ts
interface TenantState {
    currentTenantId: string | null;
    tenants: Tenant[];
}

export const useTenantStore = create<TenantState>((set) => ({
    currentTenantId: null,
    tenants: [],
    setCurrentTenant: (id) => set({ currentTenantId: id }),
}));
```

### En API Calls
```typescript
// services/api.service.ts
const api = axios.create({
    baseURL: API_URL,
});

api.interceptors.request.use((config) => {
    const tenantId = useTenantStore.getState().currentTenantId;
    if (tenantId) {
        config.headers['X-Tenant-Id'] = tenantId;
    }
    return config;
});
```

---

## Validaciones Obligatorias

### Checklist Pre-Deploy
- [ ] Toda tabla tiene columna `tenant_id`
- [ ] Toda tabla tiene RLS habilitado
- [ ] Toda tabla tiene policies de tenant
- [ ] Todo endpoint usa middleware de tenant
- [ ] Todo servicio filtra por tenant
- [ ] Frontend envía header X-Tenant-Id

### Tests Obligatorios
```typescript
describe('Tenant Isolation', () => {
    it('should not return data from other tenants', async () => {
        // Crear datos en tenant A
        await service.create(data, tenantA);

        // Consultar desde tenant B
        const results = await service.findAll(tenantB);

        // No debe encontrar datos de tenant A
        expect(results).toHaveLength(0);
    });
});
```

---

## Excepciones

Las siguientes tablas NO requieren tenant_id:
- `core_tenants.tenants` (la tabla maestra de tenants)
- `core_auth.system_settings` (configuración global del sistema)
- `core_catalogs.countries`, `core_catalogs.currencies` (catálogos globales)

---

## Referencias

- RLS PostgreSQL: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
- Directiva Diseño BD: `DIRECTIVA-DISENO-BASE-DATOS.md`

---
*Directiva específica de ERP-Core*
