# Directiva: Arquitectura Multi-Tenant PMC

**Version:** 1.0.0
**Fecha:** 2025-12-08
**Estado:** Activa
**Referencia Catalogo:** @CATALOG_TENANT

---

## Proposito

Define la estrategia de multi-tenancy para PMC, garantizando aislamiento de datos entre organizaciones (agencias) mientras se mantiene eficiencia operativa.

---

## Estrategia Seleccionada

**Tipo:** Single Database, Shared Schema con Row-Level Security (RLS)

**Justificacion:** Ver ADR-002-multi-tenancy.md

---

## Principios

### P1: Aislamiento Total de Datos

Todo dato de negocio debe estar asociado a un tenant y ser invisible para otros tenants.

```sql
-- OBLIGATORIO en todas las tablas de negocio
tenant_id UUID NOT NULL REFERENCES auth.tenants(id)
```

### P2: Filtrado Automatico

El filtrado por tenant debe ser automatico, no depender del desarrollador.

```sql
-- RLS policy en cada tabla
CREATE POLICY tenant_isolation ON {tabla}
    USING (tenant_id = current_setting('app.current_tenant')::uuid);
```

### P3: Context Propagation

El tenant_id debe propagarse automaticamente en cada request.

```typescript
// Middleware extrae tenant del JWT
const tenantId = req.user.tenantId;
await dataSource.query(`SET app.current_tenant = '${tenantId}'`);
```

### P4: Storage Isolation

Archivos deben estar segregados por tenant en storage.

```
bucket/{tenant_slug}/assets/
bucket/{tenant_slug}/generated/
bucket/{tenant_slug}/models/
```

---

## Implementacion

### 1. Middleware de Tenant

```typescript
@Injectable()
export class TenantMiddleware implements NestMiddleware {
  constructor(private dataSource: DataSource) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const tenantId = req.user?.tenantId;

    if (tenantId) {
      // Set para RLS
      await this.dataSource.query(
        `SET app.current_tenant = '${tenantId}'`
      );

      // Agregar al request context
      req.tenantContext = { tenantId };
    }

    next();
  }
}
```

### 2. Decorador de Tenant

```typescript
export const CurrentTenant = createParamDecorator(
  (data: unknown, ctx: ExecutionContext): string => {
    const request = ctx.switchToHttp().getRequest();
    return request.tenantContext?.tenantId;
  },
);

// Uso
@Get()
async findAll(@CurrentTenant() tenantId: string) {
  // tenantId ya disponible
}
```

### 3. BaseEntity con tenant_id

```typescript
export abstract class TenantAwareEntity {
  @Column('uuid')
  tenant_id: string;

  @CreateDateColumn()
  created_at: Date;

  @UpdateDateColumn()
  updated_at: Date;

  @DeleteDateColumn()
  deleted_at: Date;
}

// Uso
@Entity('clients', { schema: 'crm' })
export class Client extends TenantAwareEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  name: string;
}
```

### 4. Service Base con Tenant

```typescript
export abstract class TenantAwareService<T> {
  constructor(
    protected readonly repository: Repository<T>,
  ) {}

  async findAllByTenant(tenantId: string): Promise<T[]> {
    return this.repository.find({
      where: { tenant_id: tenantId } as any,
    });
  }

  async createForTenant(tenantId: string, dto: any): Promise<T> {
    const entity = this.repository.create({
      ...dto,
      tenant_id: tenantId,
    });
    return this.repository.save(entity);
  }
}
```

---

## Tablas sin tenant_id

Las siguientes tablas son globales (no tienen tenant_id):

| Tabla | Razon |
|-------|-------|
| auth.tenants | Es la tabla de tenants |
| auth.plans | Planes son globales |
| generation.workflow_templates (is_system=true) | Templates del sistema |
| config.feature_flags (tenant_id IS NULL) | Flags globales |

---

## Validaciones Obligatorias

### En Creacion de Entidad

```typescript
// SIEMPRE asignar tenant_id en creacion
const client = this.clientRepository.create({
  ...createClientDto,
  tenant_id: tenantId, // OBLIGATORIO
});
```

### En Queries

```typescript
// SIEMPRE filtrar por tenant (aunque RLS lo haga)
const clients = await this.clientRepository.find({
  where: { tenant_id: tenantId },
});

// NUNCA queries sin filtro de tenant en tablas tenant-aware
// INCORRECTO:
const allClients = await this.clientRepository.find(); // PROHIBIDO
```

### En Updates/Deletes

```typescript
// SIEMPRE verificar propiedad antes de modificar
const client = await this.clientRepository.findOne({
  where: { id: clientId, tenant_id: tenantId },
});

if (!client) {
  throw new NotFoundException('Client not found');
}
```

---

## Limites y Quotas por Tenant

```typescript
interface TenantLimits {
  max_users: number;
  max_brands: number;
  max_products: number;
  max_storage_gb: number;
  generations_per_month: number;
  trainings_per_month: number;
}

// Verificar antes de operaciones
async checkQuota(tenantId: string, resource: string): Promise<boolean> {
  const tenant = await this.tenantService.findById(tenantId);
  const usage = await this.usageService.getCurrentUsage(tenantId, resource);

  return usage < tenant.limits[resource];
}
```

---

## Testing Multi-Tenant

```typescript
describe('ClientService (multi-tenant)', () => {
  it('should not return clients from other tenants', async () => {
    // Crear clientes en tenant A y B
    await service.create(tenantA, { name: 'Client A' });
    await service.create(tenantB, { name: 'Client B' });

    // Query desde tenant A
    const clients = await service.findAll(tenantA);

    // Solo debe ver sus clientes
    expect(clients).toHaveLength(1);
    expect(clients[0].name).toBe('Client A');
  });
});
```

---

## Errores Comunes

### ERROR 1: Olvidar tenant_id en INSERT

```typescript
// INCORRECTO
const brand = this.brandRepository.create({
  name: dto.name,
  // Falta tenant_id!
});

// CORRECTO
const brand = this.brandRepository.create({
  name: dto.name,
  tenant_id: tenantId,
});
```

### ERROR 2: Query sin filtro de tenant

```typescript
// INCORRECTO - Expone datos de otros tenants si RLS falla
const brand = await this.brandRepository.findOne({
  where: { id: brandId },
});

// CORRECTO
const brand = await this.brandRepository.findOne({
  where: { id: brandId, tenant_id: tenantId },
});
```

### ERROR 3: No validar propiedad en acciones

```typescript
// INCORRECTO - Puede modificar recursos de otro tenant
async delete(brandId: string) {
  await this.brandRepository.delete(brandId);
}

// CORRECTO
async delete(tenantId: string, brandId: string) {
  const result = await this.brandRepository.delete({
    id: brandId,
    tenant_id: tenantId,
  });

  if (result.affected === 0) {
    throw new NotFoundException();
  }
}
```

---

## Checklist de Implementacion

- [ ] Tabla tiene columna tenant_id
- [ ] Foreign key a auth.tenants
- [ ] Index en tenant_id
- [ ] RLS policy creada
- [ ] Entity extiende TenantAwareEntity
- [ ] Service usa TenantAwareService base
- [ ] Controller usa @CurrentTenant()
- [ ] Tests verifican aislamiento
- [ ] Storage usa prefijo de tenant

---

## Referencias

- Catalogo: `core/catalog/multi-tenancy/README.md`
- ADR: `docs/97-adr/ADR-002-multi-tenancy.md`
- Patrones RLS: `core/catalog/multi-tenancy/patterns/rls-policies.md`

---

**Generado por:** Requirements-Analyst
**Fecha:** 2025-12-08
