Files
Club-12/Docs/historias-de-usuario.md
T

1068 lines
69 KiB
Markdown

# Club 12 — Historias de Usuario
> Reescritura 2026-09-02: el doc original (reunión Facundo/Franco + decisiones del owner) quedó
> desactualizado en varios puntos estructurales tras sesiones posteriores de desarrollo — más
> notablemente, la aparición de una entidad **Temporada (Season)** por encima de Torneo, que no
> existía cuando se escribió la primera versión. Esta versión fue verificada historia por historia
> contra el código real (backend + frontend), no contra memoria de lo que "se pidió" — cada HU dice
> si sigue vigente, cambió, o quedó obsoleta, citando dónde en el código se verificó. Se mantiene la
> numeración original de HU donde la historia sigue existiendo (el código la referencia por número en
> varios comentarios — renumerar rompería esa trazabilidad); las historias nuevas se agregan al final
> con números nuevos.
>
> **Actualización 2026-09-03**: se agregaron HU-118/HU-119 (umbral de 4 jugadores habilitados por
> equipo para cargar un resultado normal / para iniciar el torneo) y HU-86b (top 10 en el ranking
> público de goleadores), se corrigieron HU-62/HU-73/HU-78/HU-82/HU-109 con lo que cambió en esa
> sesión, y se marcaron dos ítems más de la Épica 24 como resueltos (`MatchId` required, roster
> mostrando "No habilitado" para todos). Mismo criterio que la reescritura de abajo: verificado
> contra código real, no contra memoria de lo pedido.
>
> **Actualización 2026-09-05**: auditoría del checkbox "Fase de grupos" del asistente de torneo, a
> pedido del owner. Se agregaron HU-120 a HU-125 (fin de la Épica 22): hoy "Fase de grupos" es un
> booleano de UN solo grupo por división (no permite dividirla en sub-grupos Zona A/B/C con reparto
> balanceado y umbral mínimo de equipos, como sí necesita un organizador real), y existe un segundo
> mecanismo legado (`CreateAutomatedStagesAsync`) contradictorio con el modelo actual del wizard. Ver
> el bloque "Auditoría 2026-09-05" antes de la Épica 23 para el detalle verificado contra código.
>
> **Actualización 2026-09-06**: HU-120 a HU-124 quedaron implementados (roster por división
> `DivisionTeamRegistration`, sub-grupos con cantidad elegida por el organizador y reparto balanceado,
> reasignación manual, edición posterior de la cantidad de sub-grupos, y el mecanismo legado
> `CreateAutomatedStagesAsync` borrado por completo) — sus textos se reescribieron para describir el
> comportamiento final entregado, no el plan. HU-125 quedó explícitamente fuera de alcance, ahora
> rechazada en el backend en vez de dejarse como un cálculo silenciosamente incorrecto. Se agregó
> HU-128 (sorteo de llave para divisiones sin fase de grupos, con vista previa, sorteo manual, traba
> de re-sorteo y auditoría).
>
> Convenciones: `[BUG]` corrige algo existente · `[NUEVA]` propuesta alineada · `[F2]` Fase 2.
> Prioridad (MoSCoW): **M** Must · **S** Should · **C** Could.
> `✅` = verificada vigente tal cual · `🔄` = vigente pero cambió su implementación (ver nota) ·
> `❌` = obsoleta/reemplazada (ver nota) · `⚠️` = vigente pero con una brecha real detectada en el código.
---
## Decisiones transversales (leer antes que nada)
Aplican a TODAS las historias. Si una historia las contradice, ganan estas.
- **D1 — Solo 2 tipos de usuario y 2 cuentas.** Sigue vigente. `Owner` y `Admin IT` son los únicos
roles (`Club12-WebClient/src/modules/core/enum/user/userRolesType.ts`); una migración
(`20260828021949_RemoveTournamentAndTeamManagerRoles`) eliminó los roles viejos de la base.
- **D2 — Equipos y jugadores están scopeados a la temporada/torneo.** Sigue vigente como principio,
pero el nivel de scope ya no es solo "torneo": ver **D7** más abajo — ahora hay una capa
`Season` (Temporada) por encima que agrupa torneos, sin cambiar cómo se scopean equipos/jugadores
(siguen colgando del Torneo vía `TeamTournamentRegistration`/`PlayerTeamRegistration`).
- **D3 — Contenido institucional bloqueado para el owner.** Sigue vigente (`/quienes-somos`,
`/reglamento`, ficha médica no tienen ruta de edición en el panel).
- **D4 — Reglamento agnóstico a la app y a la temporada.** Sigue vigente.
- **D5 — Una acción = una transacción.** Sigue vigente (el asistente de torneo, HU-38, sigue
persistiendo todo en una transacción).
- **D6 — ~~Magic link / self-service de password / auto-inscripción son Fase 2~~ — OBSOLETA.**
Magic link de invitación (HU-09) y self-service de reset de contraseña (HU-10) están **totalmente
implementados y en producción**, no diferidos: `Club12-Backend/API/Controllers/AuthController.cs`
expone `POST /api/auth/invite`, `/activate`, `/password-reset/request` y `/password-reset/confirm`;
el frontend tiene una pantalla completa "Olvidé mi contraseña"
(`Club12-WebClient/src/views/auth/forgotPassword.tsx`). Ver HU-09/HU-10 actualizadas.
- **D7 — [NUEVO] Temporada (Season) agrupa Torneos; no reemplaza el scoping por torneo.** Una
`Season` (`Club12-Backend/Domain/Entities/Models/Season.cs`) agrupa varios torneos del mismo
período competitivo (habitualmente uno masculino + uno femenino). Es el punto de entrada del panel
admin (reemplaza a "Torneos" como ítem top-level del sidebar — ver HU-26 actualizada). El FK
`Season → Tournament` es `SetNull`, no `Cascade`: borrar/limpiar una temporada nunca borra sus
torneos ni su historial. Ver HU-113 (nueva).
### Reglas de negocio resueltas (antes eran incongruencias)
- **R1 — Sanción en fechas, cleanup por días.** Sigue vigente tal cual (HU-75).
- **R2 — Todo scopeado a la temporada.** Sigue vigente (HU-98), ahora con la capa Season encima (D7).
- **R3 — Fixture se genera al iniciar el torneo, tras asignar equipos.** Sigue vigente, confirmado en
código: `TournamentService.cs` genera el fixture únicamente dentro de la transición a `Ongoing`
("En curso"). Ver HU-64/HU-108.
- **R4 — Básquet sin empates.** Sigue vigente en fase de grupos (HU-70). La prórroga
(`WentToOvertime`) es un campo puramente informativo que garantiza que no haya empate; el avance
automático de ganador entre rondas de bracket (HU-82) ya está implementado — ver Épica 24.
---
## Épica 1 — Autenticación y acceso
### HU-01 · Login oculto por URL — `M` ✅
Sigue vigente tal cual. Sin link visible en la nav pública; solo accesible tipeando `/login`.
### HU-02 · Ocultar header y footer en el login — `S` ✅
Sigue vigente. `/login` se renderiza fuera de `PublicLayout`.
### HU-03 · [BUG] Redirigir al home al cerrar sesión — `M` ✅
Sigue vigente y arreglado (`SidebarLayout.tsx`'s `handleLogOut` navega a home tras `logOut()`).
### HU-04 · [BUG] Página 404 sin layout — `S` ✅
Sigue vigente. El catch-all `*` renderiza `<NotFound/>` sin header/footer, con botón al inicio.
---
## Épica 2 — Roles y usuarios
### HU-05 · Simplificar el modelo de roles a Owner y Admin IT — `M` ✅
Sigue vigente (ver D1).
### HU-06 · Editar mi perfil y contraseña — `M` ✅
Sigue vigente. `/panel/configuracion/editar-perfil` y `/panel/configuracion/cambiar-password`.
### HU-07 · Listado de usuarios con soft delete — `S` ✅
Sigue vigente: `isActive` (activar/desactivar) es un toggle distinto de eliminar.
### HU-08 · Blanquear contraseña / editar usuario a pedido — `S` ✅
Sigue vigente.
### HU-09 · Alta de usuario por invitación con magic link — `M` 🔄
**Ya NO es Fase 2 (ver D6) — está en producción.**
**Como** owner/admin **quiero** crear un usuario cargando solo su email y que reciba un magic link
**para** que active su cuenta desde su correo.
- `POST /api/auth/invite` (Admin/Owner) crea el usuario sin contraseña y dispara el mail de
activación; `POST /api/auth/activate` consume el token y define la primera contraseña.
- Ruta admin: `/panel/usuarios/invitar`.
### HU-10 · Reset de contraseña self-service por magic link — `M` 🔄
**Ya NO es Fase 2 (ver D6) — está en producción.**
**Como** usuario **quiero** pedir un magic link para restablecer mi contraseña **para** recuperarme
sin soporte.
- `POST /api/auth/password-reset/request` + `/confirm`; pantalla "¿Olvidaste tu contraseña?" enlazada
desde el login.
---
## Épica 3 — Portada pública, noticias y vista pública del torneo
### HU-11 · Landing pública con accesos directos — `M` 🔄
**Cambió el contenido de la portada, no el principio.**
**Como** visitante anónimo **quiero** una portada que me lleve rápido a lo que me interesa sin
loguearme **para** no perder tiempo navegando.
- Hero con dos CTA: **"Ver temporadas"** y **"Campeones"** (ya no "torneos destacados" ni accesos
directos a sanciones).
- Sección **"Temporadas"** (no "torneos") con las temporadas recientes — consistente con D7: la
portada ahora es season-first.
- Sección de campeones recientes y feed de últimas noticias (ver HU-12).
- No requiere autenticación.
### HU-12 · Feed de noticias con "ver todas" — `M` ✅
Sigue vigente: últimas noticias en home + "ver todas" a `/blog` paginado.
### HU-13 · Detalle de noticia con URL por slug — `M` ✅
Sigue vigente (`/blog/:idOrSlug`).
### HU-14 · Vista pública del torneo (fixture, resultados y posiciones) — `S` 🔄
**Sigue existiendo, pero ya no es el punto de entrada principal.**
**Como** visitante **quiero** ver el fixture, resultados y posiciones de un torneo sin loguearme
**para** seguir la competencia.
- Camino principal: `/temporadas` → temporada → tarjeta de torneo → `/torneos/:tournamentId`.
- La ruta plana `/torneos` también sigue existiendo.
- Solo lectura.
### HU-15 · [BUG] Ocultar el ID en la URL cuando debería ir un slug — `S` ✅
Sigue vigente: torneo, equipo, partido, división, sanción y blog navegan por slug donde existe.
### HU-16 · Estado borrador / publicada de noticia — `S` 🔄
**Ya no es "[NUEVA]" — está implementado.** Chip Publicada/Borrador en el listado admin, switch en
edición; solo las publicadas aparecen en público.
### HU-17 · Compartir noticia con Open Graph — `C` 🔄
**Ya no es "[NUEVA]" — está implementado**, y se extendió más allá de noticias: `pageMetadata.ts`
genera Open Graph/Twitter Card para blog, equipo, torneo y temporada, no solo noticias.
---
## Épica 4 — Contenido institucional
### HU-18 · Ver "Quiénes somos" — `M` ✅
### HU-19 · Ver reglamento — `M` ✅
Ambas siguen vigentes tal cual, sin ruta de edición en el panel (D3).
### HU-20 · Descargar plantilla de ficha médica — `M` 🔄
Sigue vigente, pero la descarga cambió de mecanismo: hoy es un asset estático público
(`/documents/ficha-medica-club12.pdf`, link `download` directo), no un endpoint del backend.
### HU-21 · [BUG] Arreglar la descarga de ficha médica en producción — `M` ⚠️
**No verificable desde el código si el bug original sigue existiendo** — pero el cambio de mecanismo
(HU-20: ahora es un asset estático servido por el frontend, no un endpoint backend) cambia por
completo el modo de falla a auditar. Si vuelve a fallar en producción, el sospechoso ya no es un
problema de ruta/permiso del backend, sino si el PDF quedó efectivamente incluido en el build
estático desplegado.
---
## Épica 5 — Sanciones públicas
### HU-22 · Consulta pública de sanciones — `M` ✅
### HU-23 · [BUG] Buscar sanciones por nombre de jugador — `M` ✅
Sigue vigente y verificado con tests (`PlayerSanctionSearchTests.cs`): el buscador matchea contra el
nombre del jugador sancionado, no solo contra el motivo.
### HU-24 · [BUG] Búsqueda por coincidencia parcial (contains) — `M` ⚠️
**`contains` case-insensitive: sí. Insensible a acentos: NO, y es una decisión deliberada, no un bug
pendiente.** El código documenta por qué: requeriría una extensión de Postgres (`unaccent`/`citext`)
no habilitada en el proyecto, y el proveedor de test en SQLite no la traduciría. Este criterio de
aceptación debería sacarse del doc en vez de quedar como pendiente.
### HU-25 · Filtrar sanciones por torneo — `S` ✅
Sigue vigente, combinable con la búsqueda por nombre.
---
## Épica 6 — Panel de administración: navegación
### HU-26 · Simplificar la navegación lateral — `M` 🔄
**El sidebar cambió más de lo que esta historia anticipaba (ver D7/HU-113).**
**Como** owner/admin **quiero** un sidebar agrupado y sin entidades sueltas **para** navegar por
flujo, no por tabla.
- Divisiones, Fases y Partidos siguen sin ítem propio (se gestionan desde el torneo/división).
- **"Torneos" tampoco tiene ítem propio.** El ítem top-level del grupo "Competición" es
**"Temporadas"**; un torneo se alcanza siempre entrando por su temporada
(`Club12-WebClient/src/views/core/components/SidebarLayout.tsx`, con un test de regresión
(`SidebarLayout.test.tsx`) que confirma que "Torneos" no está en el DOM).
- Se agregó **"Inscripción de equipos"** (`/panel/registro-equipos`) como ítem propio del grupo de
gestión de equipos, y **"Equipos y planteles"** (Épica 10).
### HU-27 · El asistente de torneo no vive en el sidebar — `S` 🔄
Sigue vigente en el principio, pero el botón "Nuevo torneo" que lo dispara ya no vive en una página
global de Torneos (que ya no existe en el sidebar) — vive en el detalle de una Temporada
(`AdminSeasonDetailPage.tsx`), y pre-scopea el asistente a esa temporada (`seasonId` en el estado de
navegación).
### HU-28 · Ajustes de estilo del panel — `C`
No re-verificado en esta pasada (ítem cosmético); sin evidencia de que se haya revertido.
---
## Épica 7 — Gestión de torneos
### HU-29 · Listado y CRUD de torneos — `M` ❌ OBSOLETA (reemplazada por el flujo de HU-26/HU-27)
La tabla plana de torneos (`TournamentsPage.tsx`, ruta `/panel/torneos`) sigue existiendo como
componente/ruta, pero **no está enlazada desde ningún lado** del panel — quedó huérfana. El flujo
real hoy es: Temporada → tarjetas de torneo agrupadas por categoría (dentro de
`AdminSeasonDetailPage.tsx`). Candidata a eliminarse del código o a documentarse como vista de
respaldo, no como el flujo principal.
### HU-30 · Vista de torneo con sub-tabs — `M` 🔄
**Son 4 tabs, no 2.** `TournamentPage.tsx`: `Detalle | Divisiones | Equipos | Asignación`. El tab
**Asignación** es nuevo (ver HU-108) y no tenía HU propia en la versión anterior de este documento.
### HU-31 · Bloquear alta/edición de estructura si el torneo ya empezó — `M` ✅
Sigue vigente, con mensaje explícito en el backend
(`ErrorMessages.StructuralEditNotAllowed`): los cambios estructurales solo se permiten en
`OpenForRegistration`.
### HU-32 · [BUG] Contador de equipos por división en 0 — `M`
No re-verificado en esta pasada.
### HU-33 · [BUG] Botón deshabilitado con "no hay equipos" cuando sí hay — `M`
No re-verificado en esta pasada.
### HU-34 · Quitar mínimo/máximo de equipos del torneo — `S` ✅
Sigue vigente: `minTeams`/`maxTeams` no existen en `Tournament`, el wizard ni la request de
creación (migración `20260828010238_DropTournamentTeamCountLimits`).
---
## Épica 8 — Máquina de estados del torneo
### HU-35 · Transiciones de estado — `M` 🔄
**Ya no es estrictamente forward-only.**
Ciclo: `Programado → Inscripción abierta → Inscripción cerrada → En curso → Finalizado`, más
`Cancelado` desde cualquier estado no terminal — **y una excepción agregada**: `En curso →
Inscripción cerrada` ("revertir a borrador"), solo si el torneo no tiene partidos jugados. Ver
HU-114 (nueva).
- ⚠️ **Brecha detectada**: el mapa de transiciones del frontend
(`Club12-WebClient/src/modules/tournament/utils/tournamentStatusTransitions.ts`) todavía lista
solo `Finished`/`Canceled` como próximos estados válidos desde `Ongoing` — no incluye la reversión.
El botón dedicado "Revertir a borrador" en `TournamentPage.tsx` funciona igual porque no usa ese
mapa, pero cualquier OTRO lugar de la UI que sí lo use (ej. un selector genérico de próximo estado)
no va a ofrecer esta opción. Corregir el mapa para que quede consistente con el backend.
### HU-36 · "Programado" distinto de "Inscripción abierta" — `S` ✅
### HU-37 · "Inscripción cerrada" habilita la asignación — `M` ✅
Ambas siguen vigentes tal cual (y la nota "supersedida por HU-108" de la versión anterior del doc
sigue siendo correcta: el fixture se genera al pasar a "En curso", no al cerrar inscripción).
---
## Épica 9 — Asistente de creación de torneo
### HU-38 · Crear el torneo completo en una sola transacción — `M` ✅
Sigue vigente: el wizard envía la estructura completa en un solo `createFullTournament`.
### HU-39 · Datos base del torneo con fechas — `M` 🔄
**Se agregó un campo obligatorio que no estaba en la versión original: la Temporada.**
El primer paso del wizard ahora también exige elegir `seasonId` ("El torneo debe pertenecer a una
temporada.") — consistente con D7/HU-113. Nombre, descripción y fechas siguen igual.
### HU-40 · Validar que el límite de inscripción sea anterior al inicio — `S` ✅
### HU-41 · Selección de equipos — `M` ❌ OBSOLETA, tal como ya anotaba la versión anterior
Confirmado: el wizard no tiene paso de selección de equipos (`STEP_LABELS` no incluye "Equipos").
La inscripción vive en HU-107, la asignación a zonas en HU-108.
### HU-42 · Un equipo asignado a una zona no aparece en otra — `M` ❌ trasladada a HU-108
Sin cambios respecto a la nota anterior: se valida en la asignación post-cierre, no en el wizard.
### HU-43 · Configurar la fase de grupo (cantidad de enfrentamientos) — `M` ✅
### HU-44 · Configurar playoffs con varias copas — `M` ✅
Ambas siguen vigentes.
### HU-45 · Mapear posiciones de cada división a cada playoff por rangos — `M` 🔄
**Ya no lo edita el admin a mano en el wizard — se deriva automáticamente.**
El editor manual de rangos ("1-4 → Oro, 5-8 → Plata") ya no existe en el wizard: los rangos se
**derivan** de la cantidad de clasificados (`qualifiers`) que cada copa tiene configurada, en el
orden en que las copas fueron agregadas (`deriveCupMappings`, ver HU-112 nueva). La validación de
rangos sin solapes sigue existiendo, pero solo como guarda defensiva del lado del servidor
(`PlayoffMappingValidator.cs`).
### HU-46 · Configurar la serie final al mejor de N — `M` ⚠️
**El campo de configuración sigue existiendo y funciona en el wizard/UI — pero ver la brecha grande
en HU-82: un "mejor de 3" configurado hoy NO genera 3 partidos reales de forma automática en
producción.**
### HU-47 · Copa / división cruzada con playoff obligatorio — `M` ⚠️
**Brecha de validación client-side detectada.** El backend sigue modelando la copa cruzada con playoff
obligatorio, pero la validación del PASO del wizard (`validateCrossCupStep`) no chequea
`cups.length > 0` — el estado inicial arranca con `cups: []`, así que, tal como está escrito hoy, el
formulario permitiría avanzar con una copa cruzada sin ningún playoff configurado, contradiciendo la
regla. No confirmado si el backend rechaza esto de forma independiente al recibir la request — a
revisar antes de confiar en que está cubierto.
### HU-48 · Femenino como torneo separado, por diseño — `M` ✅
### HU-49 · Paso de revisión antes de crear — `S` ✅
Sigue vigente tal cual la nota anterior: el resumen muestra estructura (divisiones, formato, copas,
series), no equipos por zona (porque ya no se seleccionan en el wizard).
---
## Épica 10 — Equipos y planteles
### HU-50 · Sección de Equipos en el panel — `M` ✅
### HU-51 · Tab de jugadores dentro del equipo — `M` 🔄
Sigue vigente, pero el equipo tiene más tabs que antes: Detalle, **Jugadores**, Puntuaciones,
Sanciones y **Cuerpo técnico** (nuevo, ver HU-116).
### HU-52 · Alta de jugadores al plantel — `S` 🔄
**El mecanismo cambió de formulario modal a fila editable en la tabla.**
**Como** owner/admin **quiero** cargar jugadores directamente en la tabla del plantel **para** dar de
alta varios sin abrir un popup por cada uno.
- "Nuevo Jugador" agrega una fila en modo edición directamente en el `DataGrid` del plantel — no una
ventana modal separada.
- La fila nueva se completa celda por celda (Nombre/Segundo nombre/Apellido/Documento/Fecha de
nacimiento/Equipo/Teléfono/Obra social); Guardar valida y crea; Descartar la elimina sin llamar a la
API.
- Los jugadores YA existentes en la tabla siguen sin ser editables ahí mismo — la edición de un
jugador existente vive en su página de detalle, no inline (decisión explícita, con test de
regresión: "no ofrece una acción Editar en la fila — la edición vive en el detalle").
### HU-53 · Copiar plantel de una temporada anterior — `C` 🔄 REUBICADA, no eliminada
**El botón "Importar plantel de una temporada anterior" del tab Jugadores del equipo YA NO EXISTE
— fue reemplazado por importación CSV (HU-117, nueva).** Pero la funcionalidad de copiar un plantel
entero de una temporada previa **sigue existiendo**, reubicada dentro del flujo de **inscripción de
un equipo existente a un nuevo torneo** (`EnrollTeamDialog.tsx`, checkbox "Copiar plantel de su
temporada anterior") — es decir, sigue siendo parte de HU-107, no una acción separada del tab de
plantel.
### HU-54 · Límites de plantel y dorsales únicos — `C` ✅
Sigue vigente tal cual: tope configurable de jugadores por plantel, dorsal único por equipo+torneo,
un jugador no puede estar en dos equipos del mismo torneo (`PlayerService.RegisterPlayerToTeamAsync`).
---
## Épica 11 — Ficha médica y elegibilidad
(Flujo E2E sin cambios respecto a la versión anterior: descarga de plantilla → owner sube y aprueba
la ficha por jugador+equipo+torneo → habilitación → elegibilidad en la carga del partido.)
### HU-55 · Cargar ficha médica a un jugador para ese torneo y equipo — `M` ✅
### HU-56 · Bucket de almacenamiento de archivos — `M` 🔄
**Ya no es "hoy no está habilitado" — está implementado.** Bucket privado `medical-records` dedicado
(`SupabaseMedicalRecordStorage.cs`), con `StoreAsync`/`DownloadAsync` y tests propios. El código está
listo; el único trabajo restante por ambiente es de aprovisionamiento/infra, no de desarrollo.
### HU-57 · Habilitación del jugador según ficha aprobada — `M` 🔄
Sigue vigente, con una regla adicional: la habilitación también exige que exista un **archivo
realmente almacenado**, no alcanza con el estado `Approved` sin referencia de archivo.
### HU-58 · Aprobar / rechazar ficha médica — `S` ✅
### HU-59 · Ficha médica nueva por temporada — `S` ✅
### HU-60 · Solo son elegibles los jugadores habilitados — `M` ✅
### HU-61 · Un jugador sancionado no es convocable — `M` ✅
### HU-62 · Aviso de jugador no habilitado al armar la fecha — `C` 🔄
**El aviso ahora lista a TODOS los jugadores no elegibles de una planilla, no solo el primero
encontrado — y por nombre real, nunca por UUID crudo.** Corregido: antes,
`PlayerStatisticService` tiraba una excepción apenas encontraba el PRIMER jugador sancionado o no
habilitado, con el ID del jugador crudo en el mensaje (`El jugador {guid} no está habilitado...`) —
esto obligaba al admin a corregir la planilla de a un jugador por vez, y el mensaje era ilegible.
Ahora (`PlayerStatisticService.FindRosterEligibilityIssuesAsync`) junta TODAS las violaciones de
ambos equipos antes de tirar un único error agrupado por equipo, con el nombre real de cada
jugador (`ErrorMessages.MatchSheet.PlayersNotEligible`).
Las seis siguen vigentes en el fondo; ver también HU-73 y HU-109 (nuevas reglas relacionadas
agregadas en esta pasada).
### HU-118 · [NUEVO] Umbral mínimo de habilitados por equipo (walkover automático) — `M`
**Como** owner/admin **quiero** que el sistema bloquee la carga de un resultado normal si un equipo
no tiene suficientes jugadores habilitados **para** forzar que ese partido se cargue como walkover
en vez de como un resultado jugado.
- Un equipo necesita **al menos 4 jugadores habilitados** de su plantel de esa temporada para que se
le pueda cargar un resultado normal (`PlayerStatisticService.EnsureTeamMeetsHabilitadoMinimumAsync`,
usa `TournamentCompletabilityValidator.MinPlayersPerTeam`, que bajó de 5 a 4 y ahora cuenta
jugadores **habilitados**, no simplemente registrados en el plantel).
- Se chequea ANTES de mirar qué jugadores puntuaron en la planilla — un equipo por debajo del
umbral falla esta regla aunque la planilla venga vacía.
- Si un equipo no llega al mínimo, `PUT /api/matches/{id}/result-from-sheets` y
`PUT /api/matches/{id}/result-from-team-sheet` rechazan con 409 y un mensaje que indica cuántos
habilitados tiene y que debe cargarse como walkover (`ErrorMessages.MatchSheet.TeamRequiresWalkOver`);
el walkover en sí sigue siendo una acción manual del admin ("Marcar W.O.", HU-73), no automática —
el sistema bloquea el camino incorrecto, no ejecuta el correcto por su cuenta.
### HU-119 · [NUEVO] Torneo no puede iniciar si algún equipo no llega al mínimo de habilitados — `M`
**Como** owner/admin **quiero** que el sistema me impida iniciar un torneo si algún equipo inscripto
no tiene suficientes jugadores habilitados **para** no arrancar una temporada que va a terminar en
walkovers por incumplimiento de plantel.
- Extiende la guarda de completitud existente (HU-109): la regla `TeamTooFewPlayers` de
`TournamentCompletabilityValidator` ahora cuenta jugadores **habilitados** (ficha médica
Aprobada + archivo real almacenado) en vez de simplemente registrados, y el mínimo bajó de 5 a 4
— alineado con el umbral de walkover de HU-118.
- Un equipo con 5 jugadores registrados pero ninguno habilitado ya NO pasa esta guarda (antes sí,
porque solo contaba registros); el torneo se queda bloqueado en `RegistrationClosed` hasta que se
corrija.
---
## Épica 12 — Fixture y jornadas
### HU-63 · Partidos agrupados por jornada — `M` ✅
### HU-64 · Generar el fixture automático al cerrar inscripción — `M` ❌ el disparador que describe está mal
**El disparador real es "En curso" (iniciar el torneo), no "Inscripción cerrada".** Esta era ya una
inconsistencia interna del doc anterior (contradecía a HU-37/HU-108, que sí tenían el disparador
correcto). El código lo confirma sin ambigüedad: `TournamentService.cs` solo llama
`GenerateFixtureAsync` dentro de la rama `newStatus == TournamentStatus.Ongoing`. El resto de la
historia (aleatorio, respeta enfrentamientos configurados, idempotente por división) sigue vigente.
### HU-65 · Cantidad de fechas y calendario del fixture — `M` ✅
### HU-66 · Asignar cancha al partido más tarde — `M` ✅
### HU-67 · Orden de jornadas fijo — `M` ✅
### HU-68 · Reprogramar / suspender un partido — `C` 🔄
Sigue vigente, con una precisión de ubicación: la acción vive en la **lista de fixture** (fila del
partido), no en la página de detalle del partido — no es un cambio funcional, solo dónde está el
botón.
---
## Épica 13 — Carga del partido: resultado y goleadores
### HU-69 · Cargar el resultado del partido — `M` 🔄
Sigue vigente, con un campo nuevo: checkbox **"Se jugó tiempo extra"** (`Match.WentToOvertime`) — es
puramente informativo, no cambia el cálculo del marcador ni la regla de sin-empates (HU-70). **Además**,
la carga ahora está bloqueada si algún equipo no llega al mínimo de jugadores habilitados — ver
HU-118 (nueva).
### HU-70 · Sin empates: todo partido cargado tiene ganador — `M` ✅
### HU-71 · Cargar goleadores del partido — `M` ✅
### HU-72 · Persistir anotadores y conectarlos al ranking — `M` ✅
### HU-73 · Walkover / ausencia (W.O.) — `S` 🔄
Sigue vigente tal cual como acción MANUAL del admin ("Marcar W.O.", elige el equipo presente,
`MatchService.LoadWalkOverAsync`). **Precisión agregada en esta pasada**: el sistema no dispara un
walkover solo — lo que hace es bloquear la carga de un resultado NORMAL cuando un equipo no llega al
mínimo de 4 habilitados (HU-118), dejando el walkover manual como el único camino disponible para
cerrar ese partido. No hay detección automática de "equipo ausente" más allá de esa guarda.
---
## Épica 14 — Gestión de sanciones (administración)
### HU-74 · Crear una sanción desde el partido — `M` ✅
### HU-75 · Duración de la sanción en fechas, cleanup por días — `M` ✅
### HU-76 · Vencimiento automático de sanciones — `S` ✅
### HU-77 · Sanción también a equipo o staff — `C` ✅
Sigue vigente: `SanctionSubjectType` (Jugador/Equipo/Staff) existe en el modelo y la consulta pública
distingue el tipo de sujeto.
**Sin HU en la versión anterior: workflow de apelación** — ver HU-115 (nueva).
---
## Épica 15 — Clasificación y playoffs
### HU-78 · Tabla de posiciones por zona/división — `M` 🔄
Sigue vigente, con un agregado sin HU previa: en una división **sin fase de playoff**, el 1er puesto
de la tabla pública se corona (ícono + fila destacada) **en vivo**, apenas es el líder actual — no
espera a que termine la temporada (`crownFirstPlace` en `PublicDivisionPanel.tsx`/
`divisionStandings.tsx`). Corregido en esta sesión tras una corrección del owner: la primera versión
solo coronaba una vez decidido el podio; el criterio correcto es que el color siga a la posición
actual, igual que el resto del tinte por rango de clasificación.
### HU-79 · Sistema de puntaje configurable — `S` ✅
### HU-80 · Desempate de la tabla (fase de grupos) — `M` ✅
Las dos siguen vigentes tal cual.
### HU-81 · Definir los clasificados a playoffs desde la tabla — `M` 🔄
Sigue vigente en el concepto, con dos cambios: los rangos ya no son un editor manual (ver HU-45), y
**el poblado de las copas ya no requiere un paso manual separado** — ver HU-82.
### HU-82 · Bracket de playoffs con siembra, prórroga y BYE — `M` ✅
**Como** usuario **quiero** ver el cuadro de playoffs con los cruces sembrados **para** seguir la
definición.
- ✅ **Auto-siembra**: al completarse la fase de grupos de una división, las copas de playoff se
siembran **automáticamente** (`StageService.TryAutoSeedPlayoffPhaseAsync`), sin acción manual del
admin. No pisa una siembra ya hecha a mano.
- ✅ **BYE**: sigue funcionando para clasificados que no son potencia de 2.
- ✅ **Sin empates** en fase de grupos; prórroga (`WentToOvertime`) es el mecanismo, ver HU-69.
- ✅ **"Serie al mejor de N" (HU-46) se genera de verdad en producción (corregido 2026-09-02).**
`StageService.SeedPlayoffCupsAsync`/`SeedKnockoutStageAsync`/`SeedMultiGroupCrossCupStageAsync`
crean un `MatchSeries` real (vía `MatchSeriesService`) para cada cruce cuando `Stage.BestOf > 1` —
ya no siembran un `Match` plano. `SeriesInProgressPanel` (tab Playoff de la división, admin)
agrega la acción de UI que faltaba para cargar el 2º/3er partido de una serie
(`addGameToSeries`).
- ✅ **El ganador de cada serie avanza automáticamente (corregido 2026-09-02).**
`StageService.TryAdvanceStageWinnerAsync` toma el ganador de cada slot recién decidido (serie o
partido único) y lo carga en el slot correspondiente de la ronda siguiente del mismo bracket —
llamado desde las 3 acciones de carga de resultado de `MatchController`, y también desde el propio
seeding para propagar los BYE automáticamente.
- ✅ **Partido por el tercer puesto, opcional por copa.** `StageType.ThirdPlace` es un tipo de etapa
propio; cuando la copa lo incluye, los dos perdedores de semifinal avanzan automáticamente a ese
partido (`StageService.AdvanceLosersToThirdPlaceAsync`). No tenía HU propia en ninguna versión
anterior de este documento.
---
## Épica 16 — Estadísticas y rankings
### HU-83 · Estadísticas globales del sistema — `M` ✅
### HU-84 · Estadísticas por torneo — `M` 🔄
**No existe un tab "Estadísticas" dentro del torneo como tal.** `TournamentPage.tsx` tiene
`Detalle | Divisiones | Equipos | Asignación`, sin tab de estadísticas. Las estadísticas scopeadas a
un torneo viven en los tabs de cada división (Posiciones/Goleadores/Partidos/Playoff) y en la página
global `StatisticsPage.tsx`, filtrable por torneo/temporada.
### HU-85 · Estadísticas por temporada y de todos los tiempos — `S` ✅
Sigue vigente tal cual (`ScorerRepository.GetPlayerScoresAsync`).
### HU-86 · Ranking de goleadores — `S` 🔄
**Más simple de lo que describía la versión anterior — corregir las expectativas, no solo la
ubicación.**
- ✅ Ahora tiene un lugar fijo en el panel: tab **"Goleadores"** dentro de la división admin
(`DivisionScorersTable.tsx`), con Imprimir + Exportar CSV, igual que Posiciones.
- ✅ Se alimenta de la misma carga de HU-71/72, sin escritura manual aparte.
- ✅ Muestra jugador, dorsal (de la inscripción del torneo, no del puntero denormalizado), puntos y
equipo.
- ❌ **NO** muestra partidos jugados ni promedio — esos campos no existen en `ScorerByPlayerResponse`.
- ❌ **NO** hay ninguna lógica de desempate — el orden es `OrderByDescending(Points)` liso, sin
segundo criterio. Si dos jugadores empatan en puntos, el orden entre ellos es indefinido.
### HU-87 · Ficha de estadística individual del jugador — `C` ✅
### HU-88 · Historial de un jugador entre temporadas — `C` ✅
Ambas siguen vigentes tal cual.
### HU-86b · [NUEVO] Top 10 en el ranking de goleadores público — `S`
**Como** visitante **quiero** ver un ranking de goleadores corto en la página pública del torneo
**para** no tener que scrollear una lista larga en una vista que es solo un vistazo rápido.
- La tabla de goleadores de la página pública de una división limita a los primeros 10
(`PUBLIC_TOP_SCORERS_LIMIT` en `PublicDivisionPanel.tsx`, prop `limit` de `DivisionScorersTable`).
- El panel admin (HU-86) NO tiene este límite — sigue mostrando el ranking completo, paginado.
### HU-89 · Exportar posiciones / estadísticas — `C` 🔄
**Solo CSV — no hay exportación a PDF.** El botón "Imprimir" abre el diálogo de impresión nativo del
navegador sobre una hoja con estilos de impresión (el usuario puede elegir "Guardar como PDF" desde
ahí), pero no hay generación de PDF del lado de la app (`jsPDF`/`pdfmake`/servidor) en ningún punto
del código. Corregir "CSV/PDF" a "CSV + impresión nativa del navegador".
---
## Épica 17 — Canchas
### HU-90 · CRUD de canchas — `M` 🔄
Sigue vigente, con más de lo que describía: además de nombre/dirección/imagen, cada cancha tiene
**latitud/longitud** con un **mapa interactivo (Leaflet/OpenStreetMap)** — geocodificado
automáticamente desde la dirección tipeada, con un pin arrastrable a mano para corregir. También
tiene `Slug` (link público estable).
---
## Épica 18 — Respaldos y administración de datos
### HU-91 · Generar respaldo manual — `M` ✅
### HU-92 · Bloquear la app durante respaldo/restauración — `M` ✅
### HU-93 · Respaldo de seguridad automático antes de restaurar — `M` 🔄
Sigue vigente en el mecanismo (se genera una salvaguarda antes de restaurar), con una corrección: la
salvaguarda **NO se elimina** al terminar con éxito — queda en el catálogo de respaldos, sujeta solo
a la poda normal por retención en el próximo backup.
### HU-94 · Límite de copias de seguridad — `S` 🔄
El tope por defecto es **7**, no 5 (`BackupOptions.RetentionCount = 7`).
### HU-95 · Respaldos programados — `S` 🔄
El intervalo por defecto es **diario (24 horas)**, no semanal (`BackupOptions.IntervalHours = 24`).
Es configurable, pero "semanal" no es lo que trae por defecto.
### HU-96 · Borrado total de datos — `S` ✅
Sigue vigente, restringido a `AdminOrOwner` con confirmación fuerte.
### HU-97 · Carga de datos de prueba (solo Admin IT) — `S` ❌ ELIMINADA (deliberado, no un bug)
El botón "Cargar Datos de prueba" fue sacado del panel de Administración de datos. Esta historia debe
retirarse del documento, no marcarse como pendiente.
---
## Épica 19 — Modelo season-scoped (fundacional)
### HU-98 · Equipos y jugadores scopeados por temporada/torneo — `M` 🔄
El principio sigue vigente tal cual (`TeamTournamentRegistration`/`PlayerTeamRegistration` como
fuente de verdad, sin reasignar `Team.TournamentId`), pero la historia conflaba "temporada" y
"torneo" como lo mismo. Ya no lo son — ver D7/HU-113: `Season` es una entidad propia por encima de
`Tournament`. El scoping de equipos/jugadores sigue siendo a nivel Torneo, sin cambios.
### HU-99 · Identidad de club estable entre temporadas — `C` 🔄
**Ya no es "[NUEVA]"/especulativa — está implementada.** Entidad `Club` real
(`Club12-Backend/Domain/Entities/Models/Club.cs`) con `Team.ClubId` opcional y su propio
`ClubService`. Reclasificar de Could/no-empezada a Must/hecha.
---
## Épica 20 — Coherencia técnica transversal
### HU-100 · Zona horaria Argentina en fechas y horarios — `S` ✅
Sigue vigente: almacenamiento en UTC, presentación en `America/Argentina/Buenos_Aires`.
### HU-101 · Auditoría de acciones sensibles — `C` ⚠️ brecha real detectada
**Como** admin IT **quiero** un registro de quién borró datos, restauró respaldos o cambió estados
**para** trazabilidad.
- ✅ Los 4 tipos de acción de la historia original están cubiertos por el enum (`AuditAction`:
`DataWipe`, `BackupRestore`, `TournamentStatusChange`, `PasswordReset`) — no hay auditoría de
"toda mutación administrativa", solo estas 4 (consistente con `Docs/ESTADO-Y-REGLAS.md`, que ya
listaba "auditoría completa" como pendiente).
- ✅ Nombres legibles: se resuelve el nombre real del objetivo (torneo, usuario por email) en vez de
mostrar un UUID crudo, y el detalle queda en español (`ToSpanishLabel()`).
- ✅ **`AuditAction.BackupRestore` se loguea (corregido 2026-09-02).** `BackupOperationsService`
ahora recibe `IAuditService` y llama `LogAsync` al final de `RestoreBackupAsync`.
---
## Épica 21 — Rediseño visual
### HU-102 · Design system: tema oscuro con acentos naranja — `M` ✅
### HU-103 · Aplicar el tema al sitio público — `S` ✅
### HU-104 · Aplicar el tema al panel de administración — `S` ✅
### HU-105 · Consistencia responsive y accesibilidad — `C` 🔄
Las cuatro siguen vigentes en el principio; se confirmó además una implementación de sidebar
responsive específica (AppBar colapsable + drawer temporal en mobile, comentarios de código citan
"HU-105" explícitamente) que la versión anterior del doc no detallaba.
---
## Épica 22 — Rediseño del flujo de inscripción y armado del torneo
### HU-106 · El asistente crea torneo + estructura, sin equipos ni fixture — `M` ✅
### HU-107 · Inscripción de equipos con el torneo abierto — `M` 🔄
Sigue vigente tal cual, con una precisión: ahora tiene una **ruta/ítem de sidebar propios**
(`/panel/registro-equipos`, "Inscripción de equipos"), separados tanto del wizard como de la
asignación a zonas — es un paso de su propio ciclo de vida, no una sub-pantalla de otra cosa. La
opción "copiar plantel de temporada anterior" (HU-53) vive acá.
### HU-108 · Asignar inscriptos a divisiones y generar el fixture al iniciar — `M` 🔄
Sigue vigente tal cual, con una precisión de UI: el paso de asignación tiene su propio tab
**"Asignación"** en `TournamentPage.tsx` (ver HU-30).
### HU-109 · Guardas de completitud — `M` 🔄
Sigue vigente: `TournamentCompletabilityValidator` implementa las 6 reglas (zona con muy pocos
equipos, equipo sin asignar, equipo en más de una zona, rango de playoff más allá de los equipos
asignados, grupo de copa cruzada con muy pocos equipos, y equipo con muy pocos jugadores), con su
propia suite de tests. **La última regla cambió en esta pasada — ver HU-119 (nueva)**: ya no cuenta
jugadores simplemente registrados sino habilitados, y el mínimo bajó de 5 a 4.
### HU-110 · Copa cruzada con múltiples grupos de tamaño variable — `M` 🔄
**El "gap actual" que anotaba la versión anterior ya está cerrado.**
`Division.QualifiersPerGroup` es un campo de primera clase; el diseño elegido es una única División
`IsCrossDivisionCup=true` que contiene VARIOS `Stage` de tipo Grupo (no N divisiones cruzadas como
especulaba el doc viejo). El ejemplo real también cambió: ya no es "37 equipos → 10 grupos" — el
seed actual usa una Copa Cruzada masculina de 6 zonas de fase de grupos (5 de 4 equipos + 1 de 3,
ida y vuelta) alimentando un bracket combinado de 12 con byes.
### HU-111 · Calendario: jornadas de zona y copa no se solapan — `M` ✅
Sigue vigente: zonas juegan domingos, copa cruzada otro día fijo, mismo helper compartido
(`RoundCalendar`) para producción y seed.
### HU-112 · [NUEVO] Auto-derivación de las rondas de una copa según su cantidad de clasificados — `M`
**Como** owner/admin **quiero** que la cantidad de rondas de una copa (Final / Semis+Final /
Cuartos+Semis+Final...) se calcule sola a partir de cuántos equipos clasifican **para** no tener que
configurar cada ronda a mano.
- Al definir `qualifiers` (cantidad de clasificados) para una copa, el wizard deriva automáticamente
su estructura de rondas — referenciado en el propio código como "HU-112"
(`submitWizard.ts`/`deriveCupMappings`). Reemplaza el editor manual de rangos que describía la
HU-45 original.
---
## Auditoría 2026-09-05 — "Fase de grupos" no permite sub-dividir una división en zonas
Pedido del owner: revisar si el checkbox "Fase de grupos" del asistente arma correctamente una
1ª fase de grupos (con 1, 2 o 3 vueltas) + una 2ª fase de playoff opcional, y si permite repartir
una división grande en sub-grupos (Zona A, B, C...) con reparto balanceado y umbral mínimo de
equipos por zona. Verificado contra código real (`Club12-WebClient/src/views/tournament/wizard/`,
`Club12-Backend/Application/Services/StageService.cs`, `TournamentDivisionAssignment.tsx`).
**Hallazgo central: colisión de vocabulario.** En este código, lo que el wizard llama "zona"
(`ZoneConfig`) es sinónimo de "división" — cada `ZoneConfig` se convierte en UNA división completa
(ej. "Primera División"), no en un sub-grupo dentro de ella. El switch "Fase de grupos"
(`ZoneEditor.hasGroupStage`) es un booleano de UN SOLO grupo: prende/apaga un único `Stage` tipo
`Group` para toda la división, con 1 a 3 vueltas (`roundRobinLegs`) — eso YA funciona bien y coincide
con el pedido de "1era fase, puede jugarse 1/2/3 veces". Lo que NO existe hoy, en ningún lado, es
dividir esa fase de grupos en sub-grupos paralelos (Zona A / Zona B / Zona C) dentro de una misma
división — que es exactamente lo que describe el pedido para divisiones grandes.
`StageService.CreateStageAsync` lo bloquea explícitamente: un segundo `Stage` tipo `Group` en la
misma división tira `ErrorMessages.Stage.GroupStageAlreadyExistsInDivision`, salvo que la división
sea `IsCrossDivisionCup` (la Copa Cruzada, HU-110) — la única competencia que hoy puede tener varios
grupos ("Grupo 1"..."Grupo N") bajo una misma división. Es decir: la capacidad de fondo YA EXISTE en
el modelo de datos (un `Stage` es simplemente un grupo con nombre y equipos asignados), pero está
reservada por regla de negocio a un caso especial (copa cruzada entre divisiones) en vez de estar
disponible para el uso normal que describe el pedido (sub-dividir UNA división grande).
### HU-120 · [NUEVO] Dividir una división en sub-grupos (Zona A/B/C) dentro de su fase de grupos — `M`
**Como** owner/admin **quiero** que, al activar "Fase de grupos" para una división, pueda elegir en
cuántos sub-grupos se reparten sus equipos **para** organizar divisiones grandes en pools manejables
antes de un cruce de playoffs, en vez de un único todos-contra-todos gigante.
- La solución más consistente con el código existente es generalizar la capacidad que hoy solo tiene
la Copa Cruzada (`Division.IsCrossDivisionCup`, varios `Stage` tipo `Group` bajo una división) a
CUALQUIER división — hoy el chequeo de "un solo Group stage" en `StageService.CreateStageAsync`
solo se salta para `IsCrossDivisionCup`.
- La pantalla de asignación de equipos (`TournamentDivisionAssignment.tsx`) YA itera sobre una lista
de `groups` (plural) por división y arma una tarjeta de asignación por CADA `Group` stage
encontrado — el hueco real está en el wizard (`ZoneEditor` no permite crear más de un `Group` stage
por división) y en el backend (`CreateStageAsync` lo prohíbe fuera de la copa cruzada), no en la
pantalla de asignación, que ya está preparada para esto.
- Conviene nombrar el nuevo nivel explícitamente "sub-grupo" o "pool" en el modelo (no reusar "zona",
que ya significa "división" en este código) para no perpetuar la ambigüedad que originó esta
auditoría.
### HU-121 · [IMPLEMENTADO] La cantidad de sub-grupos la define el organizador; el tamaño se reparte balanceado — `M`
**Como** owner/admin **quiero** elegir CUÁNTOS sub-grupos quiero (no un tamaño fijo por grupo) y que
el sistema reparta los equipos lo más parejo posible **para** no terminar con grupos desbalanceados
(ej. "2 grupos de 5 y uno de 6" cuando "4 grupos de 4" sería mejor) ni tener que armarlo a mano.
- El wizard (`ZoneEditor`) pide la cantidad de sub-grupos (`ZoneConfig.subGroupCount`, default `1` =
el comportamiento de siempre, una sola fase de grupos) en vez de un tamaño fijo — a diferencia del
mecanismo legado `CreateAutomatedStagesAsync` (ver HU-124), que forzaba siempre grupos de
exactamente 4 equipos.
- Reparto balanceado real: con `G` sub-grupos y `T` equipos inscriptos en el roster de la división
(`DivisionTeamRegistration`), cada sub-grupo recibe `floor(T/G)` o `ceil(T/G)` equipos — nunca una
diferencia de 2 o más entre el más chico y el más grande. 16 equipos en 3 sub-grupos da 5+5+6.
- Umbral mínimo de 4 equipos por sub-grupo, aplicado en el backend: pedir una cantidad de sub-grupos
que dejaría alguno por debajo de ese piso se rechaza con un mensaje que nombra la cantidad de
equipos y de sub-grupos pedidos (`ErrorMessages.Stage.SubGroupTooFewTeams`), sin crear ni cambiar
ninguna estructura.
- Validado en dos momentos, como estaba previsto: (a) en el wizard, advertencia no bloqueante (todavía
no hay inscriptos reales, solo una estimación); (b) de forma bloqueante en
`TournamentCompletabilityValidator` (extensión de HU-109) al cerrar inscripción/iniciar el torneo,
contra la cantidad real de inscriptos — cubre tanto "muy pocos equipos" como un reparto desbalanceado
a mano (diferencia ≥ 2 entre sub-grupos).
- La cantidad elegida al armar la estructura es un punto de partida, no una decisión final: HU-122
(reparto automático + movimiento manual) y HU-123 (cambiar la cantidad después) cubren la edición
posterior, siempre antes de que el torneo arranque.
### HU-122 · [IMPLEMENTADO] Asignar equipos a sub-grupos: automático por defecto, manual siempre disponible — `S`
**Como** owner/admin **quiero** que el sistema reparta los equipos inscriptos entre los sub-grupos
automáticamente, pero pudiendo mover equipos a mano **para** ajustar por criterios que el sistema no
conoce (cercanía geográfica, nivel competitivo, evitar que dos clásicos rivales queden en la misma
zona, etc.).
- "Auto-repartir" (`TournamentDivisionAssignment.tsx` → `AutoDistributeRosterAsync`) vacía los
sub-grupos actuales y vuelve a correr el reparto balanceado sobre TODO el roster de la división —
nunca solo rellena los huecos vacíos, así el resultado queda siempre balanceado aunque el estado
previo no lo estuviera.
- El movimiento manual de un equipo entre dos sub-grupos de la misma división está siempre disponible
(acción "Mover a otro sub-grupo" por equipo, `ReassignTeamToSubGroupAsync`), sin re-disparar el
reparto del resto de los equipos. La única restricción real es no dejar el sub-grupo de origen por
debajo del piso de 4 equipos; por lo demás el movimiento no tiene ninguna restricción extra
inventada del lado del cliente — incluso un movimiento que desbalancea a propósito los sub-grupos se
permite, quedando a criterio del organizador.
- Un equipo puede estar inscripto en el roster de la división sin todavía estar ubicado en ningún
sub-grupo — es un estado válido, no un error, hasta que se corra el auto-reparto o se lo ubique a
mano.
### HU-123 · [IMPLEMENTADO] Editar la cantidad de sub-grupos de una división antes de que arranque el torneo — `S`
**Como** owner/admin **quiero** poder cambiar la cantidad de sub-grupos de una división después de
creada la estructura pero antes de iniciar el torneo **para** ajustar el armado a la cantidad real de
equipos que se terminaron inscribiendo, que casi nunca coincide con la estimación inicial del wizard.
- "Editar cantidad de sub-grupos" (`RebuildSubGroupsAsync`) reconstruye SOLO la capa de
`Stage`/`StageTeamMatch` de sub-grupos — los borra y crea de nuevo con la nueva cantidad — y vuelve a
repartir balanceado. El roster (`DivisionTeamRegistration`) nunca se toca: los mismos equipos
inscriptos siguen inscriptos, ninguno queda sin registro aunque cambie de sub-grupo.
- Disponible mientras la división sea editable, es decir, mientras el torneo no esté
`Ongoing`/`Finished`/`Canceled` — la misma guarda `EnsureDivisionStructureEditableAsync` que ya usan
la creación/edición de stages y la asignación de equipos.
### HU-124 · [RESUELTO] Dos mecanismos distintos y contradictorios para armar la fase de grupos — `M`
**Como** desarrollador **quiero** una sola fuente de verdad para "cómo se arma la estructura de
grupos de una división" **para** no tener dos caminos que puedan dejar el torneo en un estado
inconsistente según cuál se use.
- Resuelto por eliminación completa, no por retiro parcial: `StageService.CreateAutomatedStagesAsync`,
su endpoint (`POST /api/stages/generate/{id}`, `StageController.GenerateStagesAndMatches`) y todos
sus llamadores — backend y frontend (`generateStages`/`generateStagesAutomatically`), más la
constante `TournamentBracketSize` — fueron borrados por completo. No tenía ningún llamador real en
la UI, así que no hay reemplazo que mantener compatible. La única fuente de verdad para armar la
fase de grupos de una división es ahora el mecanismo de sub-grupos de HU-120/121/122/123.
### HU-125 · [FUERA DE ALCANCE] El reparto en sub-grupos rompe el cálculo actual de posiciones→copa (HU-112) — `M`
**Como** owner/admin **quiero** que, con sub-grupos, las copas de playoff clasifiquen por posición
DENTRO de cada sub-grupo (ej. "1° y 2° de cada zona") en vez de por una tabla combinada **para** que
el cruce a playoffs tenga sentido cuando hay más de una zona.
- Confirmado explícitamente fuera de alcance de esta implementación — queda para un cambio posterior,
con su propio diseño de qué tabla combinada tiene sentido entre sub-grupos.
- En vez de dejar que un cálculo combinado sin sentido se compute en silencio, el sistema RECHAZA la
combinación directamente, en cualquier orden de configuración: activar 2 o más sub-grupos en una
división que ya tiene una copa por rango de posiciones (`cupPositionRange`), o configurar una copa
por rango de posiciones en una división que ya tiene 2 o más sub-grupos, devuelven el mismo error
explicando la incompatibilidad y ofreciendo las dos salidas ("usá un solo sub-grupo o quitá el mapeo
de playoff", `ErrorMessages.Stage.SubGroupsIncompatibleWithPositionRangeCups`). Con un solo sub-grupo
(el comportamiento de siempre) las copas por rango de posiciones siguen funcionando exactamente
igual que hoy.
## Auditoría 2026-09-05 (continuación) — flujos completos con datos reales, pensando como organizador
Segunda ronda de E2E pedida por el owner: en vez de solo cargar pantallas, completar flujos reales de
punta a punta (crear → editar → eliminar, con guardado real) en Canchas, Jugadores, Novedades,
Sanciones+apelación y Usuarios. Se encontraron y arreglaron 5 bugs reales (ver commits del
2026-09-05: fix del crash al ver un jugador recién creado, fechas de nacimiento con un día de error en
toda la lista de Jugadores, falta el botón Editar en Novedades, aceptar una apelación no tenía efecto
real, y error crudo en inglés al crear un usuario con espacios en el nombre) — quedaron registrados en
la Épica 24 como resueltos. Además, pensando específicamente como organizador de torneo (no solo
QA de pantallas), se verificaron dos puntos más contra el código real:
### HU-126 · [NUEVO] La fecha límite de inscripción es puramente informativa — no bloquea ni se puede corregir — `M`
**Como** owner/admin **quiero** que la fecha límite de inscripción sea real (bloquee inscripciones
pasada esa fecha) o, si no lo es, poder corregirla libremente **para** no quedar con un dato
inconsistente que no puedo arreglar.
- Verificado en `TeamService.cs` (líneas 360/430/501): inscribir un equipo está controlado
ÚNICAMENTE por `Tournament.Status == OpenForRegistration` — la fecha `TeamRegistrationDeadline`
nunca se compara contra la fecha actual en ningún lado del backend. Es decir, un equipo puede
inscribirse aunque la "fecha límite" mostrada ya haya pasado, mientras el admin no haya cerrado la
inscripción a mano; y a la inversa, cerrar la inscripción antes de esa fecha corta todo aunque el
plazo mostrado diga que falta tiempo. El campo es puramente decorativo.
- El problema real: `TournamentEditPage.tsx` SÍ trata esa fecha como si fuera autoritativa —
`canEditRegistrationDeadline = !registrationClosed && status === OpenForRegistration`, donde
`registrationClosed` se calcula comparando la fecha contra "ahora". Una vez que la fecha pasa, el
campo se bloquea para editar, sin importar que el torneo siga técnicamente abierto a inscripciones
y que ese valor nunca haya bloqueado nada. Un organizador que decide extender la inscripción una
semana más (algo muy común en la práctica) queda con una fecha límite vieja y visiblemente
incorrecta en pantalla, sin ninguna forma de corregirla.
- Arreglo sugerido: o (a) hacer que la fecha realmente bloquee la inscripción de equipos en el
backend (coherente con lo que el campo dice ser), o (b) si se mantiene como solo informativa,
dejarla editable mientras el torneo siga en `OpenForRegistration`, sin importar si la fecha ya pasó
— la opción (a) es la más honesta con el organizador, que espera que "fecha límite" signifique algo.
### HU-127 · [NUEVO] Reprogramar un partido suspendido no valida choques de cancha ni de equipo — `M`
**Como** owner/admin **quiero** que reprogramar un partido suspendido a una nueva fecha valide que no
choque con otro partido en la misma cancha, o con otro partido del mismo equipo (incluida su copa
cruzada si juega una) **para** no terminar armando sin querer dos partidos imposibles de jugar a la
vez.
- Verificado en `MatchService.SuspendMatchAsync`: cuando se pasa `newMatchDate`, el método hace
`match.MatchDate = newMatchDate.Value` y guarda, sin ningún chequeo de conflicto — a diferencia de
la generación automática del fixture original, que sí respeta separación de horarios por cancha
(regla de las 2 horas mencionada en sesiones previas). Reprogramar a mano puede crear silenciosamente
dos partidos en la misma cancha a la misma hora, o el mismo equipo jugando dos partidos el mismo día
(su zona regular y, si corresponde, la copa cruzada — HU-110, que exime al equipo de la regla de
"una sola zona" pero no dice nada sobre choques de calendario entre ambas competencias).
- No es necesariamente un bloqueo duro: alcanza con una advertencia clara antes de confirmar
("Esta cancha ya tiene un partido a esa hora" / "Este equipo ya juega otro partido ese día") que el
admin pueda decidir ignorar conscientemente — pero hoy no hay ningún aviso, ni siquiera informativo.
### HU-128 · [IMPLEMENTADO] Sorteo de llave para divisiones sin fase de grupos — `M`
**Como** owner/admin **quiero** sortear (o armar a mano) el emparejamiento inicial de una llave de
playoffs cuando la división no tiene fase de grupos previa **para** poder organizar un torneo formato
solo-eliminación sin depender de una tabla de posiciones que no existe.
- La inscripción funciona igual que en cualquier división, vía el roster (`DivisionTeamRegistration`)
de HU-120 — no hace falta ningún `Stage` de grupos para inscribir equipos ni para que la pantalla de
asignación muestre un panel donde hacerlo.
- Sorteo aleatorio con vista previa: "Sortear llave" le pide al servidor una vista previa que no
persiste nada, y el organizador puede volver a sortear las veces que quiera antes de confirmar. Al
confirmar, la llave queda IDÉNTICA a la última vista previa mostrada — un token firmado del lado del
servidor (no un sorteo del lado del cliente) garantiza que lo confirmado sea exactamente lo
previsualizado.
- Sorteo manual: el organizador ordena los equipos a mano, sin ningún sorteo aleatorio, y confirma esa
lista exacta.
- Los byes de una cantidad de equipos que no es potencia de 2 se resuelven con el mecanismo existente
(`PlayoffSeeder.SeedPairs` + `TryAdvanceStageWinnerAsync`) — nada nuevo ahí, reusado tal cual.
- Traba de re-sorteo por llave (`Stage` + nombre de bracket, así que llaves paralelas como "Copa de
Oro"/"Copa de Plata" se traban cada una por separado, independientemente del estado del torneo): se
puede volver a sortear libremente mientras NINGÚN partido real se haya jugado; apenas se juega el
primero, un nuevo sorteo de esa llave se rechaza. Un bye nunca cuenta como "jugado" a este efecto —
una llave recién sorteada con byes sigue siendo re-sorteable.
- Cada sorteo, inicial o re-sorteo, queda auditado (`AuditAction.PlayoffDraw`, visible en el panel de
auditoría admin) con el modo (aleatorio/manual) y la cantidad de equipos — una falla al auditar
nunca bloquea el sorteo en sí.
- La vista pública de la llave muestra "Sorteo realizado el [fecha]" a partir de `Stage.DrawnAt`, sin
necesitar estar logueado ni acceder a la auditoría (que es solo para Admin/Owner); no muestra nada
mientras la división no fue sorteada.
- Explícitamente no incluye HU-125 (clasificación por sub-grupo hacia una copa) — una división con
sub-grupos y una copa por rango de posiciones sigue rechazada, como describe HU-125.
---
## Épica 23 — Temporada, sanciones ampliadas y funcionalidades nuevas sin historia previa
Historias nuevas para funcionalidad que existe en el código pero no tenía HU en ninguna versión
anterior de este documento.
### HU-113 · [NUEVO] Entidad Temporada (Season) agrupando torneos — `M`
**Como** owner/admin **quiero** agrupar los torneos de un mismo período (habitualmente uno masculino
+ uno femenino) bajo una Temporada **para** organizar el club año a año en vez de torneo por torneo
suelto.
- CRUD de Temporada (nombre, año) en el panel; es el punto de entrada del sidebar admin (ver HU-26).
- El detalle de una temporada lista sus torneos agrupados por categoría, con "Nuevo Torneo"
pre-scopeado a esa temporada.
- Vista pública equivalente (`/temporadas`, `/temporadas/:id`).
- `Season → Tournament` es `SetNull`, no `Cascade`: borrar/limpiar una temporada nunca destruye el
historial de sus torneos.
- No cambia el scoping de equipos/jugadores (sigue siendo a nivel Torneo, D2/HU-98).
### HU-114 · [NUEVO] Revertir un torneo "En curso" a borrador — `M`
**Como** owner/admin **quiero** poder deshacer el inicio de un torneo si todavía no se jugó nada
**para** corregir una asignación de zonas equivocada sin tener que recrear el torneo entero.
- Transición `Ongoing → RegistrationClosed`, disponible solo si **cero partidos fueron jugados**.
- Borra el fixture generado; conserva las asignaciones de equipos a zona (no hay que rehacerlas).
- Bloqueada (409, con motivo) si algún partido ya se jugó.
- ⚠️ Ver la brecha del mapa de transiciones del frontend anotada en HU-35.
### HU-115 · [NUEVO] Apelación de sanciones — `S`
**Como** equipo/jugador sancionado **quiero** poder apelar una sanción **para** que el admin la
revise antes de que se cumpla en su totalidad.
- Una sanción activa puede pasar a estado "apelada" con motivo; el admin la resuelve como aceptada
(levanta la sanción) o rechazada (sigue vigente).
- Sin ruta de auto-servicio pública confirmada en esta pasada — a verificar si la apelación la carga
el propio equipo o el admin en su representación.
### HU-116 · [NUEVO] Cuerpo técnico del equipo (DT / Asistente) — `S`
**Como** owner/admin **quiero** registrar el cuerpo técnico de un equipo (Director Técnico,
Asistente) por temporada **para** llevar esa información igual que el plantel de jugadores.
- Tab "Cuerpo técnico" dentro del equipo (junto a Jugadores/Puntuaciones/Sanciones).
- Roles: solo **DT** y **Asistente** (el rol "DT-Jugador" que existió brevemente fue retirado —
quedan dos roles, no tres).
- Scopeado por equipo + temporada, igual que el plantel.
- Visible también en la página pública del equipo.
### HU-117 · [NUEVO] Importación de plantel por CSV — `S`
**Como** owner/admin **quiero** cargar varios jugadores de una sola vez subiendo un CSV **para** dar
de alta un plantel completo más rápido que fila por fila.
- Reemplaza al viejo botón "Importar plantel de una temporada anterior" en el tab Jugadores del
equipo (esa funcionalidad de copiar-de-temporada-previa sigue existiendo, pero reubicada — ver
HU-53).
- Columnas en el mismo orden que la fila editable de alta manual (HU-52): Nombre, Segundo nombre,
Apellido, Documento, Fecha de nacimiento, Teléfono, Obra social.
- Cada fila se valida con las mismas reglas que el alta manual antes de enviarse; las filas inválidas
se listan con su motivo y NO se importan — las válidas sí, sin que un error en una fila bloquee al
resto.
- Botón para descargar una plantilla CSV vacía con los headers correctos.
---
## Épica 24 — Deuda técnica y brechas conocidas (no son historias, son follow-ups)
Recopilado de la auditoría 2026-09-02. No son features nuevas — son casos donde el comportamiento
documentado/esperado y el código real difieren, y alguien tiene que decidir qué lado gana.
> **Resueltos (2026-09-02, misma sesión, después de la auditoría)**: los ítems 1-5 de abajo ya
> fueron corregidos y están en `develop`. **Resueltos (2026-09-03, sesión de reglas de
> habilitación)**: los ítems 8-10. **Resueltos (2026-09-05, ronda de E2E con flujos completos y
> datos reales)**: los ítems 11-15. HU-126/HU-127 (arriba, fin de la Épica 22) quedaron abiertas de
> esa misma ronda — no son deuda técnica sino comportamiento a decidir con el owner.
1. ~~**Series playoff Best-of-N no se generan en producción (HU-82/HU-46).**~~ Resuelto:
`StageService.SeedPlayoffCupsAsync`/`SeedKnockoutStageAsync`/`SeedMultiGroupCrossCupStageAsync`
crean un `MatchSeries` real por cruce cuando `Stage.BestOf > 1`, y `SeriesInProgressPanel` (tab
Playoff de División, admin) agrega la acción de UI para cargar el 2º/3er partido de una serie.
2. ~~**Sin avance automático entre rondas de bracket (HU-82).**~~ Resuelto:
`StageService.TryAdvanceStageWinnerAsync` empuja el ganador de cada slot decidido (serie o
partido único) a la ronda siguiente del mismo bracket — llamado desde las 3 acciones de carga de
resultado de `MatchController` y desde el propio seeding (para propagar byes).
3. ~~**`AuditAction.BackupRestore` nunca se loguea (HU-101).**~~ Resuelto:
`BackupOperationsService.RestoreBackupAsync` ahora loguea vía `IAuditService`.
4. ~~**Mapa de transiciones de estado del frontend desactualizado (HU-35/HU-114).**~~ Resuelto:
`tournamentStatusTransitions.ts` incluye `Ongoing → RegistrationClosed`.
5. ~~**Validación del wizard permite copa cruzada sin playoff (HU-47).**~~ Resuelto:
`validateCrossCupStep` rechaza una copa cruzada con `cups.length === 0`.
6. ~~**`TournamentsPage.tsx` (listado plano de torneos) quedó huérfano (HU-29).**~~ Resuelto
(2026-09-02, commit `7eb4dd0`): se eliminaron las tres rutas huérfanas (`/torneos`,
`/panel/torneos`, `/panel/divisiones` sin scope) — cada "Volver"/cancelar ahora usa
`navigate(-1)` en vez de apuntar a una de estas rutas muertas. Esta nota había quedado
desactualizada en la pasada del 2026-09-03: el archivo ya no existe en el repo, solo el texto no
se había corregido.
7. ~~`Docs/QA-CHECKLIST-E2E.md`~~ — el checklist se retiró del repo (ya cumplió su función; el E2E
contra staging que documentaba se completó en la sesión del 2026-09-03, ver ítems 8-10). Si se
necesita un nuevo barrido E2E, generar uno nuevo desde cero contra las rutas reales, no reflotar
el archivo viejo.
8. ~~**`LoadMatchResultFromSheetsRequest.MatchId` marcado `required` rechazaba toda carga de
resultado (HU-69).**~~ Resuelto (2026-09-03): el DTO nunca recibe `matchId` del cliente (viene de
la ruta), pero `required` hacía que `System.Text.Json` rechazara el body entero antes de que el
controller pudiera asignarlo desde la ruta — ningún resultado se pudo cargar por este endpoint
hasta el fix.
9. ~~**El roster de un partido mostraba "No habilitado" para todos, incluso jugadores realmente
habilitados (HU-62).**~~ Resuelto (2026-09-03): `CreateMap<Player, PublicPlayerResponse>()` no
tenía mapeo para `MedicalRecordStatus`/`IsHabilitado`/`JerseyNumber` (viven en
`PlayerTeamRegistration`, no en `Player`), y el include de EF tampoco cargaba esa navegación para
el roster (sí para los goleadores). El dato real en base siempre fue correcto — solo lo que
mostraba la respuesta del partido estaba mal. Corregido con un paso `AfterMap` en
`MatchProfile.cs` + el include faltante en `MatchRepository.GetDetailByIdOrSlugAsync`.
10. ~~**Datos de seed sembrados antes de la feature de ficha médica quedaron con TODOS los jugadores
en `Pending` (HU-57/HU-118/HU-119).**~~ Resuelto (2026-09-03):
`MedicalRecordSeedBackfiller.BackfillMedicalRecordsAsync` ahora también toma como candidatos los
registros `Pending`/`Rejected` cuyo `CreatedBy == AuditConstants.SystemUser` — un valor que solo
escribe el seeder, nunca una acción real de admin — y los aprueba junto con subirles el archivo.
Un registro creado por un admin real, sin importar su estado, nunca entra en este camino. Esto
hace que el self-heal automático de `DataSeeder.SeedAsync` (que ya corre solo con que
`Seed:Enabled=true`, sin flags extra) cubra tanto una base nunca migrada a la feature de ficha
médica como una restaurada desde un backup viejo — no hace falta una acción puntual como la que
se usó para arreglar el dato en vivo esta sesión.
11. ~~**Ver un jugador recién creado desde el panel rompía toda la página (crash).**~~ Resuelto
(2026-09-05): `POST /api/players` (admin-only) devolvía `PublicPlayerResponse` (forma pública,
sin `documentNumber`/`birthDate`/`phoneNumber`/`socialSecurity`) pero el contexto de React seguía
ese objeto incompleto como el jugador "actual"; al navegar al detalle antes de que la carga real
terminara, el render usaba esos campos faltantes y tiraba. Corregido devolviendo
`AdminPlayerResponse` (mismo shape que ya usa `GetPlayerByIdCompleteDataAsync`) desde el create.
12. ~~**Fecha de nacimiento mostrada con un día de menos en toda la lista de Jugadores.**~~ Resuelto
(2026-09-05): la columna usaba `new Date(value).toLocaleDateString()`, que corre la fecha a
horario local (Argentina, UTC-3) para un campo que es solo-fecha (mediodía UTC), restando un día.
Corregido reusando `formatCalendarDate`, el helper ya establecido en el proyecto para este caso
exacto (usado en la ficha de jugador, pero no en el listado).
13. ~~**Novedades: no había forma de editar una publicación ya creada.**~~ Resuelto (2026-09-05):
`BlogPostEditPage` y su ruta (`/panel/blog/:blogPostId/editar`) ya estaban completos y andando,
pero la lista solo ofrecía "Ver" (una previsualización) y "Eliminar" — nada enlazaba a la edición.
14. ~~**Aceptar la apelación de una sanción no tenía ningún efecto real.**~~ Resuelto (2026-09-05):
`FechasRemaining`/`IsActive` se calculaban solo a partir de `Duration` y fechas jugadas, sin mirar
`AppealStatus` en ningún lado — el jugador/equipo seguía suspendido igual, apelación aceptada o no.
15. ~~**Crear un usuario con espacios en el nombre mostraba un error crudo en inglés.**~~ Resuelto
(2026-09-05): no había validación de formato de nombre de usuario en el frontend, así que el
valor viajaba tal cual al backend, que rechazaba con el texto nativo de ASP.NET Identity
("Username '...' is invalid, can only contain letters or digits.") sin traducir.
---
## Resumen
- **Test counts**: verificado 2026-09-03: **826 tests backend, 725 tests frontend**, ambos en verde
(la cifra "808/645" de dos pasadas atrás ya está vieja). Correr
`dotnet test Club12-Backend/Solution/Club12.sln` y `npx vitest run` (desde `Club12-WebClient`)
para el número vigente en cualquier momento — no confiar en un número congelado en este documento.
- **Historias obsoletas/reemplazadas**: HU-29 (listado plano de torneos, eliminado — ver Épica 24
ítem 6), HU-41/HU-42 (superadas por HU-106/107/108, sin cambios respecto a la nota original),
HU-64 (disparador de fixture mal descripto), HU-97 (eliminada a propósito).
- **Épica 24 (deuda técnica) completa**: los 10 ítems están resueltos, ninguno queda abierto. Los
últimos tres en cerrarse (2026-09-03): HU-69 (`MatchId` required bloqueaba toda carga de
resultado), HU-62 (roster mostraba "No habilitado" para todos por un mapeo faltante), y el
self-heal de datos de seed viejos (ítem 10, guarda por `CreatedBy == SystemUser`).
- **Cambio funcional más grande de esta pasada (2026-09-03)**: las tres reglas de habilitación
alrededor de la carga de resultados — HU-118 (umbral de 4 habilitados para cargar un resultado
normal) y HU-119 (mismo umbral para poder iniciar el torneo) — más la corrección del mensaje de
error de HU-62 (lista agrupada por equipo, con nombres reales) y el bug de visualización del
roster (ítem 9 de Épica 24).
- **Cambio estructural más grande de la pasada anterior**: la entidad Temporada (Season) por encima
de Torneo (D7/HU-113), que reordenó la navegación completa del panel admin y el punto de entrada
del wizard.