## Supuestos adoptados en F3.5

> Origen: auditoría manual de solo lectura contra `admin.mgm.concloo.net` (producción, Concloo
> CMS / IBM Lotus Domino), realizada el 2026-08-03 con credenciales reales cedidas por el cliente
> (`doris.hagleitner@megemit.org`), navegando con Playwright. Ninguna acción de escritura
> (guardar/borrar/reagendar) se ejecutó — solo peticiones `GET`. Ninguna captura de pantalla ni
> HTML descargado durante la auditoría se conservó: contenían datos personales reales de
> pacientes/terapeutas y, en un caso, credenciales de producción expuestas en texto plano
> (contraseña de SQL Server y token de API de solve360 en `Einstellungen → Allgemein`) — **se
> recomienda al cliente rotar ambas credenciales independientemente de esta auditoría.**

**Estado (2026-08-03):** todos los gaps que no dependían de Verena ya están implementados y
testeados (Pint + PHPStan nivel 6 + Pest, 71/71 tests). El **límite de citas por "casos activos"**
se deja explícitamente sin tocar a petición del cliente — no se modifica la lógica síncrona de
F2B/F3 hasta que Verena lo confirme, aunque queda documentado como riesgo abierto. El resto de
puntos que requieren a Verena (Fallnummer Referenz, semántica exacta de emails/Login-Name,
integración solve360, membresía vía WordPress, reagendar con/sin nuevo Fallnummer) tampoco se han
tocado en código — solo estructura de datos donde era segura de añadir sin lógica de negocio
asociada.

