# Plan: siguientes 5 deudas técnicas por ROI (lote 2)

> Fecha: 2026-06-06
> Criterio: gravedad ALTA × coste BAJO primero, luego MEDIA × BAJO
> Estado: PENDIENTE

> **Nota previa:** DT-039 ya está resuelto. `GrupoPermisoService` tiene `can()` (bool) y `assertCan()` (lanza excepción) — la separación ya existe en el código. Marcar como RESUELTO en `deuda-tecnica.md`.

---

## DT-011 — Validaciones ausentes en entidades críticas

**Impacto:** Sin `#[Assert]` en entidades, datos inválidos pueden persistirse si el objeto se construye fuera de un formulario (servicio, comando, test de integración).

### `src/Entity/Usuario.php`

Añadir `use Symfony\Component\Validator\Constraints as Assert;`

- `$nombre` (L28): añadir `#[Assert\NotBlank]` + `#[Assert\Length(max: 100)]`
- `$email` (L31): añadir `#[Assert\NotBlank]` + `#[Assert\Email]` + `#[Assert\Length(max: 150)]`
- `$usuarioBgg` (L37): añadir `#[Assert\Length(max: 100)]`

### `src/Entity/Evento.php`

Añadir `use Symfony\Component\Validator\Constraints as Assert;`

- `$titulo` (L29): añadir `#[Assert\NotBlank]` + `#[Assert\Length(min: 3, max: 200)]`

**Riesgo:** Ninguno. Los formularios ya validan estos campos; los Assert en entidad son una red de seguridad extra.

---

## DT-030 / DT-045 — Email hardcodeado en `BggApiService`

**Impacto:** `'wari@asociacion.local'` no es un email válido para producción ni configurable sin tocar código.

*(DT-030 y DT-045 son el mismo problema: el email fue renombrado de `bfbg@` a `wari@` pero sigue hardcodeado. Un solo cambio resuelve ambos.)*

### `.env`

Añadir tras `BGG_API_TOKEN`:
```
BGG_TRANSLATE_EMAIL=
```

### `src/Service/BggApiService.php`

En el constructor (~L18-23), añadir parámetro tras `$apiToken`:
```php
#[Autowire('%env(BGG_TRANSLATE_EMAIL)%')]
private readonly string $translateEmail = '',
```

En `traducirFragmento()` (~L286), reemplazar:
```php
'de' => 'wari@asociacion.local',
```
por:
```php
'de' => $this->translateEmail,
```

**Riesgo:** Ninguno. Si `BGG_TRANSLATE_EMAIL` está vacío, MyMemory acepta el campo vacío (usa el tier anónimo sin identificar).

---

## DT-048 — `GrupoService::expulsarMiembro()` bypassa `GrupoPermisoService`

**Impacto:** El check de permisos es inconsistente con el resto del servicio — usa `$adminMembership->hasPermission()` directamente en lugar de `GrupoPermisoService`.

### `src/Service/GrupoService.php` (~L204-209)

Reemplazar:
```php
$adminMembership = $this->grupoMiembroRepository->findMembership($grupo, $admin);
if ($adminMembership === null || !$adminMembership->hasPermission(PermisoGrupo::ExpulsarMiembros)) {
    throw new \RuntimeException('No tienes permisos para expulsar miembros.');
}
```
por:
```php
$this->grupoPermisoService->assertCan($grupo, $admin, PermisoGrupo::ExpulsarMiembros);
```

Verificar si `$grupoMiembroRepository` se usa en otro lugar del mismo método; si no, la inyección directa del repo puede eliminarse del constructor cuando se resuelva DT-006.

**Riesgo:** Bajo. El comportamiento es equivalente. El tipo de excepción cambia de `RuntimeException` a `AccessDeniedHttpException`, lo que es más correcto (es un error de autorización).

---

## DT-049 — Propiedades de entidades sin valor por defecto

**Impacto:** Un objeto construido sin llamar a los setters lanza `TypeError` al intentar leer la propiedad.

### Entidades afectadas y propiedades a inicializar con `= ''`:

| Archivo | Propiedades |
|---|---|
| `src/Entity/Usuario.php` | `$nombre`, `$email`, `$password` |
| `src/Entity/Evento.php` | `$titulo` |
| `src/Entity/Grupo.php` | `$nombre`, `$codigoAcceso` |
| `src/Entity/Juego.php` | `$nombre` |
| `src/Entity/AuditLog.php` | `$accion`, `$entidad` |
| `src/Entity/TelegramGrupo.php` | `$nombreIdentificativo`, `$chatId` |
| `src/Entity/TelegramBot.php` | `$nombre` |
| `src/Entity/PasswordResetToken.php` | `$tokenHash` |

**Riesgo:** Ninguno. Los valores por defecto solo actúan antes de que los setters sean llamados.

---

## DT-017 — `NotificacionService` no hace flush

**Impacto:** Las notificaciones solo se persisten si el llamador hace flush. Si el llamador falla antes de llegar a su propio flush, las notificaciones se pierden silenciosamente.

### `src/Service/NotificacionService.php` (L28)

En `crear()`, añadir flush tras persist:
```php
$this->em->persist($notif);
$this->em->flush();
```

**Riesgo:** Bajo. Puede producir un flush extra en operaciones que ya flushean después. Este comportamiento se revisará cuando se resuelva DT-002 (AuditService batch flush) — ambos servicios deberían migrar al mismo patrón definitivo en ese momento.

---

## Orden de implementación

1. DT-030/DT-045 — 2 líneas en el mismo archivo + 1 en `.env`
2. DT-048 — 3 líneas → 1 en `GrupoService`
3. DT-049 — añadir `= ''` en 11 propiedades distribuidas en 7 entidades
4. DT-011 — añadir Assert constraints en `Usuario` y `Evento`
5. DT-017 — 1 línea en `NotificacionService`
