# CLAUDE.md — etaxapi (Django REST Framework Backend)

## Stack
- Python 3.x, Django 4.1, Django REST Framework 3.14
- SimpleJWT para autenticación JWT
- drf-yasg para documentación Swagger/ReDoc
- django-filter para filtrado avanzado
- django-cors-headers para CORS
- psycopg2 (PostgreSQL en producción), SQLite en desarrollo
- Base de datos de usuarios: modelo `Usuario` extendiendo `AbstractUser`

---

## CRÍTICO — Seguridad: nunca hardcodear secretos

**El `SECRET_KEY`, credenciales de email, contraseñas y cualquier dato sensible JAMÁS van en el código fuente.**

Usar `python-decouple` o `django-environ` para leer desde `.env`:

```python
# settings.py
from decouple import config

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
```

```ini
# .env (nunca commitear)
SECRET_KEY=valor-secreto-aqui
DEBUG=False
EMAIL_HOST_PASSWORD=la-contrasena
```

`.env` debe estar en `.gitignore`. Ya existe el problema en `usuarios/api/views.py` donde la contraseña de Gmail está hardcodeada — esto debe moverse a settings/variables de entorno.

---

## Configuración — separar settings por entorno

Estructura recomendada:

```
etaxApi/
  settings/
    __init__.py    ← importa base
    base.py        ← configuración compartida
    development.py ← DEBUG=True, SQLite
    production.py  ← DEBUG=False, PostgreSQL, ALLOWED_HOSTS real
```

- `CORS_ORIGIN_ALLOW_ALL = True` y `CORS_ORIGIN_WHITELIST` son mutuamente excluyentes. En producción usar solo el whitelist con `CORS_ORIGIN_ALLOW_ALL = False`.
- `DEBUG = True` solo en `development.py`. **Nunca en producción.**
- `ALLOWED_HOSTS` en producción debe ser explícito (sin `'*'`).

---

## Modelos

### Convenciones de nombres
- Campos de modelo: `snake_case` (convención Django/Python).
- **Problema actual:** muchos campos usan `camelCase` (`ssContribuyente`, `fechaSolicitud`). Al crear modelos nuevos, usar `snake_case`. Al serializar hacia el frontend Angular, usar `source=` en el serializer si es necesario.
- Clases: `PascalCase` (`Solicitud`, `Seniors`, `Usuario`).
- Nombres de clase descriptivos: `sendmail` → `SendMailViewSet`.

### Siempre definir `__str__`
```python
class Solicitud(models.Model):
    ...
    def __str__(self):
        return f"{self.nombreContribuyente} — {self.fechaSolicitud}"
```

### Usar `TextChoices` e `IntegerChoices` en vez de listas de tuplas
```python
# EVITAR
solicitud_select = [(1, 'Estatal-PR'), (2, 'Federal 1040 US')]

# USAR
class TipoSolicitud(models.IntegerChoices):
    ESTATAL_PR  = 1, 'Estatal-PR'
    FEDERAL_1040 = 2, 'Federal 1040 US'
    PR_ACTC     = 3, '1040 PR - ACTC'
    ESTADOS_US  = 4, 'Estados - US'

solicitud = models.IntegerField(choices=TipoSolicitud.choices, default=TipoSolicitud.ESTATAL_PR)
```

### Normalizar modelos repetitivos — no duplicar campos
**Problema actual:** `Solicitud` tiene `nombreDependiente1..4`, `ssdependiente1..4`, etc. (4 grupos × 10 campos = 40 campos repetidos).  
**Regla:** crear un modelo `Dependiente` con FK a `Solicitud`:

```python
class Dependiente(models.Model):
    solicitud = models.ForeignKey(Solicitud, related_name='dependientes', on_delete=models.CASCADE)
    nombre = models.CharField(max_length=150)
    apellido_paterno = models.CharField(max_length=150, blank=True)
    ss = models.CharField(max_length=9, blank=True)
    nacimiento = models.DateField(null=True, blank=True)
    relacion = models.IntegerField(choices=Relacion.choices)
    meses = models.IntegerField(null=True, blank=True)
```

### Meta class en todos los modelos
```python
class Meta:
    ordering = ['-created']
    verbose_name = 'Solicitud'
    verbose_name_plural = 'Solicitudes'
```

### Índices para campos de búsqueda frecuente
Los campos usados en `search_fields` o `filterset_fields` deben tener índice:
```python
ss_contribuyente = models.CharField(max_length=9, db_index=True)
```

---

## Vistas (ViewSets)

### Optimizar queries — siempre usar `select_related`/`prefetch_related`
```python
# EVITAR — genera N+1 queries
queryset = Solicitud.objects.all()

# USAR
queryset = Solicitud.objects.select_related('usuario_created', 'banco').all()
```

Para relaciones M2M o reverse FK:
```python
queryset = Solicitud.objects.prefetch_related('dependientes').select_related('banco')
```

### Permisos — definir explícitamente en cada ViewSet
No asumir permisos por defecto. Todos los ViewSets deben declarar `permission_classes`.