| Decisión | Supuesto adoptado | Alternativa descartada | Quién debe validar |
|---|---|---|---|
| Países soportados | **Implementado.** Ampliado de 3 (`DE`/`AT`/`CH`) a **5**, añadiendo `IT` (Italy) y `SK` (Slovakia) — `beraters.country`/`feiertags.country` convertidos de ENUM a VARCHAR validado en Filament (migración `2026_08_03_190001`). | Mantener solo 3 países asumiendo que Italy/Slovakia son casos residuales o de prueba — descartado porque los terapeutas encontrados tienen datos completos y activos, no parecen de test. | Verena — confirmar si Italy/Slovakia siguen operativos o están en fase de cierre. |
| Catálogo de Hauptthema | **Implementado.** `KategorieSeeder` ampliado de 14 a **19** Hauptthemen reales (añadidos Augenerkrankung, Cardio Vascular, Pädiatrie, Pneumologie como Hauptthema propio, Schilddrüse, Diagnostik Besprechung; eliminado el fake "nicht definiert" de primer nivel), con Unterkategorien verificadas 1:1 contra producción (incluyendo los dos Hauptthemen sin ninguna Unterkategorie). | Mantener 14 asumiendo que los 5 restantes son adiciones recientes sin uso real — descartado: sus timestamps de creación (2022–2025) y estado `Aktiv=Ja` indican uso activo continuado. | — (hecho objetivo, verificado directamente contra producción). |
| Catálogo de Berater | **Implementado.** `BeraterSeeder` ampliado de 3 a **7** asesores reales activos (Dr. Petra Blum, Gunter Schlegel, Eciel Hölzler, Ina Chammah, Dr. Andrea Stecher Schilling, Dr. Renate Schied, Dr. Ursula Bubendorfer). No se replica el "Testberater" inactivo. | Mantener 3 asumiendo que son los únicos relevantes para el cutover — descartado por evidencia directa de 7 con citas activas repartidas en el listado de Slots. | Verena — confirmar si los 7 continúan todos activos en el momento del cutover (F10); `weekly_schedule`/`advance_days` asignados son valores por defecto razonables, no una réplica exacta (el modelo real de disponibilidad es el de Slots discretos, fuera de alcance — ver más abajo). |
| Campo `Freitextfeld` en Unterkategorie | **Implementado.** `kategorien.requiere_freitextfeld` (migración `2026_08_03_190002`), auto-marcado `true` para todo Unterkategorie con patrón "Andere .../Anderes ..." en el seeder. `TerminResource` (admin) y `BuchenWizard` (portal) muestran/exigen `weitere_angaben` con etiqueta dinámica `Weitere Angaben (<Unterkategorie>)` según este flag — verificado visualmente en `/admin/termins/{id}/edit` y en `/de/buchen`. **Bug real encontrado y corregido durante la verificación visual del portal**: el `<select>` de `unterkategorieId` en `buchen-wizard.blade.php` usaba `wire:model` (diferido) en vez de `wire:model.live`, por lo que la etiqueta dinámica no se actualizaba hasta la siguiente acción del usuario — corregido y cubierto con test de regresión (`PortalBookingTest`). | Mantener el campo `weitere_angaben` siempre visible con etiqueta fija — descartado por ser una divergencia de UX observable y fácil de corregir sin ambigüedad de negocio. | Dev — hallazgo técnico verificable, no requiere validación de Verena. |
| Anamnesis clínica ampliada | **Implementado (estructura).** 14 campos nuevos y opcionales en `termins` (migración `2026_08_03_190005`): hauptsymptome, ausloeser, operationen, fruehere_erkrankungen, familiaere_erkrankungen, impfreaktionen, allergien_unvertraeglichkeiten, unfaelle, sozio_emotionaler_zustand, medikamente_text, ernaehrungsrichtung, stuhl_schleimhautverhalten, zahnstatus, therapievorschlag_immunstatus. Añadidos a `TerminResource` (sección colapsable "Erweiterte Anamnese") — verificado visualmente. **No añadidos todavía al wizard del portal (F3) ni al export XLSX (F5)** hasta confirmar con Verena. | No añadirlos, asumiendo que `weitere_angaben` los cubre de forma agregada — descartado: en producción son campos independientes y estructurados. | Verena — confirmar si deben ser obligatorios/opcionales, si van también en el portal público, y si deben incluirse en el export XLSX de F5 (el CSV de referencia usado para F5 no los mostraba explícitamente). |
| Tercer campo de notas internas | **Implementado.** `termins.notas_tratamiento_interno` (migración `2026_08_03_190005`), distinto de `interner_kommentar`, con helper text "Für den Therapeuten nie sichtbar. Getrennt vom internen Kommentar (F3.5)" — verificado visualmente que ambos campos coexisten de forma independiente. | Reutilizar `interner_kommentar` para ambos usos — descartado porque producción los mantiene como campos separados con propósitos distintos. | Verena — confirmar el propósito exacto de cada uno para pulir las etiquetas/ayudas contextuales (la etiqueta actual es una aproximación razonable, no una traducción confirmada por el cliente). |
| **`Kommentar zum Termin` — comentario visible, cuarto campo (2026-08-03, cuarta revisión)** | **Implementado.** Este campo se había documentado conceptualmente desde la primera auditoría (CLAUDE.md, docs/preguntas-Verena.md, listado en la tabla original de columnas de Termine) pero **nunca se implementó realmente** — se confundió por error con `interner_kommentar` al mapear los campos, dejando 3 campos en vez de los 4 reales de producción. Se comprobó el HTML del formulario real (`name="kztermin"` = "Kommentar zum Termin", visible/no interno, distinto de `name="ikztermin"` = "Interner Kommentar zum Termin" que sí correspondía a `interner_kommentar`). Añadido como `termins.kommentar_termin` (migración `2026_08_03_210003`), visible en la sección "Buchung" de `TerminResource` y como columna opcional en el listado — verificado visualmente. | Asumir que "documentado" significaba "implementado" sin re-verificar contra el HTML real del formulario — este error concreto es la causa raíz de que se perdiera durante 3 rondas de revisión previas. | Dev — hallazgo/corrección técnica, no requiere validación de negocio de Verena; el propósito ("comentario visible sobre la cita en sí, no sobre el terapeuta") es razonablemente claro por el nombre. |
| Distinción "Vom Therapeut abgesagt" vs. cancelación general | **No implementado, solo documentado.** El detalle de Termin en producción tiene 3 toggles independientes (`Aktiv`, `Erledigt`, `Vom Therapeut abgesagt`), mientras nuestra implementación usa un único enum `status` (`pendiente`/`completada`/`cancelada`) que no distingue **quién** canceló la cita. No se ha tocado esta lógica por tocar el flujo de cancelación ya testeado en F2B/F3/F4 (email de cancelación, liberación de slot). | Añadir ya un campo `cancelado_por_therapeut` sin confirmar si esta distinción tiene algún efecto real (¿cambia el email? ¿afecta a las estadísticas de F5?) — descartado por riesgo de construir algo sin uso real. | Verena — confirmar si distinguir quién cancela la cita (terapeuta vs. asesor/admin) tiene relevancia de negocio (ej. para F5 o para el email de F4). |
| Feiertag sin campo de país en producción | **Hallazgo, no gap nuestro.** El formulario de edición y el listado de Feiertage/Ferien en producción (12 registros totales) **no tienen ningún campo de país** — es una lista plana sin distinguir DE/AT/CH/IT/SK, a pesar de mezclar festivos claramente nacionales (ej. "Nationalfeiertag Österreich"). Nuestra implementación SÍ modela `feiertags.country` (heredado de `helpmi-spec.html` en F1, ampliado a 5 países en F3.5) — es decir, nuestra arquitectura es más granular que la de producción en este punto concreto, no al revés. | Eliminar el campo `country` de Feiertag para igualar producción — descartado: la funcionalidad real de `SlotAvailabilityService` (excluir festivos del país del asesor) necesita ese campo para funcionar correctamente entre 5 países; producción podría estar aplicando festivos globalmente a todos los asesores sin distinción de país (posible limitación/bug del sistema original, no algo a replicar). | Verena — ¿producción realmente respeta festivos por país al calcular disponibilidad, o los aplica a todos los asesores por igual? Si es lo segundo, nuestra implementación sería una mejora intencional, no una divergencia a corregir. |
| Medikament/Kategorie (Hauptthema/Unterkategorie/Präparate) — quinta revisión, 2026-08-03 | **Sin gaps encontrados.** Se revisaron los formularios de edición completos de las 3 entidades en producción, campo por campo: Präparat (Aktiv, nombre único — sin `serie` visible ni traducción separada), Hauptthema (Aktiv, nombre), Unterkategorie (Aktiv, Hauptthema, nombre, Freitextfeld) — todo ya replicado. **Hallazgo del mismo tipo que Feiertag/país**: ninguna de las 3 tiene un campo de orden ("Sortierung") editable en producción — solo `Hilfsthemen` lo tiene. Nuestra implementación sí tiene `sort_order` en `Kategorie` y `Medikament` (heredado de `helpmi-spec.html`, F1) — de nuevo, nuestra arquitectura contempla algo que producción no expone en su UI simple, no una carencia nuestra. | Eliminar `sort_order` de nuestros modelos para igualar producción — descartado: ya se usa activamente para ordenar el listado de Hauptthemen/Präparate en la UI y en el export XLSX (F5), y no rompe nada mantenerlo aunque producción no lo edite explícitamente. | — (revisión completada sin hallazgos que requieran acción; confirma que 4 rondas de auditoría de entidades van cerrando el listado de gaps reales). |
| **Sección "Termine" dentro de Therapeut — sexta revisión, gap estructural real (2026-08-03)** | **Implementado.** El cliente señaló que `/Therapeut/{Land}/Termin/{code}` (URL nested bajo Therapeut) no tenía réplica en nuestra app. Confirmado: en producción, la ficha de cada Therapeut incluye una sección "Termine" con sus citas como enlaces clicables y un botón "Termin hinzufügen" — mientras que `TherapeutResource::getRelations()` estaba **vacío desde F2A**, sin ningún RelationManager. Este era un gap **estructural/de navegación**, no solo de campos (los campos del propio Termin ya estaban todos cubiertos por `TerminResource`, ver revisión de la cuarta ronda). Se implementó `TerminsRelationManager` (tabla de solo lectura con fecha/asesor/Hauptthema/estado/Fallnummer, botón "Termin hinzufügen" y acción "Ansehen/Bearbeiten" que enlazan a las páginas YA existentes de `TerminResource` — que sí pasan por `TerminBookingService::book()`, ver CLAUDE.md § Reglas absolutas — en vez de duplicar lógica de creación aquí). **2 bugs reales encontrados y corregidos durante la verificación visual**: (1) el título de la sección mostraba "Termins" sin traducir (fallback en inglés del nombre de la relación) — corregido con `getTitle()` explícito; (2) el badge de estado mostraba el valor crudo del enum (`cancelada`) en vez de la etiqueta traducida (`Storniert`/`Cancelada`) — corregido reutilizando `TerminResource::statusOptions()` (que hubo que hacer `public`, antes era `private`). Cubierto con test `Livewire::test(TerminsRelationManager::class, ...)` que confirma que un Therapeut solo ve sus propias citas, no las de otros. | Duplicar el formulario/tabla completos de `TerminResource` dentro del RelationManager — descartado por violar el principio de fuente única de verdad; en su lugar se usa una tabla resumen con enlaces a las páginas ya existentes. | Dev — hallazgo estructural confirmado directamente contra producción, no requiere validación de negocio de Verena. |
| **Campo "Unterlagen" (adjuntar ficheros) en Termin — séptima revisión (2026-08-03)** | **Implementado.** El cliente señaló que la ficha de Termin en producción tiene un campo de subida/gestión de documentos ("Unterlagen": nombre, tamaño, descarga, borrado) sin réplica en `TerminResource` (el admin nunca tuvo UI de documentos — la subida solo existía en el portal público, F3, `BuchenWizard`; el panel admin no exponía nada). Re-verificados los ~30 campos restantes de esa misma ficha contra el HTML real — todos ya cubiertos en rondas anteriores, "Unterlagen" era el único que faltaba. Se instaló el plugin oficial `filament/spatie-laravel-media-library-plugin` (nueva dependencia de Composer, confirmada explícitamente con el cliente antes de añadirla) y se añadió `SpatieMediaLibraryFileUpload` a la sección "Unterlagen" de `TerminResource`, reutilizando la colección `documentos` (disco `local` privado) ya definida en el modelo desde F3. **2 bugs reales encontrados y corregidos durante la verificación en navegador con subida real de archivos**: (1) el generador de URL por defecto del plugin asumía un disco público (`/storage/...`) y producía un enlace de descarga **completamente roto** (403 tanto con sesión como sin ella — no era una fuga de seguridad, pero tampoco funcionaba para nadie) — corregido con `getUploadedFileUsing()` propio que genera una URL firmada (`URL::temporarySignedRoute()`, 30 min) hacia un controlador nuevo (`TerminMediaDownloadController`) protegido por `TerminPolicy::view()` (mismo scope que ya protege el propio Termin: admin ve todo, berater solo lo suyo); (2) al verificar el rechazo sin sesión, se descubrió que **cualquier ruta fuera del enrutado propio de Filament protegida solo por el middleware `auth` desnudo devolvía un 500** en vez de redirigir al login — Laravel intentaba resolver la vista de login de Fortify (`route('login')`), que este proyecto no usa (el login real es el de Filament, Fortify solo se usa para 2FA, ver CLAUDE.md). Este bug ya afectaba a la ruta preexistente `/gestion/ping` (de F1/F2A) y no fue introducido por este cambio — se corrigió globalmente con `$middleware->redirectGuestsTo('/admin/login')` en `bootstrap/app.php`. Cubierto con 6 tests (`TerminMediaDownloadTest`: redirección sin sesión, cabecera `inline` con nombre original, descarga por admin, denegación a un berater ajeno, descarga por el berater propietario, rechazo de firma manipulada/expirada). **Ajuste posterior a petición del cliente**: los documentos deben poder abrirse directamente en el navegador sin descargarlos antes — se cambió `response()->streamDownload()` (que fuerza `Content-Disposition: attachment`) por una `StreamedResponse` propia con `Content-Disposition: inline`, y se añadió `->openable()` junto a `->downloadable()` en el campo del formulario. Se usa `$media->name` (nombre original conservado por Spatie) en vez de `$media->file_name` (nombre generado en disco) para que el navegador muestre/guarde el archivo con su nombre real. | Implementar el upload sin plugin, con un `FileUpload` genérico + hooks manuales sobre el modelo — descartado tras confirmar con el cliente que el plugin oficial (que ya complementa `spatie/laravel-medialibrary`, ya en el stack) era preferible a reinventar la lógica de subida/descarga/borrado. | Dev — hallazgo técnico + 2 bugs de seguridad/fiabilidad reales, no requieren validación de negocio de Verena. Cierra el punto ya señalado en `docs/supuestos-F3.md` ("URL firmada para documentos — no implementado todavía"). |
| Campo `Fallnummer Referenz` | **No implementado, sin cambios.** Se observó en producción como un campo de texto libre distinto de `Fallnummer` (contador secuencial), aparentemente usado para anotar manualmente un caso de referencia/seguimiento. Sin acceso al código fuente original no se puede confirmar si dispara alguna lógica o es puramente informativo. | Implementar una relación formal `termin.referencia_fallnummer_id` sin confirmar la semántica real — descartado por riesgo de construir una funcionalidad incorrecta. | Verena — imprescindible antes de implementar. |
| Listas configurables (Geschlecht/Alter/Betreff) | **Parcialmente implementado.** Se añadió `divers` a `termins.geschlecht` (convertido de ENUM a VARCHAR validado, migración `2026_08_03_190003`) en admin y portal — verificado visualmente. Migrar `geschlecht`/`alter_range`/`betreff` a listas configurables desde `Einstellungen` (como en producción) queda **fuera de alcance**, sin cambios. | Migrar las tres listas a configurables ya en F3.5 — descartado por ampliar el alcance más allá de lo mínimo defendible; `divers` sí se adoptó por ser un gap concreto y de bajo riesgo. | Verena — confirmar si `divers` ya se usa en producción o si conviene revisar el texto legal/DSGVO antes de exponerlo en el portal. |
| Vista de documentos en el panel admin | **No implementado en esta ronda.** Gap real ya señalado en `docs/supuestos-F3.md` (URL firmada pendiente) — producción sí ofrece listado paginado + descarga desde el panel. Queda para F4 (junto con la infraestructura de descargas firmadas). | — | — (decisión técnica, ya aceptada; solo se pospuso la implementación). |
| Acción "Termin verschieben" | **No implementado, sin cambios** — depende de si debe preservar el mismo Fallnummer/FAX-Code o generar uno nuevo. | Añadir ya la acción asumiendo un comportamiento (p. ej. mismo Fallnummer) — descartado por ser una decisión de negocio, no solo de UI. | Verena — confirmar si reagendar debe preservar el mismo Fallnummer/FAX-Code o generar uno nuevo. |
| Campos adicionales de Therapeut | **Implementado (estructura).** 4 direcciones de email (`email`, `email_2`, `email_3`, `email_fachkreis`), campo `anrede` (fórmula de tratamiento) y campo `login_name` añadidos a `therapeuts` (migración `2026_08_03_190004`) y a `TherapeutResource`. **Sin lógica de negocio asociada** (qué email recibe qué notificación, si login_name se usa para algo más allá de identificador) — eso se define en F4. | Mantener solo 1 email — descartado por evidencia directa de que producción usa múltiples direcciones. | Verena — confirmar el uso exacto de cada dirección de email antes de diseñar las plantillas de F4. |
| Campos adicionales de Therapeut (segunda ronda, 2026-08-03) | **Detectado tras revisión del cliente — se me habían quedado sin documentar ni implementar en la primera ronda de F3.5, pese a estar visibles en la captura original del detalle de Therapeut.** Se implementan ahora como estructura simple (sin lógica de negocio): `betreuer_asignado` (**"Betreut durch" / columna "assigned to" de la lista** — persistente por Therapeut, no re-introducido en cada cita; distinto del campo `termins.betreuer_name` por-cita que ya existía desde F1), `fachrichtung` (especialidad, texto libre — production muestra valores como "Généraliste, Homéopathe, Acupuncteur"), `akademischer_grad` (p.ej. "Dr. med."), `spezialisierung` (texto libre, a menudo vacío en producción), `ausbildungsstand` (p.ej. "A2, FS" o "WS/AK,FS", texto libre — no coincide con el enum "Selektion aus el campo 'Nivel formación actual': A1,A2,FS,WS,AK" visto en `Einstellungen → Allgemein`, sugiere que el formato real es una combinación libre de esos códigos). **No implementado**: `new-date` (booleano, propósito no confirmable sin acceso al código — ¿marca terapeutas recién dados de alta?). | Asumir que estos campos son solo visuales/informativos sin implementarlos — descartado porque el cliente los señaló explícitamente como una brecha real tras revisar producción directamente; mejor pecar de completos en la captura de datos aunque la lógica de negocio quede pendiente. | Verena — confirmar el propósito de `new-date`, y si `betreuer_asignado` (persistente por Therapeut) debe sustituir o convivir con `termins.betreuer_name` (por-cita) — son dos modelos de datos distintos y el original (F1, docs/supuestos-F1.md) asumió el segundo sin conocer el primero. |
| Campos de Therapeut ligados al límite de citas ("Aktive Fälle") | **No implementado — depende de la pregunta bloqueante nº1 (ver docs/preguntas-Verena.md).** Producción muestra en el detalle de cada Therapeut: `Aktive Fälle` (contador, probablemente calculado, no almacenado), `max-Termine` (override individual del límite global "Maximale Beratungstermine pro Therapeut"), `max` (booleano, propósito no confirmado — ¿indica si ya alcanzó su máximo?), `block-new` (booleano — ¿bloquea nueva cita manualmente al margen del límite automático?). Implementar cualquiera de estos sin resolver antes el modelo real del límite de citas sería construir sobre un supuesto ya identificado como posiblemente incorrecto. | Añadir los campos ya, sin lógica, "por si acaso" — descartado porque estos campos SOLO tienen sentido en el contexto del modelo de "casos activos", que es precisamente lo que está aparcado a la espera de Verena; añadirlos ahora sin la lógica sería ruido, no señal. | Verena — misma pregunta bloqueante nº1 de `docs/preguntas-Verena.md`. |
| `Hilfsthemen` como catálogo | Confirmado que en producción es un catálogo CRUD (orden + activo/inactivo), no columnas booleanas fijas como en la implementación actual (decisión ya documentada en CLAUDE.md, tomada conscientemente por alcance de F1). Se mantiene la decisión ya tomada — no se revierte en F3.5. | Migrar ya a catálogo CRUD — descartado por alcance; queda anotado como candidato a revisar antes de F9 si el negocio lo pide. | Dev — decisión ya tomada y documentada; solo se re-confirma con evidencia de producción. |
| Modelo de Slots discreto | **Fuera de alcance de F3.5.** Producción gestiona 109 slots individuales por asesor/día/hora con toggle propio, en vez de un `weekly_schedule` computado. Cambiar `SlotAvailabilityService` a este modelo es un cambio de arquitectura mayor que no se aborda aquí. | — | Verena/Dev — decidir si merece la pena migrar antes de F9, dado que el modelo actual (computado) ya pasa todos los tests de disponibilidad y es más simple de mantener. |
| Campos de seguridad/UX en Berater (tercera revisión, 2026-08-03) | **Implementado.** Al revisar el **formulario de edición completo** de Berater en producción (no solo la lista, que ya se había revisado antes) se encontraron 3 campos reales no replicados: `color` (color de calendario/slots, `ColorPicker`), `auto_logout_enabled` + `auto_logout_minutos` (cierre de sesión automático **configurable por asesor**, distinto del `SESSION_LIFETIME` global de Laravel — implementado vía middleware `EnforceAutoLogout`, basado en una marca de tiempo de sesión propia, no en el mecanismo interno de Laravel), y `force_password_change` (vive en `users`, no en `beraters` — persistido mediante `afterSave()`/`afterCreate()` en las páginas de Filament, aplicado vía middleware `EnforcePasswordChange` que redirige al perfil, y se limpia automáticamente al guardar una nueva contraseña desde `App\Filament\Pages\Auth\EditProfile`, que sobrescribe la página de perfil nativa de Filament habilitada para esto). **Bug real encontrado y corregido durante el testing**: `Carbon::diffInMinutes()` en esta versión no siempre devuelve un valor absoluto (puede ser negativo según el orden de las fechas) — sin `abs()`, la comparación `>= minutos` nunca se cumplía y el cierre de sesión automático no habría funcionado nunca en producción. Cubierto con 7 tests dedicados (`F3_5BeraterSecurityFieldsTest`). Kennwort/Kennwort-Bestätigung/2FA/Administrationskennwort vistos en el mismo formulario de producción NO se replican — ya cubiertos por la arquitectura de autenticación real (Fortify + Spatie + google2fa), un mecanismo distinto y más apropiado que el original. | No implementarlos, asumiendo que la primera revisión (solo del listado) había sido suficiente — descartado tras que el cliente señalara explícitamente pedir una segunda revisión más exhaustiva de los formularios de edición completos. | — (hallazgo técnico verificable, no requiere validación de negocio de Verena). |
| Límite real de citas ("Aktive Fälle") | **Explícitamente aparcado a petición del cliente (2026-08-03) — no se toca la lógica síncrona de F2B/F3 en esta ronda de F3.5**, ni siquiera parcialmente. Producción parece limitar por "casos activos" por terapeuta (config. global "Maximale Beratungstermine pro Therapeut" = 3, con override individual "max-Termine" por terapeuta), no por "2/día" (F2B) ni "total no-socio" (F3) como se asumió. Esto podría invalidar el diseño síncrono ya construido y testeado, pero permanece intacto hasta que Verena lo confirme. | Modificar ya F2B/F3 para usar el modelo de casos activos — descartado explícitamente por el cliente hasta tener confirmación de Verena, dado que tocar lógica síncrona ya testeada (mutex/locks) sin certeza de negocio es alto riesgo. | Verena — **crítico, bloqueante antes de cualquier cambio a la lógica de límite de citas. Sigue pendiente de revisión, sin fecha.** |
| Integración solve360 (CRM) | **No implementado, solo documentado.** Producción sincroniza en vivo contra solve360 (usuario + API token + timestamp de último sync). El plan (F7) solo contempla un esqueleto `HubSpotCrmAdapter`. | Empezar ya la migración a HubSpot sin evaluar si solve360 sigue siendo la fuente de verdad — descartado, es una decisión de negocio, no técnica. | Verena — decidir si F7 sigue siendo HubSpot o si hay que mantener/replicar solve360. |
| Verificación de membresía (`Mitglied`) vía WordPress | **No implementado, solo documentado.** Producción tiene una conexión SQL directa a una base de datos WordPress externa (tabla de usuarios) — probable mecanismo real para derivar `Mitglied`, en vez de (o además de) un campo manual en `Therapeut`. | Implementar ya esta integración — descartado: requiere conocer el mecanismo exacto (¿sync periódico? ¿consulta en tiempo real?) y las credenciales expuestas durante esta auditoría deben rotarse antes de replicar nada contra ese servidor. | Verena — imprescindible, y coordinar con quien gestione la infraestructura de WordPress/megemit.org. |
| Reglas de retención/purga de datos | **Fuera de alcance de F3.5**, pertenece a F8 (DSGVO). Producción purga festivos expirados a los 28 días y citas expiradas a los 120 días (con 60 días de vista residual para Berater/Therapeut tras la expiración). | — | Verena/legal — confirmar si estos plazos son requisito DSGVO o solo housekeeping operativo. |
| Restricciones de selección de horarios en `Einstellungen → Allgemein` | **Parcialmente implementado (2026-08-03).** Se detectó en la auditoría inicial pero no se evaluó formalmente hasta una revisión posterior (omisión, no decisión deliberada). De los 5 ajustes reales de producción se implementaron 2: `Einstellung.antelacion_minima_dias` (antelación mínima para reservar, "Minimale Anzahl von Tagen...") y `Einstellung.max_dias_antelacion_global` (tope global que acorta —nunca amplía— el `advance_days` de cada Berater, "Spätester Tag..."), ambos wireados en `SlotAvailabilityService::unavailabilityReason()` y cubiertos con tests (`SlotAvailabilityServiceTest`). **Efecto secundario positivo**: se descubrió que `beraters.advance_days` (campo de F2B) nunca se aplicaba en ningún sitio — ahora sí se respeta. Se ajustó `BeraterFactory` (advance_days por defecto 3650 en tests, no 30) para no acoplar tests preexistentes con fechas lejanas a esta nueva restricción. **No implementado**: tope de slots ofrecidos ("Maximale Anzahl von Terminen die zur Auswahl gestellt werden") — no tiene equivalente en nuestra arquitectura de slot-por-día; offset "Spezi" — depende de una funcionalidad (agenda prioritaria) que no existe en la réplica. | No implementar el tope de slots ni "Spezi" — el primero exigiría rediseñar el modelo de disponibilidad (agregación multi-día), el segundo depende de una funcionalidad no construida; ambos quedan fuera hasta que se decida abordar el modelo de Slots discretos. | Dev — hecho técnico ya cerrado para los 2 puntos implementados. Verena — confirmar si el tope de slots y "Spezi" merecen construirse como parte de un futuro rediseño del modelo de disponibilidad. |

