# PITFALLS: Trampas y Anti-patrones — [Nombre del Proyecto]

> Catálogo de errores comunes, anti-patrones del stack elegido y lecciones
> aprendidas de proyectos anteriores. Leer ANTES de escribir código.
> El objetivo es no repetir errores conocidos.
> Autor: [nombre] | Fecha: YYYY-MM-DD

---

## Cómo usar este documento

Antes de implementar cualquier feature, revisar si alguna trampa aplica
a lo que se va a construir. Si se descubre una trampa nueva durante el desarrollo:
documentarla aquí para que el siguiente desarrollador (o el agente) no caiga.

Severidad:
- CRÍTICO: Causa bugs silenciosos, pérdida de datos o vulnerabilidades de seguridad
- ALTO: Causa errores en producción o degradación de performance
- MEDIO: Causa deuda técnica o dificulta el mantenimiento
- BAJO: Mejora de calidad o legibilidad

---

## Trampas de SQLAlchemy + Async

### [CRÍTICO] MissingGreenlet: acceder a relaciones sin eager loading

**Trampa**:
```python
# MALO — lanza MissingGreenlet en async
facturas = await db.execute(select(Factura))
for factura in facturas.scalars():
    print(factura.cliente.nombre)  # acceso lazy en contexto async = error
```

**Solución**:
```python
# BIEN — eager loading explícito
stmt = select(Factura).options(selectinload(Factura.cliente))
facturas = await db.execute(stmt)
```

**Regla**: Todo atributo de relación accedido fuera del contexto de la query
necesita `selectinload()` o `lazy="selectin"` en el modelo.

---

### [ALTO] N+1 queries en endpoints de listado

**Trampa**: Un endpoint que devuelve 100 facturas con sus clientes genera
101 queries: 1 para las facturas + 1 por cada cliente.

**Cómo detectarlo**: Activar logging SQL y contar queries:
```python
# En desarrollo, en alembic.ini o en la config de SQLAlchemy:
echo=True
```

**Solución**: `selectinload()` para todas las relaciones que se serializan.

---

### [CRÍTICO] db.commit() en services (no en endpoints)

**Trampa**: El service hace `db.commit()` internamente. Si el endpoint necesita
hacer dos operaciones en la misma transacción, la primera se commitea antes
de que la segunda complete, dejando datos inconsistentes.

**Regla**: Los services solo hacen `db.add()`, `db.flush()`, `db.refresh()`.
El `db.commit()` SIEMPRE en el endpoint, después de que todas las operaciones
del service completaron correctamente.

---

## Trampas de FastAPI

### [ALTO] Rutas paramétricas antes de rutas estáticas

**Trampa**:
```python
router.get("/{factura_id}")     # Esta ruta captura CUALQUIER string
router.get("/exportar")          # NUNCA se alcanza — capturada por la anterior
```

**Solución**: Declarar rutas estáticas ANTES de las paramétricas:
```python
router.get("/exportar")          # Primero las específicas
router.get("/{factura_id}")     # Luego las paramétricas
```

---

### [CRÍTICO] IDOR: filtrar datos solo por query params sin verificar ownership

**Trampa**:
```python
@router.get("/facturas")
async def listar_facturas(empresa_id: UUID, db: AsyncSession = Depends(get_db)):
    # PELIGRO: cualquier usuario puede ver facturas de cualquier empresa
    return await db.execute(select(Factura).where(Factura.empresa_id == empresa_id))
```

**Solución**: El filtro de ownership SIEMPRE viene del JWT, nunca del cliente:
```python
@router.get("/facturas")
async def listar_facturas(
    db: AsyncSession = Depends(get_db),
    usuario: Usuario = Depends(get_current_user)  # del JWT
):
    return await db.execute(
        select(Factura).where(Factura.empresa_id == usuario.empresa_id)
    )
```

---

### [MEDIO] GET endpoints que modifican estado

**Trampa**: Lógica de negocio que muta datos dentro de un handler GET.
Viola el principio HTTP de idempotencia. Puede ejecutarse múltiples veces
por bots, prefetch del navegador, etc.

**Regla**: GET endpoints son read-only. Cualquier mutación va en POST/PUT/PATCH.

---

## Trampas de Pydantic v2

### [ALTO] Usar `str` sin restricciones para campos con dominio cerrado

**Trampa**:
```python
class FacturaCreate(BaseModel):
    estatus: str  # Acepta cualquier string — "activo", "ACTIVO", "Activo", "xyz"
```