### Separar permisos y serializers en archivos propios
- `permissions.py` → clases de permiso (`IsOwner`, `IsSuperuser`, etc.)
- `serializers.py` → serializers del modelo
- `views.py` → ViewSets únicamente
- `router.py` → registro de rutas

`IsOwner` y `CustomPagination` están en `usuarios/api/serializers.py` — moverlos a archivos dedicados: `usuarios/api/permissions.py` y `core/pagination.py`.

### Formato de respuesta consistente para errores
```python
from rest_framework.exceptions import ValidationError

# No retornar strings planos en errores — usar excepciones DRF
raise ValidationError({'email': 'Este email ya está registrado'})
```

---

## Serializers

### No usar `fields = '__all__'` en producción
Listar explícitamente los campos expuestos. Nunca exponer `password`, `last_login`, `is_superuser` por accidente.

### Formatear `fields` en múltiples líneas
```python
# EVITAR — una línea con 100 campos
fields = ['id','solicitud','localidad','fechaSolicitud',...]

# USAR
fields = [
    'id',
    'solicitud',
    'localidad',
    'fecha_solicitud',
    'turno',
    ...
]
```

### Validaciones de negocio van en el serializer, no en la vista
```python
def validate_ss_contribuyente(self, value):
    if len(value) != 9:
        raise serializers.ValidationError('El SS debe tener exactamente 9 dígitos')
    return value

def validate(self, data):
    if data.get('ss_contribuyente') == data.get('ss_conyuge'):
        raise serializers.ValidationError({'ss_conyuge': 'El SS del cónyuge no puede ser igual al del contribuyente'})
    return data
```

### Read vs Write serializers
Patrón actual es correcto (`SolicitudSerializer` para write, `SolicitudReadSerializer` para read). Mantener este patrón.  
Nombrar como `SolicitudWriteSerializer` / `SolicitudReadSerializer` para mayor claridad.

---

## Autenticación y Seguridad

### JWT — reducir lifetime del access token
```python
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),  # 1 hora, no 1 día
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
}
```

### Email — usar django.core.mail correctamente
**No** construir la conexión SMTP manualmente en la vista con credenciales hardcodeadas.  
Configurar en `settings.py` y usar el backend de Django:

```python
# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = config('EMAIL_HOST')
EMAIL_PORT = config('EMAIL_PORT', cast=int)
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = config('EMAIL_HOST_USER')
```

```python
# views.py
from django.core.mail import send_mail

send_mail(subject=asunto, message='', html_message=cuerpo, from_email=None, recipient_list=to)
```

### CORS en producción
```python
# production.py
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
    'https://childtaxcreditpr.com',
]
```

---

## Estructura de apps

Cada app Django sigue esta estructura:
```
app/
  __init__.py
  admin.py        ← registro en admin con list_display, search_fields
  apps.py
  models.py       ← modelos con __str__, Meta, choices como TextChoices/IntegerChoices
  tests.py        ← tests unitarios
  views.py        ← views Django tradicionales (si aplica)
  api/
    __init__.py
    permissions.py  ← clases de permiso específicas del app
    serializers.py  ← serializers de lectura y escritura
    views.py        ← ViewSets DRF
    router.py       ← registro DefaultRouter
```

---

## Admin

Registrar todos los modelos con configuración mínima útil:
```python
@admin.register(Solicitud)
class SolicitudAdmin(admin.ModelAdmin):
    list_display = ['id', 'nombre_contribuyente', 'ss_contribuyente', 'fecha_solicitud', 'created']
    search_fields = ['ss_contribuyente', 'nombre_contribuyente']
    list_filter = ['solicitud', 'estatus_civil', 'created']
    readonly_fields = ['created', 'updated']
```

---

## Tests

- Cada app tiene tests en `tests.py` o un directorio `tests/`.
- Usar `APITestCase` de DRF para endpoints.
- Los tests de autenticación verifican que endpoints protegidos retornen 401 sin token.
- Los tests de permisos verifican que un usuario no pueda editar registros de otro.

```python
from rest_framework.test import APITestCase, APIClient

class SolicitudAPITest(APITestCase):
    def setUp(self):
        self.user = Usuario.objects.create_user(username='test', password='test123')
        self.client.force_authenticate(user=self.user)
```

---

## Convenciones Python

- PEP 8 en todo el código.
- Imports ordenados: stdlib → django → drf → local (usar `isort`).
- F-strings sobre concatenación o `.format()`.
- Type hints en funciones nuevas.
- No dejar `print()` en código de producción — usar `logging`.

```python
import logging
logger = logging.getLogger(__name__)

# En vez de print:
logger.debug('Datos recibidos: %s', request.data)
```

---

## Checklist antes de hacer commit

- [ ] Sin credenciales, passwords ni SECRET_KEY en el código
- [ ] Sin `print()` dejados por debugging (usar `logger.debug`)
- [ ] Sin `DEBUG = True` en configuración de producción
- [ ] Todos los modelos nuevos tienen `__str__` y `Meta`
- [ ] Todos los ViewSets tienen `permission_classes` explícito
- [ ] Sin `fields = '__all__'` en serializers de producción
- [ ] Nuevos endpoints documentados (drf-yasg los pick up automáticamente con docstrings)
- [ ] `select_related`/`prefetch_related` en querysets con FK