## Importación de datos históricos (adelantada de F10, 2026-08-04)

> El cliente proporcionó (`backup/`) dos exports DXL reales de la base Domino/Notes original:
> `MGMTherapeut(...).dxl` (4.193 documentos, verificado con parser XML real, no con el conteo
> inicial por regex que estaba inflado) y `MGMTermine(...).dxl` (3.787 documentos). También hay
> 3 ficheros ZIP (~10.4 GB) con los adjuntos subidos 2024-2026, sin procesar todavía. El plan
> (`docs/helpmi-planificacion.md` § F10) sitúa la importación de datos de producción en el
> cutover, con un `ProductionSeeder` placeholder sin construir — se adelanta aquí la construcción
> del importador (no su ejecución contra producción, que sigue siendo F10) por ser trabajo
> desbloqueado independiente de las fases F7/F8 en curso.

| Decisión | Supuesto adoptado | Alternativa descartada | Quién debe validar |
|---|---|---|---|
| Formato real de Fallnummer | **Corregido en código (2026-08-04).** Los datos reales exportados muestran un contador secuencial plano (`fallnr`: "10002", "10003"... coincide con el "Nächste Fallnummer" ya visto en `Einstellungen → Allgemein` de producción, valor real 10613), **no** el formato `AAAA-NNNN` (ej. "2026-0001") implementado y testeado en F2B sin confirmación contra datos reales. `TerminBookingService::generateFallnummer()` corregido para usar `einstellungen.siguiente_fallnummer` (nueva columna, arranca en 10614) como contador real, con el mismo mecanismo de lock de la fila singleton que ya existía. Test actualizado (`TerminBookingServiceTest`). | Mantener el formato `AAAA-NNNN` y solo usarlo para datos nuevos, dejando el histórico con formato distinto — descartado: mezclar dos formatos de un mismo identificador es peor que corregir uno, y el histórico importado necesita continuidad real con la numeración de producción. | — (hecho objetivo confirmado con datos reales, no decisión de negocio; ya corregido). |
| Faltan exports de Berater y Slots para importar Termine | **Resuelto (2026-08-04).** El cliente entregó `Berater.dxl` (8 documentos, form `MGMprefsBerater`) y `Slots.dxl` (109 documentos, form `MGMprefsSlots`). Verificado por cruce de UNIDs contra los 3.787 documentos de Termine: los 7 UNID distintos referenciados por el campo `berater` casan 7/7 contra `Berater.dxl` (8 documentos totales — el octavo es "Testberater (nur für Entwicklung)", no referenciado por ninguna cita real, coherente con los "7 Berater reales" ya confirmados). Los 99 UNID distintos referenciados por el campo `termin` (formato `<serial-Lotus>:<UNID-slot>`) casan 99/99 contra `Slots.dxl`. El número serial se decodifica como fecha (epoch Lotus/Domino 1899-12-30) — verificado cruzando con la fecha `created` real de una muestra de documentos (la cita siempre cae unos días después de creado el registro, nunca antes ni con una diferencia absurda), no es una suposición sin contrastar. | Asumir el epoch sin verificarlo contra datos reales — descartado; se contrastó explícitamente antes de construir el importador. | — (bloqueante técnico ya resuelto con los ficheros recibidos). |
| Faltan exports de Berater y Slots para importar Termine — **nuevo bloqueante descubierto al diseñar el importador, RESUELTO (2026-08-04)** | Los campos `hauptthema` y `ukategorie` de Termine **también son referencias UNID Domino** (mismo formato que `berater`/`termin`). Domino **no ofrece exportación** para esa base (confirmado por el gestor de la plataforma vía Slack). Se resolvió leyendo `Einstellungen → Hauptthema` y `→ Unterkategorie` en producción con el mismo acceso de solo lectura ya usado en la auditoría de F3.5 (Playwright, credenciales cedidas por el cliente), extrayendo el UNID de la URL de cada entrada (19 Hauptthemen + 100 Unterkategorien). Guardado en `database/data/kategorie_legacy_unids.json` (no es un export DXL, es una lista id↔nombre construida manualmente — nombres de categoría, no datos clínicos ni de pacientes). Verificado contra Termine.dxl: los 18 UNID de `hauptthema` y los 91 UNID de `ukategorie` referenciados en las 3.787 citas casan 18/18 y 91/91. Nuevo comando `import:kategorie-unids` enlaza `kategorien.legacy_unid` a las Kategorien ya sembradas (por nombre exacto; Unterkategorie además por Hauptthema padre). `import:termine` ya resuelve `kategorie_id`/`unterkategorie_id` (opcional — si no hay coincidencia, se deja `null`, no bloquea la importación de la cita). | Adivinar la correspondencia sin verificarla contra los UNID reales de Termine.dxl — descartado, se cruzó explícitamente antes de dar el bloqueante por resuelto. | — (bloqueante técnico ya resuelto). |
| **Gap de contenido descubierto al enlazar Kategorie: `KategorieSeeder` solo tenía 51/100 Unterkategorien reales de producción — RESUELTO (2026-08-04)** | Al ejecutar `import:kategorie-unids` contra los 100 Unterkategorien reales, solo 48 encontraron coincidencia por nombre en nuestra BD (51 sembradas en total, 3 sin equivalente real). La quinta revisión de F3.5 (Medikament/Kategorie) había concluido "sin gaps", pero solo comparó cualitativamente unos Hauptthemen de muestra, no el listado completo con paginación (production muestra por defecto solo ~25 filas; el total de 100 solo se ve forzando el selector de página a "500" o leyendo "Gesamt: 100") — mismo patrón de error que "revisar la lista pero no el detalle completo" ya visto con Berater/Therapeut en rondas anteriores. **Ampliado `KategorieSeeder`** con las 52 Unterkategorien reales que faltaban, repartidas bajo su Hauptthema correcto, incluyendo el valor real de `requiere_freitextfeld` de producción (2 excepciones que no seguían el patrón "Andere/Anderes...": "reaktivierte Viren" y "Stress", añadidas explícitamente en `FREITEXTFELD_OVERRIDES`). Total ahora: 103 Unterkategorien (100 reales de producción + 3 "nicht definiert" catch-all que ya existían en el seeder desde antes de F3.5 y no tienen equivalente en producción — adición deliberada de la spec original, no un gap). Cubierto por `SeedersTest`. | Confiar en la revisión cualitativa previa sin recontar contra el total exacto ("Gesamt") — es la causa raíz de este gap, igual que en rondas anteriores de F3.5. | — (gap de contenido ya cerrado; pendiente solo re-sembrar la BD de staging/producción cuando corresponda, ya que la BD local de desarrollo tiene Termine que referencian Kategorien existentes y no se puede truncar sin perder datos). |
| Importador de Therapeut | **Implementado y verificado con `--dry-run` contra el DXL real** (`php artisan import:therapeuten {ruta} [--dry-run]`, `App\Console\Commands\ImportTherapeutenFromDxl`). Parser XML en streaming (`XMLReader`, evita cargar los 109 MB completos en memoria). Upsert idempotente por `therapeuts.legacy_unid` (columna nueva, guarda el UNID Domino original — permite reimportar sin duplicar y enlazar `Termine.therapeutunid` en el futuro). Mapeo de 15 campos DXL → columnas ya existentes (vorname, nachname, ANREDE→anrede, LOGIN→login_name, ASSIGNEDTONAME→betreuer_asignado, FACHRICHTUNG, AKAGRAD→akademischer_grad, SPEZIAL→spezialisierung, AUSBILDUNGSSTAND, mail→email, MAIL2/3/4→email_2/3/email_fachkreis, LAND→land, plz, ID→solve360_id, MITGLIED→mitglied booleano). **`LGNCODE` (credencial/token legacy) nunca se lee ni se registra en ningún sitio** — verificado explícitamente en el dry-run (0 lecturas). Dry-run real: 4.193 creados, 0 omitidos, sin exponer ningún dato personal en consola (solo UNIDs opacos y contadores). **Ejecutado contra la BD local de desarrollo (2026-08-04, a petición explícita del usuario)**: 4.193 creados, 0 omitidos, todos con `legacy_unid`. **Bug real encontrado y corregido durante esta ejecución**: `vorname`/`nachname` son columnas `NOT NULL` en `therapeuts`, pero el importador convertía cualquier valor vacío del DXL a `null` de forma uniforme para todos los campos mapeados — la primera ejecución real abortó a mitad (un documento con `nachname="Pankow"` pero sin `vorname`) y, al envolver todo el bucle en una única `DB::transaction()`, la excepción revirtió los ~2.500 registros ya procesados sin dejar ningún rastro. Corregido: `vorname`/`nachname` usan cadena vacía (`''`) en vez de `null` cuando el DXL no trae valor, en vez de heredar la conversión genérica del resto de campos (que si pueden ser `null`). Reejecutado con éxito tras el fix. | Adivinar el destino de los campos sin mapeo confirmado (MEMBERSHIP, CANCELATION, LIMIT, MAX, maxtermine, spezi/spezioffset, ASSIGNEDTO-UNID) inventando columnas nuevas — descartado; el comando solo cuenta su presencia (para dimensionar el problema) sin inventar semántica. `MEMBERSHIP` (datetime) y `CANCELATION` (texto) son campos completamente nuevos, nunca vistos en la UI de producción auditada — posible información interna no expuesta en el panel. | Verena — confirmar el significado de `MEMBERSHIP`/`CANCELATION` antes de decidir si merecen columna propia; el resto de campos sin mapeo (`LIMIT`/`MAX`/`maxtermine`/`spezi`) ya están ligados a la pregunta bloqueante nº1 (límite de citas) y a "Spezi" (fuera de alcance, ver filas anteriores). |
| Importador de Berater (enlace de UNID) | **Implementado** (`php artisan import:berater-unids {ruta} [--dry-run]`, `App\Console\Commands\ImportBeraterUnidsFromDxl`). NO crea Beraters nuevos — solo enlaza `beraters.legacy_unid` (columna nueva) contra los 7 ya sembrados por `BeraterSeeder`, emparejando por nombre exacto (nuestro modelo `Berater` no tiene columna `email`). Verificado contra el DXL real: 3/3 enlazados en la BD local de desarrollo actual (que solo tiene sembrados los primeros 3 de los 7 Beraters reales — estado de la BD local, no un fallo del importador; con los 7 sembrados enlazaría 7/7). | — | — (hecho técnico cerrado). |
| Importador de Termine | **Implementado y EJECUTADO con éxito contra la BD local (2026-08-04, a petición explícita del usuario)**: 3.553 de 3.787 citas creadas. `App\Console\Commands\ImportTermineFromDxl` resuelve `therapeut_id` (`therapeutunid`→`Therapeut.legacy_unid`), `berater_id` (`berater`→`Berater.legacy_unid`), `kategorie_id`/`unterkategorie_id` (`hauptthema`/`ukategorie`→`Kategorie.legacy_unid`) y `starts_at`/`ends_at` (`termin`→fecha decodificada + horario del Slot). Mapea 17 campos de anamnesis/texto directos más `betreff`/`fallnummer`/`gewicht`/`groesse` con tratamiento especial (ver abajo). Otros ~15 campos sin destino 1:1 confirmado (bbemerkung, empfehlung, fallnrref, files, hilfen, ikztermin, iranking, kztermin, more, sstatus, STATUS, TERMINALT, ACTIVITYID, CHANGE, REMINDER) solo se cuentan, no se importan. Omitidos: 11 (therapeutunid sin resolver, documentos huérfanos), 82 (Hauptthema en placeholder "Bitte auswählen", nunca se eligió uno real) y 141 (betreff vacío) — los tres son huecos reales de los datos de producción, no fallos del importador. **4 bugs reales encontrados y corregidos durante la ejecución real** (el `--dry-run` no los detectó porque solo simula el resultado sin ejecutar el INSERT contra MySQL, así que las restricciones de columna nunca se comprobaban de verdad): (1) `vorname`/`nachname`/`fallnummer` `NOT NULL` recibían `null` desde el mapeo genérico de campos vacíos — corregido con manejo explícito por columna; (2) `alter`/`hauptthema` y otros desplegables de producción usan el literal `"Bitte auswählen"` ("por favor, seleccione") en vez de dejar el campo vacío cuando no se elige nada — se normaliza a cadena vacía en todos los campos antes de mapear; (3) `gewicht`/`groesse` llegan con coma decimal alemana (ej. "26,4") pero las columnas son enteras — se parsean y redondean; (4) `ernaehrungsrichtung` era la única de 14 columnas de anamnesis ampliada definida como `string`(255) en vez de `text` como sus 13 hermanas (descuido de la migración `2026_08_03_190005`, no relacionado con el import) — corregido con migración `2026_08_04_130001` (afecta también a citas nuevas del admin/portal, no solo al histórico). El `fallnummer` de las 3.197 citas que no lo traían en el DXL (+ 37 duplicados) se generó con `Einstellung.siguiente_fallnummer`, el mismo contador real que usa `TerminBookingService::generateFallnummer()` para citas nuevas (decisión explícita del usuario, "generar uno nuevo con el contador real") — el contador local terminó en 13725 tras el import. `betreff` (ENUM de 3 valores) se normalizó desde los 3 textos reales de producción ("Fallbesprechung"/"Interpretation Laboruntersuchungen"/"Sonstiges"). Verificado tras la importación: 3.553 `fallnummer` y `fax_code` únicos (sin colisiones). | Adivinar semántica de campos sin mapeo confirmado, o generar valores para `fallnummer`/`betreff` sin verificar antes contra los datos reales — descartado en ambos casos. | — (importador ya ejecutado con éxito contra BD local; pendiente decidir con Verena si se ejecuta también contra staging/producción antes de UAT). |
| Adjuntos (3 ZIP, ~10.4 GB) — **importador construido y verificado con `--dry-run` (2026-08-05)** | Se confirmó primero que el item `files` de cada documento `MGMTermine` contiene lo necesario para reconocer cada adjunto: tuplas `<id-hex-24>##YY##<nombre original>##YY##<mime>##YY##<tamaño>` separadas por `##**##`, donde el id de 24 caracteres es exactamente el nombre de fichero que usa Domino en disco dentro de `Uploads 2024`/`2025`/`2026` (verificado 1:1 por tamaño en bytes contra una muestra real). Construido `php artisan import:termine-uploads {dxl} {dirs*} [--dry-run]` (`App\Console\Commands\ImportTermineUploadsFromDxl`), que resuelve el `Termin` destino por `legacy_unid`, localiza el fichero en las carpetas dadas y lo mueve (no copia — comportamiento por defecto de `addMedia()` de Spatie Media Library al no usar `preservingOriginal()`) a la colección `documentos` ya usada por F3/F3.5, conservando el nombre original vía `usingFileName()`/`usingName()` y marcando cada adjunto con `custom_properties->legacy_file_id` para poder relanzar el comando sin duplicar (idempotente). Se extrajo un trait compartido `Concerns/ParsesDxlDocuments` (usado también por `import:therapeuten` e `import:termine`, que duplicaban el mismo parseo XML). Se amplió `Termin::registerMediaCollections()->acceptsMimeTypes` de 5 a 14 tipos MIME tras comprobar los tipos reales presentes en el DXL (incluye png, tiff, excel, opendocument, pptx, zip, html, texto plano y octet-stream) — cambio aislado de la validación de subidas nuevas en admin/portal, que restringen con su propia lista más corta. **Dry-run real contra los 3.787 documentos y las 3 carpetas ya descomprimidas**: 5.486 ficheros listos para mover, 133 omitidos por `Termin` sin `legacy_unid` resuelto (mismas citas que ya se omitieron al correr `import:termine`), 2.120 sin fichero en ninguna de las 3 carpetas (adjuntos subidos antes de 2024, fuera del rango de años entregado — no es un fallo del importador), 0 rechazados por mime, 0 tuplas malformadas. **Ejecutado contra la BD local de desarrollo (2026-08-05, a petición explícita del usuario): 5.473 adjuntos movidos con éxito** (`storage/app/private`, colección `documentos`, 11 GB), enlazados a 2.188 Termine distintos. **2 bugs reales encontrados y corregidos durante la ejecución real** (el `--dry-run` no los detecta, no llega a invocar `addMedia()` de verdad): (1) `config('media-library.max_file_size')` estaba en el valor por defecto de Spatie (10MB) — 172 ficheros reales superan ese límite (máximo real ~54MB), abortando el comando entero a mitad con una excepción no capturada; corregido subiendo el límite a 100MB (`config/media-library.php`, cambio aislado de los límites de subida de admin/portal, que validan 10MB de forma independiente en sus propios formularios). (2) el mime declarado en el item `files` del DXL no siempre coincide con el mime real que Media Library detecta por contenido del fichero (`FileUnacceptableForCollection`) — un único fichero con este problema abortaba también el resto del import; corregido envolviendo `addMedia()`/`toMediaCollection()` en un `try/catch` por fichero (nunca se registra el nombre original en el log de error, solo `legacy_unid`/`legacy_file_id` y la clase de excepción). Resultado final tras ambos fixes: 5.473 movidos, 133 sin Termin resuelto, 2.121 sin fichero en disco, 12 con error real de aceptación (mime real ≠ aceptado), 0 tuplas malformadas — el comando es idempotente (`custom_properties->legacy_file_id`), por lo que ambas ejecuciones parciales antes de cada fix no duplicaron nada al continuar. | Copiar los ficheros en vez de moverlos, dejando el original en `backup/` — descartado explícitamente por el usuario ("los ficheros también se tendrán que mover a la carpeta que el proyecto usa para uploads"). | — (importador ejecutado con éxito; los 846 ficheros que quedan en `backup/Uploads *` corresponden a adjuntos sin Termin resuelto, sin referencia en el DXL, o con error de mime — revisar si merece la pena investigarlos caso a caso o darlos por perdidos). |
| Productos recomendados (`termin_medikament`) — **bug real encontrado y corregido (2026-08-05)**: la tabla pivote que alimenta los checkboxes de "Präparate" (`TerminResource`) tenía **0 filas** pese a que `import:termine` ya había creado 3.553 Termine. Causa raíz: el campo `empfehlung` de `MGMTermine.dxl` (relleno en 3.765 de 3.787 citas, 100% de sus valores no vacíos son UNID Domino de 32 caracteres hex, uno o varios por cita) estaba en `ImportTermineFromDxl::UNMAPPED_FIELDS` — nunca se intentó resolver, solo se contaba su presencia. Mismo patrón que `hauptthema`/`ukategorie`: Domino no exporta el catálogo de Präparate por DXL, así que el UNID→código solo podía obtenerse leyendo `Einstellungen → Präparate` en producción. **Esta vez la extracción se hizo con Playwright MCP** (instalado en la sesión con `claude mcp add playwright -- npx -y @playwright/mcp@latest`), login con las credenciales ya cedidas por el cliente (`doris.hagleitner@megemit.org`, las mismas de la auditoría original), navegación de solo lectura, y `page.evaluate()` para extraer el UNID del atributo `onclick` de las 61 filas de la tabla (visibles todas en una sola página) en vez de abrir cada ficha una a una — guardado en `database/data/medikament_legacy_unids.json` (nombres de producto, no PII). **Hipótesis verificada al 100% antes de dar el hallazgo por cerrado**: los 45 UNID únicos referenciados en `empfehlung` a lo largo de las 3.787 citas casan 45/45 contra este fichero. Construidos: migración `2026_08_05_140001` (`medikaments.legacy_unid`), comando `import:medikament-unids` (calcado de `import:kategorie-unids`, enlaza por `codigo` exacto — 61/61 en producción) y comando dedicado `import:termine-medikamente` (resuelve `empfehlung`→`termin_medikament` vía `syncWithoutDetaching()`, idempotente por el `unique(['termin_id','medikament_id'])` ya existente). **Ejecutado con éxito contra la BD local (2026-08-05, a petición explícita del usuario)**: 61/61 Medikamente enlazados, 6.778 relaciones `termin_medikament` creadas sobre 2.954 Termine distintos, 144 Termine sin `legacy_unid` resuelto (mismas citas ya omitidas por `import:termine`), 0 UNID de Medikament sin coincidencia. | Pedir de nuevo las credenciales de producción en el chat sin más — se aclaró primero que el login de `doris.hagleitner@megemit.org` fue cedido deliberadamente por el cliente para esta auditoría (a diferencia de la contraseña de SQL Server/token de solve360 hallados expuestos *dentro* de `Einstellungen → Allgemein`, esos sí recomendados a rotar) — no es repetir el incidente de credenciales ya documentado, es el mismo acceso ya autorizado. Se encontró además `accesConcloo.txt` sin trackear en la raíz del repo con las credenciales en texto plano — añadido a `.gitignore` para que no se cuele en un commit. | — (hallazgo cerrado con éxito; queda pendiente decidir si el fichero `accesConcloo.txt` debe eliminarse del disco del desarrollador una vez ya no haga falta, y si merece la pena rotar también el login de concloo por higiene, aunque no haya sido un incidente de exposición). |

Este archivo se revisa con Verena antes del UAT (F9). El punto del **límite de citas por "casos
activos"** es el más urgente: bloquea con confianza cualquier cambio ulterior a F2B/F3 hasta que
se confirme cuál es el modelo real de negocio. La **importación de datos históricos** es el
trabajo activo más reciente — ver también `docs/preguntas-Verena.md` para las preguntas nuevas
que genera (formato de Fallnummer ya resuelto; significado de MEMBERSHIP/CANCELATION pendiente).
