# HelpMI — Planificación de Implementación

> **Stack:** Laravel 11 · Filament 3 · Livewire 3 · MySQL 8.0 · Redis  
> **Estimación con agentes IA:** 5–6 semanas · Sin agentes: 13–14 semanas (un desarrollador)  
> **Versión:** 1.2 · Julio 2026

---

## Tabla de contenidos

1. [Resumen ejecutivo](#1-resumen-ejecutivo)
2. [Protocolo de desarrollo con agentes IA](#2-protocolo-de-desarrollo-con-agentes-ia)
3. [Principios de desarrollo](#3-principios-de-desarrollo)
4. [Estrategia de testing](#4-estrategia-de-testing)
5. [Cronograma (Gantt)](#5-cronograma-gantt)
6. [Fases detalladas](#6-fases-detalladas)
7. [Matriz de cobertura de tests](#7-matriz-de-cobertura-de-tests)
8. [Mapa de dependencias](#8-mapa-de-dependencias)
9. [Variante: un solo desarrollador](#9-variante-un-solo-desarrollador)

---

## 1. Resumen ejecutivo

La plataforma HelpMI (actualmente en Concloo CMS / IBM Lotus Domino) se reimplementa completamente en Laravel 11. El proveedor actual cesa la relación y no se dispone de acceso al código fuente. El sistema existente permanece activo e intacto hasta la Fase 10.

| Métrica | Valor |
|---|---|
| Semanas totales (agentes IA + 1 dev) | 5–6 |
| Semanas totales (1 dev sin agentes) | 13–14 |
| Tracks en paralelo (pico S3–S5) | 4 |
| Fases | 12 (F0–F10 + F3.5) |
| Tipos de test | 5 |
| Productos en catálogo | 61 |
| Hauptthemen en catálogo | 19 (revisado en F3.5; antes se asumían 14) |
| Asesores (Berater) reales | 7 (revisado en F3.5; antes se asumían 3) |
| Países soportados | 5: DE/AT/CH/IT/SK (revisado en F3.5; antes se asumían 3) |
| Preguntas pendientes con Verena | 17 + puntos adicionales de F3.5 (ver `docs/supuestos-F3.5.md`) |

> ⚠️ **Prerequisito bloqueante.** Las **17 preguntas pendientes** con Verena Heufler (documentadas en `helpmi-spec.html`) deben resolverse **antes de F2B y F3**. En particular: rol de Betreuer (¿tiene login?), textos de emails, lógica del límite de 2 citas, y visibilidad entre asesores.
>
> ⚠️ **F3.5 (2026-08-03, gaps no dependientes de Verena ya cerrados).** Una auditoría manual de solo lectura contra el panel de admin de producción (`admin.mgm.concloo.net`) reveló varios gaps de paridad funcional entre lo ya construido (F0–F3, F5, F6-adelantado) y la plataforma original. Todo lo que no dependía de Verena ya está implementado y testeado (países IT/SK, 19 Hauptthemen, 7 Berater, Freitextfeld dinámico, `divers`, anamnesis ampliada en admin, tercer campo de notas, campos adicionales de Therapeut). El **modelo real del límite de citas ("casos activos" por terapeuta, no "2/día" ni "total no-socio") queda explícitamente aparcado a petición del cliente** — no se ha tocado la lógica síncrona de F2B/F3. Ver fase F3.5 y `docs/supuestos-F3.5.md` para el detalle completo y lo que sigue pendiente de Verena antes de F4.

---

## 2. Protocolo de desarrollo con agentes IA

Esta sección define cómo trabajar con agentes para minimizar los riesgos específicos de su uso.

### 2.1 Los cinco riesgos principales

| Riesgo | Cómo se manifiesta | Mitigación |
|---|---|---|
| **Regla de negocio incorrecta** | El código compila y los tests pasan, pero el comportamiento es incorrecto | Tests escritos en Given/When/Then antes del código; criterios de aceptación explícitos por tarea |
| **Tests circulares** | El agente escribe tests que prueban su propia implementación, no el requisito | Redactar los tests en términos de comportamiento de usuario, no de implementación; revisión manual de todos los tests antes de aceptarlos |
| **Pérdida de contexto entre sesiones** | El agente de la sesión siguiente contradice decisiones de la sesión anterior | `CLAUDE.md` actualizado al final de cada fase; registro de supuestos por fase |
| **Scope creep silencioso** | El agente añade abstracciones, helpers o features no solicitados | Límite de alcance explícito por fase ("fuera de alcance" en cada fase); instrucción de rechazo por defecto |
| **Vulnerabilidades de seguridad** | IDOR, falta de autorización, datos en logs | Checklist de seguridad integrado en cada fase, no solo en F8 |

### 2.2 CLAUDE.md — archivo de contexto persistente

Crear `/CLAUDE.md` en la raíz del repositorio antes de escribir una sola línea de código. El agente lo lee automáticamente en cada sesión. Incluir:

```markdown
# HelpMI — Contexto para agentes IA

## Qué es este proyecto
Reimplementación de una plataforma de gestión de consultas de microinmunoterapia
para MeGeMIT (DACH). El sistema original es Concloo CMS / IBM Lotus Domino.
No hay acceso al código fuente original.

## Reglas absolutas
- NUNCA modificar migraciones ya ejecutadas en staging o producción.
- NUNCA hardcodear strings en vistas — siempre __() o trans().
- NUNCA almacenar datos personales de pacientes en logs.
- NUNCA implementar funcionalidad fuera del alcance de la fase activa.
- NUNCA crear Jobs para validaciones que deben ser síncronas (ver F2B).
- El límite de 2 citas por terapeuta por día es SÍNCRONO — no un job.

## Decisiones de arquitectura ya tomadas
- SlotAvailabilityService: interface en F1, implementación real en F2B.
- CRM: NullCrmAdapter activo por defecto. HubSpotCrmAdapter es un esqueleto.
- Portal: autenticación por token HMAC-SHA256, no por sesión.
- Slots: duración fija 30 min. CSV muestra duración real de llamada, no del slot.
- i18n: alemán (de) es la lengua de referencia. Rutas con prefijo /{locale}/.

## Stack exacto (no sugerir alternativas)
Laravel 11, Filament 3, Livewire 3, MySQL 8.0, Redis,
Pest (tests), Laravel Dusk (functional), k6 (performance),
Spatie Permission, Spatie Media Library, Spatie Translatable,
mcamara/laravel-localization, pragmarx/google2fa-laravel,
Laravel Fortify, maatwebsite/excel.

## Fuente de verdad
La especificación completa está en docs/helpmi-spec.html.
Ante cualquier ambigüedad, consultar la especificación antes de asumir.
```

### 2.3 Reglas de prompting por sesión

**Al iniciar una nueva sesión con el agente:**
1. Indicar la fase activa: `"Estamos en F2B. Lee CLAUDE.md y docs/helpmi-spec.html antes de escribir código."`
2. Dar el alcance exacto de la tarea, no de la fase completa.
3. Pedir que el agente liste los supuestos que va a hacer antes de escribir código.

**Al recibir código del agente:**
1. Verificar que no hay imports de paquetes no listados en el stack.
2. Verificar que no hay lógica fuera del alcance declarado de la tarea.
3. Verificar que cada test tiene un caso de fallo explícito, no solo el caso feliz.
4. Verificar que no hay strings hardcodeados en vistas.

**Señales de alerta — rechazar el output si:**
- El agente crea una nueva abstracción no solicitada (repositorios, DTOs, service layers adicionales).
- Los tests tienen el mismo nombre que los métodos que prueban (test circular obvio).
- El agente modifica migraciones en lugar de crear nuevas.
- Aparecen paquetes como `livewire/volt`, `filament/spatie-laravel-tags` u otros no listados.
- El agente "mejora" el modelo de datos sin que se le haya pedido.

### 2.4 Formato de tareas para el agente

Cada tarea que se encarga al agente debe seguir esta estructura:

```
TAREA: [nombre corto]
FASE: [F0–F10]
ALCANCE EXACTO: [qué archivos crear o modificar]
FUERA DE ALCANCE: [qué NO tocar]
COMPORTAMIENTO ESPERADO:
  Dado [estado inicial]
  Cuando [acción]
  Entonces [resultado observable]
SUPUESTOS QUE DEBE DOCUMENTAR ANTES DE ESCRIBIR:
  - [lista de ambigüedades conocidas]
```

### 2.5 Registro de supuestos

Al final de cada fase, el agente debe producir un archivo `docs/supuestos-FASE.md` con:

```markdown
## Supuestos adoptados en F[N]

| Decisión | Supuesto adoptado | Alternativa descartada | Quién debe validar |
|---|---|---|---|
| [decisión] | [qué asumió el agente] | [qué podría haber hecho] | [Verena / dev / pendiente] |
```

Este archivo se revisa con Verena antes del UAT. Si algún supuesto está mal, se corrige antes de F9.

### 2.6 Tests primero, siempre

El agente **escribe los tests antes que el código de producción**. La secuencia correcta:

```
1. Dev escribe los criterios Given/When/Then en lenguaje de negocio
2. Agente traduce los criterios a código de test (Pest)
3. Dev revisa que los tests reflejan el requisito, no una implementación
4. Agente escribe el código de producción hasta que los tests pasen
5. Dev revisa el código de producción
```

Si el agente escribe el código de producción antes que los tests, los tests resultantes son circulares por definición.

---

## 3. Principios de desarrollo

- **Tests primero:** los criterios de aceptación se traducen a código de test antes de cualquier implementación.
- **Feature branches por fase:** cada fase vive en su propia rama; merge a `develop` solo con CI en verde.
- **Seeders desde el día 1:** catálogo de 61 productos, 14 categorías y los 3 asesores reales desde F1. Todos los entornos parten del mismo estado base.
- **Staging siempre actualizado:** `develop` → staging en cada merge. Verena puede validar en cualquier momento.
- **Concloo permanece activo hasta F10:** no hay cutover hasta que la nueva plataforma supere el UAT.
- **Alemán como lengua de referencia:** textos en `de` primero; otras traducciones no bloquean el desarrollo.
- **`CLAUDE.md` como memoria persistente:** toda decisión arquitectónica se añade a `CLAUDE.md` inmediatamente, no al final de la fase.

---

## 4. Estrategia de testing

### Tipos de test

| Tipo | Herramienta | Cuándo se escribe | Criterio de paso |
|---|---|---|---|
| **Unit** | Pest + PHPUnit | **Antes** del código de producción | 100 % en lógica de negocio crítica |
| **Integration** | Pest + SQLite in-memory | Al completar cada módulo | Flujos DB verificados; sin queries N+1 |
| **Functional** | Laravel Dusk | Al completar F3 y F2 | Flujo de reserva, login y post-consulta en browser real |
| **Performance** | k6 o Artillery | Fase 8 | Calendar load <300 ms · 50 usuarios simultáneos |
| **Acceptance** | Manual con Verena | Fase 9 | 17 preguntas pendientes validadas; flujo aprobado |

### Formato obligatorio de tests unitarios e de integración

Los tests **no** se nombran por el método que prueban (`test_getAvailableSlots()`). Se nombran por el comportamiento de negocio:

```php
// MAL — circular, no describe un requisito
it('calls getAvailableSlots', fn() => ...);

// BIEN — describe un comportamiento de negocio verificable
it('no ofrece slots en días festivos del país del asesor', function () {
    // Dado
    $berater = Berater::factory()->create(['country' => 'DE']);
    Feiertag::factory()->create(['date' => '2026-10-03', 'country' => 'DE']); // Tag der Deutschen Einheit

    // Cuando
    $slots = app(SlotAvailabilityServiceInterface::class)
        ->getAvailableSlots($berater, Carbon::parse('2026-10-03'));

    // Entonces
    expect($slots)->toBeEmpty();
});
```

Todos los tests de comportamiento crítico deben incluir **al menos un caso de fallo** (qué pasa cuando la regla se viola) además del caso feliz.

---

## 5. Cronograma (Gantt)

```
Fase                  │ S1   S2   S3   S4   S5   S6   S7   S8   S9   S10
──────────────────────┼──────────────────────────────────────────────────
F0 · Infra            │ ████
F1 · Modelos + Auth   │      ████ ████
──────────────────────┼──────────────────────────────────────────────────
F2A · Admin gestión   │           ████ ████ ████
F2B · Admin citas     │           ████ ████ ████
F3  · Portal público  │                ████ ████ ████
F6  · i18n (transv.)  │      ████ ████ ████ ████ ████ ████
──────────────────────┼──────────────────────────────────────────────────
F3.5· Auditoría paridad│                     ████
──────────────────────┼──────────────────────────────────────────────────
F4  · Emails + colas  │                          ████ ████
F5  · Estadísticas    │                               ████ ████
F7  · HubSpot arch.   │                               ████
──────────────────────┼──────────────────────────────────────────────────
F8  · Testing avanz.  │                                    ████ ████
F9  · UAT             │                                         ████
F10 · Lanzamiento     │                                              ████
```

**Semanas de mayor paralelismo (S3–S5):** F2A + F2B + F3 + F6 corren simultáneamente.  
Con un solo desarrollador + agentes, se trabaja en una sola rama a la vez pero el agente genera el código de cada módulo en horas, no días.

---

## 6. Fases detalladas

Cada fase incluye:
- **Alcance exacto** — qué entra
- **Fuera de alcance** — qué el agente no debe tocar
- **Supuestos a documentar** — ambigüedades que el agente debe surfacer antes de escribir
- **Tests en formato Given/When/Then** para los comportamientos críticos
- **Checklist de seguridad** integrado
- **Definition of Done** verificable

---

### F0 · Infraestructura y configuración — Semana 1

> 🔒 Bloqueante para todo lo demás.

#### Alcance exacto

- Crear repositorio Git con estrategia `main` / `develop` / `feature/*` / `fix/*`
- Provisionar producción y staging (Hetzner o DigitalOcean) con Laravel Forge
- Nginx + PHP 8.3-FPM + MySQL 8.0 + Redis en ambos entornos
- S3 o MinIO para documentos clínicos
- Email transaccional (Mailgun o Postmark) con dominio verificado
- Laravel 11 + Filament 3 + Livewire 3 + Alpine.js
- Paquetes core (versiones exactas a fijar en `composer.json`):
  - `spatie/laravel-permission ^6`
  - `spatie/laravel-medialibrary ^11`
  - `spatie/laravel-translatable ^6`
  - `maatwebsite/excel ^3.1`
  - `mcamara/laravel-localization ^2`
  - `pragmarx/google2fa-laravel ^2`
  - `laravel/fortify ^1.21`
- Laravel Pint + PHPStan nivel 6
- Pest como framework de tests
- CI/CD (GitHub Actions): `lint → phpstan → test → deploy staging` en merge a `develop`
- Laravel Telescope (dev/staging) + Sentry o Bugsnag (producción)
- `.env.example` con todas las variables documentadas

#### Fuera de alcance

Ningún modelo, ninguna migración, ningún controlador. Solo infraestructura.

#### Supuestos a documentar

- Proveedor de email elegido (Mailgun vs Postmark)
- Proveedor de servidor (Hetzner vs DigitalOcean)
- Dominio final de la nueva plataforma

#### Tests

```
Dado el pipeline de CI/CD configurado
Cuando se introduce un test que falla en una PR
Entonces la PR no puede mergearse (CI en rojo)
```

#### Checklist de seguridad (F0)

- [ ] `APP_DEBUG=false` en `.env.production`
- [ ] `APP_ENV=production` en `.env.production`
- [ ] Claves de Sentry/Bugsnag en variables de entorno, nunca en código

#### Definition of Done

- [ ] CI/CD pasa en verde con un test vacío
- [ ] Staging responde en HTTPS con certificado válido
- [ ] `.env.example` documenta todas las variables
- [ ] `CLAUDE.md` creado con el contexto inicial del proyecto

---

### F1 · Modelos de datos y autenticación — Semanas 1–2

> 🔒 Bloqueante para F2A, F2B, F3.

#### Alcance exacto

**Migraciones:**

| Tabla | Notas |
|---|---|
| `users` | Usuarios del panel |
| `therapeuts` | Con campo `hubspot_id` nullable |
| `beraters` | Con horario semanal (JSON), días de antelación, país |
| `betreuer` | **Condicional** — solo si Verena confirma que tienen login. Si no, es solo un campo `betreuer_name` en `termins`. |
| `termins` | Con todas las columnas del CSV de producción (78 campos mapeados) |
| `medikaments` | Con `serie`, `sort_order`, soporte translatable |
| `kategorien` | Árbol: `parent_id` nullable (Hauptthema = null, Unterkategorie = parent_id) |
| `feiertags` | Con `country` enum(DE, AT, CH, ALL) |
| `berater_vacations` | `berater_id`, `start_date`, `end_date`, `substitute_berater_id` nullable |

**Interface de slots (crítico para el paralelismo):**

```php
// app/Contracts/SlotAvailabilityServiceInterface.php
interface SlotAvailabilityServiceInterface
{
    /** @return Collection<int, Carbon> */
    public function getAvailableSlots(Berater $berater, Carbon $date): Collection;
    public function isSlotAvailable(Berater $berater, Carbon $dateTime): bool;
    public function canTherapistBook(Therapeut $therapeut, Carbon $date): bool;
}
```

Registrar un `StubSlotAvailabilityService` (devuelve datos hardcodeados de prueba) en `AppServiceProvider`. F2B lo reemplazará con la implementación real.

**Autenticación:**
- Laravel Fortify: login, logout, reset de contraseña
- TOTP 2FA con `pragmarx/google2fa-laravel`
- Spatie Permission: roles `admin`, `berater`, `betreuer` (betreuer condicional)
- Política de contraseñas: mínimo 12 caracteres

**Seeders:**
- 14 Hauptthemen + subcategorías (extraídas del CSV de producción)
- 61 productos con código, serie y sort_order
- 3 asesores: Dr. Petra Blum (lun/mar), Gunter Schlegel (mié), Dr. Ursula Bubendorfer (jue)
- Festivos DE/AT/CH para el año en curso

#### Fuera de alcance

Ningún Resource de Filament. Ningún componente Livewire. Ninguna ruta pública.

#### Supuestos a documentar

- ¿Betreuer tiene login o es solo un campo de texto en la cita? (pendiente Verena)
- ¿Qué columnas de las 78 del CSV son obligatorias en la creación vs opcionales?
- ¿El `Fallnummer` es secuencial global o reinicia cada año?

#### Tests en formato Given/When/Then

```
Dado un Berater con horario de lunes y martes
  y un festivo en 2026-10-03 para DE
Cuando se solicitan los slots del 2026-10-03
Entonces la colección de slots devuelta está vacía
[Nota: este test usa el Stub — verifica la interfaz, no la lógica real]

Dado un usuario con rol "berater"
Cuando intenta acceder a una ruta con middleware "role:admin"
Entonces recibe un 403, no una redirección al login

Dado que se ejecutan los seeders en una BD vacía
Cuando se cuenta Medikament::count()
Entonces el resultado es exactamente 61
```

#### Checklist de seguridad (F1)

- [ ] Passwords hasheadas con `bcrypt` (mínimo cost 12), nunca en texto plano
- [ ] Tokens 2FA no se almacenan en logs
- [ ] Middleware de rol aplicado en `routes/admin.php`, no solo en los controladores

#### Definition of Done

- [ ] `php artisan migrate:fresh --seed` sin errores en entorno limpio
- [ ] Login + 2FA funciona en staging
- [ ] `SlotAvailabilityServiceInterface` resolvible desde el Service Container (stub activo)
- [ ] `docs/supuestos-F1.md` creado con las ambigüedades documentadas

---

### F2A · Panel admin — Gestión — Semanas 2–4

> ⇌ Paralelo con F2B · F3 · F6

#### Alcance exacto

Filament Resources para catálogos y configuración. **No tocar** `Termin`, slots ni lógica de citas.

| Resource / Page | Detalles |
|---|---|
| `TherapeutResource` | Tabla con filtro por membresía, badge visual, contador usos no-socio, campo `hubspot_id` |
| `BeraterResource` | CRUD + horario semanal (checkboxes por día), días de antelación de agenda, gestión de vacaciones con sustituto |
| `BetreuerResource` | CRUD básico — **solo crear si Verena confirma que tiene login; si no, campo de texto** |
| `MedikamentResource` | CRUD con `serie`, `sort_order`, translatable (de/en/fr) |
| `KategorieResource` | Árbol Hauptthema → Unterkategorie, translatable |
| `FeiertageResource` | CRUD por país DE/AT/CH/ALL |
| `EinstellungenPage` | Filament Settings: límite usos no-socios (default: 2), parámetros globales |

**Autorización:** Policies Filament — solo `admin`. El rol `berater` no ve estos módulos en la navegación.

#### Fuera de alcance

- `TerminResource` (es de F2B)
- Cualquier lógica de disponibilidad de slots
- Formulario post-consulta

#### Supuestos a documentar

- ¿Cuántos días de antelación puede ver el asesor su agenda? (¿configurable por asesor o global?)
- ¿El sustituto hereda todas las citas o solo las nuevas durante la ausencia?
- ¿`Betreuer` tiene login o es catálogo de texto?

#### Tests en formato Given/When/Then

```
Dado un usuario con rol "berater" logueado
Cuando navega a /admin/therapeuts
Entonces recibe 403 o es excluido de la navegación (no ve el link)

Dado un Medikament con nombre traducido en "de" = "Lymphomyosot"
Cuando se cambia el locale a "en" y se consulta el nombre
Entonces se devuelve el string en inglés si existe, o el de alemán como fallback

Dado un Berater con sustituto B asignado para la semana del 2026-08-01
Cuando el sistema consulta quién cubre las citas del 2026-08-03
Entonces el sustituto B es el responsable de esas citas
```

#### Checklist de seguridad (F2A)

- [ ] Policy `TherapeutPolicy`: `admin` puede editar; `berater` no tiene acceso
- [ ] `hubspot_id` no se muestra en ningún endpoint accesible por el terapeuta
- [ ] Ningún campo de configuración global modificable por `berater`

#### Definition of Done

- [ ] Los 6 Resources/Pages funcionan en staging
- [ ] Login con usuario `berater` no muestra los módulos de gestión
- [ ] Tests pasan en CI
- [ ] `docs/supuestos-F2A.md` creado

---

### F2B · Panel admin — Citas — Semanas 2–4

> ⇌ Paralelo con F2A · F3 · F6

#### Alcance exacto

**`TerminResource` — Vista admin:**
- Tabla con todas las columnas del CSV: fecha, terapeuta, asesor, betreuer, Hauptthema, estado, FAX-Code, Fallnummer
- Filtros: asesor, Hauptthema, periodo, estado, membresía del terapeuta
- Indicador visual (badge) para terapeutas sin membresía
- Creación manual de cita (para reservas por teléfono)
- Acciones: marcar completada, cancelar, reasignar asesor
- Campo `interner_kommentar`: visible solo para `admin` y `berater`

**Vista del asesor (Berater):**
- Solo las citas asignadas al asesor logueado (o al sustituto activo)
- Detalle: anamnesis completa, documentos descargables
- Formulario post-consulta: cuadrícula de 61 productos agrupados por serie, checkboxes de formaciones, texto libre, comentario interno
- Acción: marcar como completada → dispara `AppointmentCompleted`

**`SlotAvailabilityService` — Implementación real:**

Implementa `SlotAvailabilityServiceInterface` definida en F1. Reemplaza el stub en `AppServiceProvider`.

Reglas de negocio exactas (fuente: CSV de producción + especificación):
- Slots de exactamente **30 minutos** fijos
- Excluye: festivos del país del asesor, periodos de vacaciones del asesor
- Considera: días activos del horario semanal del asesor
- **Límite de reserva: máximo 2 citas por terapeuta por día de consulta**
  - Esta validación es **síncrona** y ocurre antes de crear el `Termin`
  - Nunca delegar esta validación a un Job asíncrono

**Generación de identificadores:**
- `Fallnummer`: secuencial por año (`YYYY-NNNN`, reinicio el 1 de enero)
- `FAX-Code`: UUID v4 truncado a 8 caracteres, único en BD
- Ambos se generan en el Service antes de persistir el `Termin`, no en el modelo

#### Fuera de alcance

- Componentes Livewire (son de F3)
- Lógica de emails (es de F4)
- Exportación XLSX (es de F5)

#### Supuestos a documentar

- ¿El límite de 2 citas es por terapeuta por día, o por terapeuta por asesor por día?
- ¿Un asesor sustituto puede crear nuevas citas o solo ver las heredadas?
- ¿El `interner_kommentar` es editable por el berater o solo por el admin?

#### Tests en formato Given/When/Then

```
[CRÍTICO] Regla del límite de 2 citas — validación síncrona:

Dado un Therapeut T con 2 citas ya reservadas el 2026-09-15
Cuando T intenta reservar una tercera cita el 2026-09-15
Entonces el servicio lanza ValidationException con mensaje "Límite de citas alcanzado"
  y NO se crea ningún Termin en la BD

[CRÍTICO] Concurrencia en reserva del mismo slot:

Dado un slot libre el 2026-09-15 a las 10:00 con el Berater B
Cuando dos terapeutas distintos intentan reservarlo simultáneamente (DB transaction test)
Entonces exactamente uno de los dos recibe confirmación
  y el otro recibe error "Slot no disponible"
  y en la BD hay exactamente 1 Termin para ese slot

[CRÍTICO] Visibilidad del comentario interno:

Dado un Termin con interner_kommentar = "Nota privada"
Cuando un Therapeut accede a los detalles de su cita via portal
Entonces el campo interner_kommentar NO aparece en la respuesta (IDOR)

Dado un Berater A logueado
Cuando manipula la URL para ver una cita asignada al Berater B
Entonces recibe 403
```

#### Checklist de seguridad (F2B)

- [ ] Policy `TerminPolicy`: `berater` solo accede a sus citas (scope en Eloquent, no solo en UI)
- [ ] `interner_kommentar` excluido de cualquier respuesta accesible por el terapeuta
- [ ] Generación de `FAX-Code` usa fuente criptográficamente segura (`random_bytes`)
- [ ] La validación del límite de 2 citas usa DB transaction para evitar race conditions

#### Definition of Done

- [ ] `SlotAvailabilityService` real registrado en Service Container (stub reemplazado)
- [ ] Test de concurrencia pasa en entorno con MySQL real (no solo SQLite)
- [ ] Formulario post-consulta guarda los 61 productos correctamente
- [ ] `docs/supuestos-F2B.md` creado

---

### F3 · Portal público de reserva — Semanas 3–5

> ⇌ Paralelo con F2A · F2B · F6  
> Usa `SlotAvailabilityServiceInterface` — funciona con el stub de F1; la implementación real de F2B se inyecta automáticamente al estar lista.

#### Alcance exacto

Aplicación Livewire embebible como `<iframe>` en `megemit.org`. El terapeuta se identifica por token HMAC, sin sesión Laravel.

**`TherapistTokenService`:**
- Genera token: `HMAC-SHA256(email + "|" + timestamp, APP_KEY)`
- Verifica: firma válida + timestamp no expirado (expiración configurable, default 24h)
- Rejects: firma incorrecta → 401; timestamp expirado → 401 con mensaje específico
- **Nunca** lanza 500 por un token inválido

**Componentes Livewire:**

| Componente | Función |
|---|---|
| `BookingCalendar` | Selector de asesor + calendario de fechas disponibles. Reactivo, sin recarga. |
| `AnamnesisForm` | Campos obligatorios: Betreff, Hauptthema, Unterkategorie (dependiente), Geschlecht, Alter, Gewicht, Größe, PLZ. Weitere Angaben opcional. |
| `FileUpload` | Formatos: PDF, JPEG, EPS, DOC/DOCX. Aviso de anonimización obligatorio y visible antes del primer upload. |
| `BookingConfirmation` | Muestra Fallnummer y FAX-Code. Checkbox DSGVO obligatorio para completar la reserva. |
| `AppointmentManagement` | Citas activas del terapeuta. Opciones: modificar, cancelar. |

**Integración iframe:**
- Ruta: `/{locale}/buchen?token={hmac}`
- Eliminar header `X-Frame-Options` en estas rutas (conflicto con CSP)
- Añadir: `Content-Security-Policy: frame-ancestors 'self' https://megemit.org`
- La URL del iframe en megemit.org se actualiza en F10, no antes

#### Fuera de alcance

- Login de usuario (el portal no usa sesión Laravel)
- Panel admin (es de F2)
- Lógica de emails (es de F4)

#### Supuestos a documentar

- ¿El token lo genera Verena manualmente o se auto-genera al registrar un terapeuta?
- ¿Qué pasa si un terapeuta no-socio llega al límite — puede ver sus citas existentes o pierde acceso total?
- ¿El calendario muestra todos los asesores o el terapeuta elige primero el asesor?

#### Tests en formato Given/When/Then

```
[CRÍTICO] Token expirado:

Dado un token generado hace 25 horas (expiración = 24h)
Cuando el terapeuta accede al portal con ese token
Entonces recibe una página de error con mensaje claro en alemán
  y NO recibe una pantalla en blanco ni un stack trace

[CRÍTICO] Anonimización:

Dado un formulario de anamnesis con "Weitere Angaben" = "El paciente es Hans Mueller"
Cuando el sistema valida el campo (no se puede hacer automáticamente — es texto libre)
Entonces el aviso de anonimización es visible y el terapeuta confirma haberlo leído
  (checkbox obligatorio antes del submit)

[CRÍTICO] Limite no-socio en el portal:

Dado un Therapeut no-socio con 2 citas ya registradas
Cuando accede al portal con token válido
Entonces el botón de nueva reserva está deshabilitado con mensaje explicativo
  y puede ver y gestionar sus citas existentes

[CRÍTICO] Reserva crea Termin con todos los campos:

Dado un terapeuta con token válido que completa el formulario
Cuando hace submit de la reserva
Entonces en la BD existe un Termin con:
  - therapeut_id correcto
  - berater_id correcto
  - fecha y slot correctos
  - todos los campos de anamnesis rellenados
  - fallnummer y fax_code únicos generados
  - estado = "pendiente"
```

#### Checklist de seguridad (F3)

- [ ] Token HMAC usa `hash_equals()` para la comparación (previene timing attacks)
- [ ] Token inválido devuelve siempre 401, nunca información sobre por qué falló (no exponer si el email existe)
- [ ] Documentos subidos a S3/MinIO con visibilidad `private` — nunca `public`
- [ ] `Content-Security-Policy` configurado correctamente para el iframe
- [ ] No se almacenan datos del formulario en la sesión de PHP (portal sin sesión)

#### Definition of Done

- [ ] Portal funciona dentro de un iframe de prueba en staging
- [ ] Token expirado muestra error legible, no pantalla en blanco
- [ ] Documentos subidos no accesibles por URL directa (requieren URL firmada)
- [ ] `docs/supuestos-F3.md` creado

---

### F3.5 · Auditoría de paridad funcional con la plataforma original — Semana 5

> ⚠️ **Origen:** auditoría manual (2026-08-03) de `admin.mgm.concloo.net` (producción, Concloo/Domino),
> navegada con credenciales reales cedidas por el cliente en modo exclusivamente lectura (ninguna
> acción de guardado/borrado ejecutada). El objetivo del plan original era reimplementar
> "sin acceso al código fuente" (§1), pero sí existe acceso de solo-lectura al panel de admin en
> producción — esta fase cierra la brecha entre lo ya construido (F0–F3, F5, F6-adelantado) y lo
> que realmente existe en producción, **antes de continuar con F4 en adelante**.
>
> Todos los hallazgos de esta fase están documentados en detalle en `docs/supuestos-F3.5.md`,
> con capturas de pantalla NO conservadas (contenían datos personales de pacientes/terapeutas
> reales — se descartaron tras extraer la información estructural).

#### Alcance exacto

- **Países**: ampliar de 3 (`DE`/`AT`/`CH`) a **5** — añadir `IT` (Italy) y `SK` (Slovakia) en
  el enum/lista de países de `Therapeut`, `Berater`, `Feiertag` y en `lang/{de,es}/admin.php` →
  `pais.*`. Confirmado con datos reales: 7 terapeutas en Italy, 1 en Slovakia en producción.
- **Hauptthema**: el seeder actual solo siembra 14; producción tiene **19** activos. Faltan:
  `COVID`, `Cardio Vascular`, `Innere Medizin`, `Schilddrüse`, `Pneumologie`,
  `Diagnostik Besprechung`. Añadir al seeder con sus Unterkategorien correspondientes
  (producción tiene 100 Unterkategorien en total, repartidas entre los 19 Hauptthemen).
- **Berater**: el seeder/tests actuales asumen 3 asesores reales; producción tiene **7** activos
  (Dr. Petra Blum, Gunter Schlegel, Eciel Hölzler, Ina Chammah, Dr. Andrea Stecher Schilling,
  Dr. Renate Schied, Dr. Ursula Bubendorfer) + 1 "Testberater" inactivo de desarrollo. Revisar
  `SeedersTest` ("siembra exactamente 3 asesores reales") y el seeder real antes de producción.
- **Unterkategorie — campo `Freitextfeld`**: cada Unterkategorie en producción tiene un flag
  booleano que determina si al seleccionarla se muestra un campo de texto libre adicional
  ("Weitere Angaben") con **etiqueta dinámica** (`Weitere Angaben (<nombre de la Unterkategorie>)`),
  en vez de un campo siempre visible con etiqueta fija como en la implementación actual de F2B/F3.
  Añadir columna `requiere_freitextfeld` (o similar) a `kategorias` y condicionar la UI.
- **Anamnesis clínica ampliada**: el formulario real de F3/F2B captura ~15 campos clínicos
  adicionales, ausentes en la implementación actual: `Hauptsymptome (welche seit wann)`,
  `Auslöser`, `Operationen`, `Frühere Erkrankungen`, `Familiäre Erkrankungen`, `Impfreaktionen`,
  `Allergien/Unverträglichkeiten`, `Unfälle (was/wann)`, `Sozio-emotionaler Zustand`,
  `Medikamente`, `Welche Ernährungsrichtung`, `Stuhl- und Schleimhautverhalten`,
  `Zahnstatus, Parodontitis`, `Therapievorschlag und Kurzinterpretation Immunstatus`.
  Estos campos deben añadirse a `Termin`, al wizard del portal (F3) y a las 78 columnas del
  export XLSX (F5) si el CSV de producción real los incluye (revisar contra el CSV de referencia,
  ya que las columnas capturadas hasta ahora en `docs/03-2026 (1).xlsx - Termine.csv` no las
  contienen explícitamente — puede ser un export distinto/parcial).
- **Tercer campo de notas internas**: producción distingue `Kommentar zum Termin` (visible),
  `Interner Kommentar` (interno) y un tercer campo `Therapieempfehlung / Notizen (vom Therapeuten
  nicht einsehbar)` — específico para la recomendación de tratamiento, nunca visible al terapeuta.
  La implementación actual solo tiene `interner_kommentar`. Añadir el campo que falta.
- **Campo `Fallnummer Referenz`**: distinto de `Fallnummer` (que es un contador secuencial global
  por Therapeut). Parece usarse para vincular una cita de seguimiento con el caso original.
  Confirmar semántica exacta con Verena antes de implementar (ver Supuestos).
- **Géneros/listas configurables**: en producción, `Geschlecht`, `Alter` y `Betreff` son listas
  editables desde `Einstellungen → Allgemein` (textarea de opciones), no enums fijos en BD.
  `Geschlecht` incluye una tercera opción, **`divers`**, ausente en la implementación actual
  (`geschlecht` es un `ENUM('weiblich','maennlich')` — cambiar a lista configurable o, como
  mínimo, añadir `divers` al enum).
- **Documentos — vista de administración**: producción sí tiene un listado paginado con
  descarga de los documentos subidos por el terapeuta, visible desde el panel admin. F3 solo
  implementó la subida (Media Library) sin UI de descarga en el panel — cerrar esa brecha ya
  señalada como pendiente en `docs/supuestos-F3.md`.
- **Acción "Termin verschieben" (reagendar)**: botón dedicado en el detalle de cita del panel
  admin para reagendar una cita existente sin cancelar+recrear. No existe en `TerminResource`
  actual (solo completar/cancelar/reasignar asesor).
- **Therapeut — campos adicionales**: 4 direcciones de email (`Mailadresse`, `2. Mailadresse`,
  `3. Mailadresse`, `Fachkreis-Mailadresse`) en vez de 1; campo `Anrede` (fórmula de tratamiento
  para plantillas de email, relevante para F4); campo `Login-Name` (identificador de usuario,
  no necesariamente una autenticación con contraseña — confirmar con Verena si tiene algún uso
  más allá de un slug identificador).
- **`Hilfsthemen` como catálogo gestionable**: en producción, "Grundlagenseminar / A1 / A2 /
  Lyty / Serologien / Bestimmte Pathologien / Sonstiges" es una lista CRUD con orden y estado
  activo/inactivo (`Einstellungen → Hilfsthemen`), no columnas booleanas fijas en `Termin`
  (documentado como decisión ya tomada en CLAUDE.md — se adoptó fijo por alcance de F1, revisar
  si merece la pena migrarlo a un catálogo antes de F9).

#### Fuera de alcance (de esta fase; quedan como fases/tickets propios)

- **Modelo de Slots discreto por asesor/día/hora** (109 registros individuales en producción,
  cada uno con su propio toggle activo/inactivo, en vez de un `weekly_schedule` +
  `start_time`/`end_time` computado). Migrar `SlotAvailabilityService` a este modelo es un cambio
  de arquitectura mayor — documentar como decisión pendiente, no implementar aquí.
- **Integración con solve360 (CRM) en vivo**: producción sincroniza periódicamente contra
  solve360 (usuario + API token configurados en `Einstellungen → Allgemein`, con marca de
  "Letzter Datenabgleich"). El plan actual (F7) solo contempla un esqueleto `HubSpotCrmAdapter`
  — evaluar si migrar a HubSpot sigue siendo la decisión correcta o si hay que mantener/replicar
  la integración real con solve360 primero. Requiere decisión de negocio con Verena.
- **Verificación de membresía (`Mitglied`) contra WordPress**: producción tiene una conexión SQL
  directa a una base de datos WordPress externa (tabla de usuarios, campos `user_login`/
  `user_email`) — probable mecanismo real para determinar si un terapeuta es socio, en vez de
  (o además de) un campo manual. **Las credenciales de esa conexión se vieron expuestas en texto
  plano en el panel de producción durante esta auditoría — recomendamos rotarlas** independientemente
  de esta fase. No implementar la réplica de esta integración sin antes confirmar con Verena el
  mecanismo real y sin exponer credenciales en ningún commit/doc.
- **Modelo real del límite de citas**: producción no parece implementar "2 citas/día" (F2B) ni
  "límite total no-socio" (F3) tal como se documentó — en su lugar hay un límite global de
  **"Aktive Fälle" (casos activos) por terapeuta**, configurable (`Maximale Beratungstermine pro
  Therapeut`, valor real observado: 3) con posibilidad de override individual por terapeuta
  (`max-Termine`, "nicht definiert" = usa el valor global) y un toggle "no dar cita nueva hasta
  agotar/cerrar los casos activos". Ver Supuestos — **no tocar la lógica síncrona ya implementada
  y testeada de F2B sin confirmar antes con Verena**, dado que ambos modelos (día/socio vs.
  casos-activos) podrían coexistir o el segundo podría ser el único real.
- **Reglas de retención/purga de datos** (festivos expirados a 28 días, citas expiradas a 120
  días con vistas residuales de 60 días para Berater/Therapeut) — relevante para DSGVO/F8, no F3.5.
- **Recordatorio de email pre-cita (24h configurable)** — pertenece a F4, ya contemplado ahí.

#### Supuestos a documentar

Ver tabla completa en `docs/supuestos-F3.5.md`. Puntos que requieren confirmación explícita de
Verena antes de tocar código ya construido (F2B/F3/F5):

- ¿El límite real de citas es por "casos activos" (no por día ni por socio/no-socio)? Si es así,
  **F2B y F3 se implementaron sobre un supuesto incorrecto** y habría que revisar el diseño
  síncrono ya testeado, no solo añadir campos nuevos.
- ¿Qué significa exactamente `Fallnummer Referenz` frente a `Fallnummer`?
- ¿El campo `Login-Name` del terapeuta implica autenticación real (contraseña) en algún flujo,
  o es solo un identificador?
- ¿La integración con solve360 sigue siendo la fuente de verdad para `Mitglied`, o se sustituye
  por el nuevo panel sin más? Si sigue viva, F7 (HubSpot) necesita replantearse.
- ¿Las 15 columnas de anamnesis ampliada deben incluirse en el export XLSX de F5 (78 columnas)?
  El CSV de referencia usado para F5 no las mostraba — confirmar si es un export distinto/recortado.

#### Tests

- [x] `it [F3.5] siembra exactamente 19 Hauptthemen (verificado contra producción)` (reemplaza el
      test de 14 de F1) — `tests/Feature/SeedersTest.php`
- [x] `it [F3.5] siembra exactamente 7 asesores reales (verificado contra producción)` (reemplaza
      el test de 3 de F1) — `tests/Feature/SeedersTest.php`
- [x] `it [F3.5] acepta IT y SK como país de un Berater` / `de un Feiertag` —
      `tests/Feature/F3_5GapsTest.php`
- [x] `it [F3.5] marca requiere_freitextfeld=true en Unterkategorien con el patrón "Andere/Anderes"`
      — `tests/Feature/F3_5GapsTest.php`
- [x] `it [F3.5] acepta "divers" como valor válido de Geschlecht en un Termin` —
      `tests/Feature/F3_5GapsTest.php`
- [x] `it [F3.5] Therapeut acepta 4 direcciones de email, Anrede y Login-Name` —
      `tests/Feature/F3_5GapsTest.php`
- [x] `it [F3.5] Termin persiste los 14 campos de anamnesis ampliada` —
      `tests/Feature/F3_5GapsTest.php`
- [x] `it [F3.5] notas_tratamiento_interno es independiente de interner_kommentar` —
      `tests/Feature/F3_5GapsTest.php`
- [x] `it [F3.5] la página de edición de Termin renderiza sin errores con los nuevos campos` —
      `tests/Feature/F3_5GapsTest.php`
- [x] `it [F3.5] no ofrece slots con menos antelación que la mínima configurada en Einstellungen` —
      `tests/Feature/SlotAvailabilityServiceTest.php`
- [x] `it [F3.5] el tope global de antelación acorta/nunca amplía el advance_days del Berater` —
      `tests/Feature/SlotAvailabilityServiceTest.php`

#### Checklist de seguridad (F3.5)

- [x] Ninguna captura de pantalla ni HTML descargado de `admin.mgm.concloo.net` durante esta
      auditoría se conserva en el repositorio ni en el historial de commits (contenían datos
      personales de pacientes/terapeutas reales y, en un caso, credenciales de producción)
- [ ] Recomendar al cliente rotar la contraseña de SQL Server y el token de API de solve360
      vistos expuestos en `Einstellungen → Allgemein` del panel de producción (pendiente de
      confirmación del cliente de que se ha hecho)
- [x] Ninguna acción de escritura (guardar/borrar/reagendar) se ejecutó contra producción durante
      la auditoría — confirmado: solo se realizaron peticiones `GET`

#### Definition of Done

- [x] `docs/supuestos-F3.5.md` creado con la tabla completa de hallazgos y decisiones pendientes
- [x] `helpmi-planificacion.md` (este documento) y `CLAUDE.md` actualizados con los gaps aceptados
- [x] Gaps que NO dependían de Verena implementados y verificados (tests + Pint + PHPStan nivel 6
      + verificación visual en navegador de país IT/SK, Freitextfeld dinámico y anamnesis ampliada)
- [ ] Verena ha confirmado (o se ha marcado explícitamente como pendiente) cada punto de
      "Supuestos a documentar" restante antes de iniciar F4 — en particular el límite de citas por
      "casos activos", que queda **explícitamente aparcado sin tocar** a petición del cliente
- [ ] Cliente notificado de la exposición de credenciales de producción encontrada

---

### F4 · Emails automáticos y Scheduler — Semanas 5–6

> 🔗 Depende de F2B y F3 (eventos `AppointmentCreated`, `AppointmentCancelled`, `AppointmentCompleted`)

#### Alcance exacto

**Plantillas de email:**

| Plantilla | Evento/Trigger | Destinatario |
|---|---|---|
| Confirmación de reserva | `AppointmentCreated` | Terapeuta |
| Recordatorio 24h antes | Scheduler diario 08:00 | Terapeuta |
| Agenda del asesor | Scheduler diario 07:00 (X días antes, config. por asesor) | Berater |
| Aviso límite no-socio | Después de crear la 2ª cita del no-socio | Terapeuta |

Todas las plantillas en `resources/views/emails/` (Blade). Sin HTML inline en el código PHP.  
Strings en `lang/de/emails.php`. Textos exactos a validar con Verena en F9.

**Jobs y Scheduler:**
```
SendBookingConfirmationJob   ← listener de AppointmentCreated
SendReminderJob              ← Scheduler diario 08:00
SendAdvisorAgendaJob         ← Scheduler diario 07:00
SendNonMemberWarningJob      ← listener de AppointmentCreated (si es no-socio en límite)
GenerateMonthlyReportJob     ← Scheduler día 1 de cada mes 06:00
```

> ⚠️ El email de aviso de no-socio es asíncrono (Job). La **validación** que bloquea la reserva es síncrona en F2B/F3. Son dos cosas distintas.

#### Fuera de alcance

- Nuevas plantillas no listadas arriba
- Lógica de reintentos avanzada (la que trae Laravel Queue por defecto es suficiente)
- Sistema de notificaciones in-app

#### Supuestos a documentar

- Textos exactos de los emails (pendiente validación con Verena en F9)
- ¿Cuántos días antes se envía la agenda al asesor? (¿es configurable por asesor?)
- ¿El email de aviso no-socio se envía al crear la 2ª cita, o al intentar crear la 3ª?

#### Tests en formato Given/When/Then

```
Dado un Termin creado con el evento AppointmentCreated disparado
Cuando el worker procesa la cola
Entonces el Terapeuta recibe un email con:
  - Su Fallnummer en el asunto
  - Su FAX-Code en el cuerpo
  - La fecha y hora de la cita correctas
  - El email redactado en alemán

Dado un Berater configurado para recibir la agenda 2 días antes
Cuando el Scheduler ejecuta SendAdvisorAgendaJob el 2026-09-13
Entonces el Berater recibe la lista de citas del 2026-09-15
  y NO las citas del 2026-09-14

Dado que el Scheduler ejecuta en una BD sin citas para mañana
Cuando corre SendReminderJob
Entonces no se envía ningún email (cero jobs encolados)
```

#### Checklist de seguridad (F4)

- [x] Ningún dato personal del paciente en el asunto ni en el cuerpo del email (DSGVO) — solo
      Fallnummer/FAX-Code/fecha/hora/nombre del terapeuta destinatario, sin datos clínicos
- [ ] Emails de prueba enviados a Mailtrap en staging — nunca a direcciones reales (pendiente:
      staging aún no desplegado, ver F10)
- [x] Variable `MAIL_MAILER=log` en `.env.testing` para que los tests no envíen emails reales

#### Definition of Done

- [ ] Queue workers activos en staging (Supervisor) — pendiente de despliegue (F10)
- [ ] Scheduler activo en staging (`* * * * * php artisan schedule:run`) — pendiente de
      despliegue (F10); registrado en código vía `bootstrap/app.php`→`withSchedule()`
- [x] Email de confirmación verificado — renderizado completo comprobado localmente (todos los
      campos correctos, alemán); verificación en Mailtrap real pendiente de staging (F10)
- [x] `docs/supuestos-F4.md` creado (textos y varios puntos marcados `[PENDIENTE Verena]`)
- [x] 8 tests cubriendo confirmación, aviso no-socio (momento exacto), recordatorio (idempotente),
      agenda del asesor (días configurables), informe mensual — `tests/Feature/F4EmailsTest.php`
- [x] Bug real encontrado y corregido: registrar los listeners manualmente en
      `AppServiceProvider` duplicaba cada email (conflicto con el auto-discovery de Laravel 11)

---

### F5 · Estadísticas e informes — Semanas 6–7

> ⇌ Paralelo con F7  
> 🔗 Depende de F2B (modelo `Termin` completo)

#### Alcance exacto

**Dashboard (Filament):**
- Filtro de periodo: mensual, trimestral, semestral, anual, rango personalizado
- Métricas: total de citas, desglose por asesor / país / Hauptthema
- Top 10 productos más recomendados
- Ratio socios/no-socios

**Exportación XLSX:**
- **78 columnas exactas** en el orden del CSV de producción (desde `Termin` hasta `RANTES 27CH`)
- Exportación manual desde el panel
- El `GenerateMonthlyReportJob` (F4) llama al mismo exportador

#### Fuera de alcance

- Gráficos o visualizaciones interactivas (la especificación no las contempla)
- Exportación a CSV o PDF
- Dashboard accesible por el rol `berater`

#### Supuestos a documentar

- ¿El dashboard de estadísticas es visible para `berater` (solo sus propias citas) o solo para `admin`?
- ¿El informe mensual se envía a una dirección fija o configurable desde el panel?

#### Tests en formato Given/When/Then

```
Dado 5 citas del Berater A y 3 del Berater B en marzo 2026
Cuando se consultan las estadísticas del periodo marzo 2026
Entonces el total es 8
  y el desglose muestra Berater A: 5, Berater B: 3
  y la suma de los desgloses es igual al total

Dado un dataset de prueba de 10 citas con campos conocidos
Cuando se exporta el XLSX
Entonces el fichero tiene exactamente 78 columnas
  y los headers de las columnas coinciden exactamente con el CSV de referencia
  y los valores de las columnas 1-10 son correctos para las primeras 3 filas
```

#### Checklist de seguridad (F5)

- [ ] Exportación XLSX accesible solo para `admin`
- [ ] Las métricas del dashboard no revelan datos personales de terapeutas

#### Definition of Done

- [ ] XLSX exportado manualmente comparado columna por columna con el CSV de referencia
- [ ] Tests pasan en CI con el orden de columnas verificado programáticamente

---

### F6 · Multiidioma — Transversal, Semanas 2–7

> ⟳ No es una fase independiente. Disciplina que acompaña a cada fase.

#### Alcance exacto

**Semana 2 (junto con F1):**
- `mcamara/laravel-localization`: routing con prefijo `/{locale}/` en rutas públicas
- Middleware `SetLocale`: lee `Accept-Language` o parámetro `locale` y llama `App::setLocale()`
- Filament: locale del perfil del usuario admin

**Por cada módulo entregado en F2–F5:** añadir sus claves a `lang/de/`:

| Fichero | Fase |
|---|---|
| `auth.php`, `validation.php`, `pagination.php` | F1 |
| `admin.php` | F2A, F2B |
| `booking.php`, `anamnesis.php` | F3 |
| `emails.php` | F4 |
| `stats.php` | F5 |

#### Fuera de alcance

- Traducciones a idiomas distintos de `de` (son una ampliación posterior)
- Traducción manual de contenido en BD (lo introduce el admin desde Filament)

#### Tests

```
Dado que se recorre cada fichero Blade y Livewire del proyecto
Cuando se busca cualquier string literal en alemán fuera de __() o trans()
Entonces el resultado es cero ocurrencias
(implementar como comando artisan: php artisan i18n:audit)
```

#### Definition of Done

- [ ] `php artisan i18n:audit` devuelve cero strings hardcodeados
- [ ] Cambiar locale del usuario admin cambia la UI completa

---

### F7 · Arquitectura HubSpot — Semana 7

> ⇌ Paralelo con F5  
> ⚠️ Solo arquitectura y eventos. Sin llamadas HTTP reales a HubSpot.

#### Alcance exacto

**Eventos de dominio** (registrados en `EventServiceProvider`):
```php
AppointmentCreated::class
AppointmentCompleted::class
AppointmentCancelled::class
TherapistCreated::class
TherapistMembershipChanged::class
```

**Capa CRM:**
```php
interface CrmAdapterInterface {
    public function syncContact(Therapeut $therapeut): void;
    public function recordActivity(Termin $termin): void;
}

// Activo por defecto — no-op, no falla
class NullCrmAdapter implements CrmAdapterInterface {}

// Esqueleto — métodos lanzan NotImplementedException
class HubSpotCrmAdapter implements CrmAdapterInterface {}
```

- `SyncToCrmJob`: Job en cola que consume los eventos
- `CRM_ADAPTER=null|hubspot` en `.env` controla el adapter
- Binding en `AppServiceProvider`

#### Fuera de alcance

- Ninguna llamada HTTP a la API de HubSpot
- Ningún mapeo de campos específico de HubSpot
- Ningún webhook de entrada desde HubSpot

#### Tests

```
Dado CRM_ADAPTER=null
Cuando se crea un Termin y se dispara AppointmentCreated
Entonces NullCrmAdapter::syncContact() no lanza ninguna excepción
  y el Termin se crea correctamente independientemente del CRM

Dado CRM_ADAPTER=hubspot
Cuando se crea un Termin
Entonces SyncToCrmJob se encola
  y al procesarlo llama a HubSpotCrmAdapter::recordActivity()
  y lanza NotImplementedException (comportamiento esperado del esqueleto)
```

#### Definition of Done

- [ ] Eventos visibles en Laravel Telescope al realizar acciones en staging — pendiente de
      despliegue (Telescope no instalado/staging no existe todavía, ver F10)
- [x] `CRM_ADAPTER=null` no genera ningún log de error — verificado con test
- [x] Arquitectura completa (interfaz, NullCrmAdapter, HubSpotCrmAdapter esqueleto, SyncToCrmJob,
      5 eventos de dominio, binding condicional) — `docs/supuestos-F7.md`
- [x] 7 tests cubriendo ambos escenarios Given/When/Then del plan más disparo correcto de
      `TherapistCreated`/`TherapistMembershipChanged` — `tests/Feature/F7CrmArchitectureTest.php`

---

### F8 · Testing avanzado y seguridad — Semanas 7–8

> ⇌ Paralelo con F7 durante S7  
> 🔗 Requiere F2, F3, F4, F5 completos y estables en staging

#### Alcance exacto

**Tests de rendimiento (k6 o Artillery):**

| Escenario | Umbral (p95) |
|---|---|
| Carga del calendario de slots con 50 usuarios simultáneos | < 300 ms |
| 10 uploads simultáneos de ficheros | < 30 s sin timeout |
| Login en panel admin | < 200 ms |
| Generación XLSX con 200+ citas | < 30 s |
| Reserva simultánea del mismo slot (2 usuarios) | 1 éxito, 1 error 409, cero 500 |

**Tests funcionales E2E (Dusk):**
- Flujo completo: reserva → confirmación email → consulta → post-consulta → XLSX
- Flujo de cancelación: terapeuta cancela → slot disponible en calendario
- Flujo de sustitución: asesor en vacaciones → sustituto ve las citas
- Portal embebido: renderizado correcto dentro de iframe

**Revisión de seguridad (checklist final):**
- [ ] `interner_kommentar` no accesible en ningún endpoint del portal
- [ ] IDOR: berater no puede ver citas de otro berater por manipulación de URL
- [ ] CSRF: token presente y validado en todos los formularios (incluyendo Livewire)
- [ ] Documentos clínicos: solo accesibles por URL firmada con tiempo limitado (<1h)
- [x] Headers: `X-Powered-By` eliminado, `Referrer-Policy: strict-origin-when-cross-origin`
- [ ] DSGVO: datos personales no aparecen en logs de Sentry (scrubbing configurado) — bloqueado, pendiente cuenta/DSN de Sentry del cliente
- [ ] SQLi: ningún query construido con concatenación de strings del usuario
- [x] 2FA: si un usuario tiene 2FA activo, no puede saltárselo manipulando la URL

#### Fuera de alcance

- Pentest completo (está fuera del alcance de esta fase, se puede contratar externamente post-lanzamiento)
- Análisis de dependencias (Dependabot se configura en F0 y corre en CI)

#### Definition of Done

- [ ] Informe de k6 adjunto en el PR de F8 (todos los umbrales cumplidos)
- [ ] Tests Dusk pasan en CI con ChromeDriver
- [ ] Checklist de seguridad revisado y todos los ítems marcados
- [ ] `docs/supuestos-F8.md` documenta cualquier vulnerabilidad aceptada como riesgo conocido

---

### F9 · UAT con Verena Heufler — Semana 9

> 🔗 Requiere F8 sin incidencias bloqueantes abiertas  
> 🔒 Las 17 preguntas pendientes y todos los archivos `docs/supuestos-FN.md` deben estar respondidos antes de esta fase

El desarrollador prepara staging con ≥ 20 citas de prueba representativas (distintos asesores, socios y no-socios).

#### Checklist de aceptación

**Reserva y portal:**
- [ ] Reservar cita como terapeuta socio → confirmar email y Fallnummer
- [ ] Intentar 3ª cita el mismo día → sistema bloquea con mensaje claro
- [ ] Reservar como no-socio hasta el límite → recibir email de aviso
- [ ] Cancelar una cita → el slot aparece libre en el calendario

**Panel del asesor:**
- [ ] Revisar anamnesis completa y documentos descargables
- [ ] Rellenar formulario post-consulta (marcar productos, añadir comentario)
- [ ] Marcar como completada
- [ ] Confirmar que `interner_kommentar` no es visible al terapeuta

**Panel de administración:**
- [ ] Crear cita manualmente
- [ ] Gestionar festivos: añadir, eliminar
- [ ] Configurar vacaciones de un asesor con sustituto
- [ ] Modificar el catálogo: añadir un producto nuevo

**Emails:**
- [ ] Confirmación recibida con datos correctos (Verena valida el texto alemán)
- [ ] Recordatorio llegado 24h antes (forzar con Scheduler manual)
- [ ] Agenda del asesor correcta

**Informes:**
- [ ] XLSX generado con 78 columnas en el orden correcto
- [ ] Comparar con el CSV de referencia de marzo 2026

**Portal embebido:**
- [ ] Renderizado correcto en el iframe de megemit.org (staging)
- [ ] Responsive en las dimensiones actuales del iframe

**DSGVO:**
- [ ] Checkbox de protección de datos presente y obligatorio
- [ ] Aviso de anonimización visible antes del upload
- [ ] Verena confirma que los datos cumplen la política de MeGeMIT

#### Gestión de incidencias

| Severidad | Definición | Acción |
|---|---|---|
| Bloqueante | Impide completar un flujo de negocio | Corrección inmediata, re-test el mismo día |
| Mayor | Incorrecto pero con workaround | Corrección antes del lanzamiento |
| Menor | Visual o de texto | Backlog post-lanzamiento |

#### Definition of Done

- [ ] Checklist de aceptación completo y firmado por Verena
- [ ] Cero incidencias bloqueantes abiertas
- [ ] Todos los archivos `docs/supuestos-FN.md` revisados y validados
- [ ] Fecha de lanzamiento confirmada

---

### F10 · Lanzamiento a producción — Semana 10

> ✅ Requiere F9 aprobado sin incidencias bloqueantes

#### Preparación pre-cutover

- [ ] Backup completo de la BD Concloo antes de cualquier acción
- [ ] Smoke test en staging con datos de producción anonimizados
- [ ] Queue workers configurados con Supervisor en producción
- [ ] Crontab con `* * * * * php artisan schedule:run` activo
- [ ] SPF, DKIM, DMARC verificados en el dominio de email de producción
- [ ] `spatie/laravel-backup` configurado: backups diarios, retención 30 días
- [ ] Alertas de Sentry activas para producción

#### Secuencia de cutover

```bash
# 1. Deploy
git checkout main && git merge develop && git tag v1.0.0
# (CI/CD despliega a producción automáticamente al pushear el tag)

# 2. Migraciones
php artisan migrate --force

# 3. Datos iniciales de producción
php artisan db:seed --class=ProductionSeeder

# 4. Verificar servicios
php artisan queue:monitor
php artisan schedule:list

# 5. Test de email en producción
php artisan tinker → Mail::to('test@megemit.org')->send(new TestMail())

# 6. Actualizar iframe en megemit.org (WordPress)
# Cambiar la URL del iframe al nuevo dominio

# 7. Verificar el portal desde megemit.org en producción
```

> 📅 **Ventana recomendada:** miércoles tarde, después de las consultas de Gunter Schlegel. El jueves (Dr. Bubendorfer) será el primer día real en el nuevo sistema.

#### Decisiones sobre datos históricos (acordar con Verena antes del cutover)

- ¿Se migran citas históricas cerradas, o solo los terapeutas y catálogos?
- ¿Se mantiene acceso de lectura a Concloo durante la transición?
- ¿Qué periodo de datos históricos importar?

#### Post-lanzamiento

- Monitorización activa durante 48h (Sentry, logs, colas)
- Sistema Concloo accesible en modo lectura **2 semanas** como fallback
- Plan de rollback documentado: cómo revertir la URL del iframe si hay fallo crítico
- Sesión de formación con Verena: panel admin, citas, estadísticas, exportación
- Documento de handover: accesos, cómo añadir idiomas, cómo añadir productos

---

## 7. Matriz de cobertura de tests

| Módulo / Funcionalidad | Unit | Integration | Functional | Performance | Acceptance |
|---|:---:|:---:|:---:|:---:|:---:|
| Login + 2FA | ✓ | ✓ | | | ✓ |
| Roles y permisos (IDOR incluido) | ✓ | ✓ | | | |
| SlotAvailabilityService (festivos, vacaciones) | ✓ | ✓ | | ✓ | |
| Límite 2 citas/día — validación síncrona | ✓ | ✓ | | | ✓ |
| Concurrencia en reserva del mismo slot | | ✓ (MySQL) | | ✓ | |
| TherapistTokenService (expiración, HMAC) | ✓ | ✓ | | | |
| Formulario de reserva completo | ✓ | ✓ | ✓ | | ✓ |
| Upload de documentos (S3 privado) | | ✓ | ✓ | ✓ | |
| Generación Fallnummer + FAX-Code (unicidad) | ✓ | ✓ | | | ✓ |
| Formulario post-consulta (61 productos) | ✓ | ✓ | ✓ | | ✓ |
| `interner_kommentar` no accesible por terapeuta | ✓ | ✓ | | | ✓ |
| Email confirmación de reserva | ✓ | ✓ | | | ✓ |
| Email recordatorio 24h | ✓ | ✓ | | | ✓ |
| Email agenda del asesor (días correctos) | ✓ | ✓ | | | ✓ |
| Email aviso no-socio (asíncrono, no bloquea) | ✓ | ✓ | | | ✓ |
| Exportación XLSX (78 columnas en orden exacto) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Dashboard estadísticas (sumas coherentes) | ✓ | ✓ | ✓ | | ✓ |
| Gestión festivos + vacaciones + sustituciones | ✓ | ✓ | | | ✓ |
| Multiidioma UI (cero strings hardcodeados) | ✓ | ✓ | | | |
| Translatable en BD (fallback a de) | ✓ | ✓ | | | |
| Eventos de dominio (CRM arch.) | ✓ | ✓ | | | |
| Portal embebido en iframe | | | ✓ | | ✓ |
| Token inválido → 401, no 500 | ✓ | ✓ | | | |
| Documentos: URL firmada, no pública | ✓ | ✓ | | | |
| DSGVO: datos personales fuera de logs | ✓ | | | | ✓ |

---

## 8. Mapa de dependencias

| Fase | Requiere | Paralelo con |
|---|---|---|
| F1 | F0 completo | — |
| F2A | F1 (modelos) | F2B · F3 · F6 |
| F2B | F1 (interfaz SlotService) | F2A · F3 · F6 |
| F3 | F1 (interfaz SlotService como stub) | F2A · F2B · F6 |
| F4 | F2B + F3 (eventos de dominio) | — |
| F5 | F2B (modelo Termin completo) | F7 |
| F6 | F0 (infra i18n) | F2A · F2B · F3 · F4 · F5 |
| F7 | F2B (eventos de dominio definidos) | F5 |
| F8 | F2 · F3 · F4 · F5 en staging | F7 (durante S7) |
| F9 | F8 + supuestos validados | — |
| F10 | F9 aprobado | — |

**Clave de desacoplamiento F2B–F3:** la interfaz `SlotAvailabilityServiceInterface` se define en F1 con un stub. F3 programa contra la interfaz. Cuando F2B implementa la clase real, el Service Container la resuelve automáticamente sin cambios en F3. Este es el único punto de coordinación entre tracks paralelos.

---

## 9. Variante: un solo desarrollador

Estimación con agentes IA: **5–6 semanas**.

```
S1:       F0 (infra, CLAUDE.md) + inicio F1
S2:       F1 completo (modelos, auth, seeders, interfaz SlotService)
S3–S4:    F2A + F6 integrado módulo a módulo
S5–S6:    F2B (SlotService real, TerminResource, post-consulta)
S6–S7:    F3 (Portal Livewire, TokenService)
S8:       F4 (Emails, Jobs, Scheduler)
S9:       F5 (Estadísticas, XLSX) + F7 (HubSpot arch.)
S10–S11:  F8 (Testing avanzado, seguridad)
S12:      F9 (UAT con Verena)
S13:      F10 (Lanzamiento) + buffer
```

> Con agentes, el cuello de botella no es escribir código sino revisar que las reglas de negocio están correctamente implementadas. Las fases con más lógica de negocio (F2B, F3) requieren más revisión, aunque el código se genere rápido.

---

*Documento generado a partir de `helpmi-spec.html` y los datos de producción del CSV de marzo 2026.*  
*Última revisión: julio 2026 · v1.2*