**Solución**: `Literal[]` o `Enum`:
```python
from typing import Literal
class FacturaCreate(BaseModel):
    estatus: Literal["borrador", "timbrada", "cancelada"]
```

Los valores DEBEN coincidir exactamente con el CheckConstraint en el modelo ORM.

---

### [ALTO] Optional[datetime] para columnas Time

**Trampa**:
```python
hora_inicio: Optional[datetime]  # Incorrecto para columna Time de PostgreSQL
```

**Solución**:
```python
from datetime import time
hora_inicio: Optional[time]
```

---

## Trampas de Angular

### [ALTO] No parsear PaginatedResponse — spinner infinito

**Trampa**: El service Angular devuelve el objeto completo del endpoint paginado.
El componente intenta hacer `.filter()` sobre el objeto en lugar del array.

```typescript
// MALO — getFacturas() devuelve Observable<PaginatedResponse<Factura>>
this.facturas = this.facturasService.getFacturas();
// El template hace @for (f of facturas) — pero facturas es un objeto, no array
```

**Solución**:
```typescript
// BIEN — el service extrae el array
getFacturas(): Observable<Factura[]> {
  return this.http.get<PaginatedResponse<Factura>>('/api/facturas')
    .pipe(map(resp => resp.items));
}
```

---

### [MEDIO] Usar *ngIf / *ngFor en lugar de @if / @for

**Trampa**: Angular 17+ usa la nueva sintaxis de control flow. Mezclar sintaxis
genera inconsistencias y confusión.

**Regla**: SIEMPRE `@if`, `@for`, `@switch`. NUNCA `*ngIf`, `*ngFor`, `*ngSwitch`.
`@for` requiere `track`: `@for (item of items; track item.id)`.

---

### [ALTO] Funciones en templates que se evalúan en cada change detection cycle

**Trampa**:
```html
<!-- MALO: calcularTotal() se llama en CADA ciclo de change detection -->
<span>{{ calcularTotal(items) }}</span>
```

**Solución**:
```typescript
// BIEN: computed() se recalcula solo cuando cambian sus dependencias
total = computed(() => this.items().reduce((s, i) => s + i.precio, 0));
```
```html
<span>{{ total() }}</span>
```

---

## Trampas de PostgreSQL

### [CRÍTICO] NULL en cláusulas IN

**Trampa**:
```python
# MALO: column.in_([valor, None]) NO matchea filas donde column IS NULL
stmt.where(Factura.aprobador_id.in_([usuario_id, None]))
```

**Solución**:
```python
from sqlalchemy import or_
stmt.where(or_(
    Factura.aprobador_id == usuario_id,
    Factura.aprobador_id.is_(None)
))
```

---

### [ALTO] Índices no usados por diferencia de tipos

**Trampa**: Un índice en columna `VARCHAR` no se usa si la query compara con
un valor `TEXT` o viceversa. PostgreSQL necesita hacer cast implícito.

**Solución**: Asegurarse de que el tipo del valor en la query coincida con el
tipo de la columna indexada. Verificar con `EXPLAIN ANALYZE`.

---

## Trampas de seguridad

### [CRÍTICO] eval() o exec() con input del usuario

Ya documentado en `reglas/seguridad.md`. La trampa más común:
```python
# CRÍTICO — permite ejecución arbitraria de código
formula = request.json["formula"]
resultado = eval(formula)  # XSS / RCE
```

### [CRÍTICO] Logging de datos sensibles

**Trampa**:
```python
logger.info(f"Login exitoso para usuario {usuario.email}, token: {token}")
# El token aparece en los logs — cualquiera con acceso a logs puede impersonar
```

**Regla**: NUNCA loggear tokens, passwords, números de tarjeta, CURP, RFC completo.

---

## Trampas de testing

### [ALTO] Tests que dependen del orden de ejecución

**Trampa**: Un test modifica estado global (BD, archivo, variable de módulo)
y el siguiente test asume ese estado. Funciona en secuencia, falla en paralelo
o cuando pytest cambia el orden.

**Solución**: Cada test limpia su propio estado. Usar transacciones que se
revierten en el teardown o fixtures aislados.

---

## Trampas descubiertas durante el desarrollo de ESTE proyecto

[Sección vacía al inicio. Agregar aquí cualquier trampa específica que se
descubra durante el desarrollo de este proyecto que no esté cubierta arriba.]

### [SEVERIDAD] Título de la trampa descubierta

**Contexto**: Dónde y cuándo se encontró.
**Trampa**:
```
código que causó el problema
```
**Solución**:
```
código correcto
```
**Fecha**: YYYY-MM-DD | Descubierta por: [nombre]
