fix(adm-001): cascada de inactividad Medio→Seccion — Closes #16 #18

Merged
dmolinari merged 5 commits from fix/adm-001-cascada-inactividad into main 2026-04-17 14:50:03 +00:00
Owner

Contexto

REQ-SEC-001 (crear Seccion) ya rechazaba con 404 si el Medio estaba inactivo, pero REQ-SEC-002 (update), REQ-SEC-003 (deactivate) y REQ-SEC-004 (reactivate) no extendían esa regla al padre. Esto dejaba una ventana abierta para mutar Secciones de un Medio desactivado.

Decisión adoptada (2026-04-17, issue #16): Opción B — freeze total en cascada: si Medio.Activo = false, TODAS las mutaciones de Seccion devuelven 409 medio_inactivo. Solo reactivar el Medio desbloquea la operación. Formalizado como REQ-SEC-006.

Cambios

Backend

  • MedioInactivoException nueva en SIGCM2.Domain/Exceptions/ (extiende DomainException)
  • Guard en los 3 handlers (UpdateSeccion, DeactivateSeccion, ReactivateSeccion): inyecta IMedioRepository, valida medio.Activo antes de cualquier mutación
  • ExceptionFilter: mapea MedioInactivoException409 { error: "medio_inactivo", message }

Frontend

  • Nuevo componente MedioInactivoBanner (Alert destructive) en features/secciones/components/
  • SeccionesListPage: cuando hay filtro por medioId y el medio está inactivo → muestra banner + deshabilita "Nueva sección" + deshabilita Editar/Desactivar/Reactivar en tabla
  • SeccionDetailPage: fetch del medio padre; si inactivo → muestra banner + deshabilita Editar/Desactivar/Reactivar
  • DeactivateSeccionModal: acepta prop disabled para bloquear el trigger

Commits

  1. feat(domain): MedioInactivoException (issue #16)
  2. feat(secciones): validar medio activo en update/deactivate/reactivate — issue #16
  3. feat(api): mapping 409 medio_inactivo en ExceptionFilter — issue #16
  4. feat(web): banner y disabled de secciones de medio inactivo — issue #16
  5. test(secciones): cobertura cascada de inactividad — issue #16

Tests

  • Unit (xUnit + NSubstitute): +3 tests (uno por handler): Handle_MedioInactivo_ThrowsMedioInactivoExceptionAndNoAuditLogged — verifica que el throw ocurre ANTES del audit (fail-closed). Total: 38 → 41 passed.
  • Integration (SIGCM2.Api.Tests): +3 tests: UpdateSeccion_WhenMedioInactive_Returns409, DeactivateSeccion_WhenMedioInactive_Returns409, ReactivateSeccion_WhenMedioInactive_Returns409 — verifican error: "medio_inactivo" + sin AuditEvent row.
  • Frontend (Vitest + RTL): +1 test en SeccionesListPage.test.tsx verificando que el banner NO aparece cuando no hay filtro de medio activo. Total: 208 passed.

Test plan

  • Crear un Medio, crear una Seccion, desactivar el Medio
  • Intentar PUT /api/v1/admin/secciones/{id} → debe devolver 409 { error: "medio_inactivo" }
  • Intentar POST .../deactivate sobre la Seccion → 409
  • Intentar POST .../reactivate sobre la Seccion → 409
  • Reactivar el Medio → verificar que las 3 operaciones vuelven a funcionar
  • UI: filtrar SeccionesListPage por ese medioId → debe verse el banner y botones deshabilitados
  • UI: navegar al detalle de una Seccion de medio inactivo → banner presente, Editar/Desactivar/Reactivar deshabilitados
  • dotnet test verde
  • npx vitest run verde (208 tests)
## Contexto REQ-SEC-001 (crear Seccion) ya rechazaba con 404 si el Medio estaba inactivo, pero REQ-SEC-002 (update), REQ-SEC-003 (deactivate) y REQ-SEC-004 (reactivate) no extendían esa regla al padre. Esto dejaba una ventana abierta para mutar Secciones de un Medio desactivado. **Decisión adoptada (2026-04-17, issue #16):** Opción B — freeze total en cascada: si `Medio.Activo = false`, TODAS las mutaciones de Seccion devuelven `409 medio_inactivo`. Solo reactivar el Medio desbloquea la operación. Formalizado como **REQ-SEC-006**. ## Cambios ### Backend - `MedioInactivoException` nueva en `SIGCM2.Domain/Exceptions/` (extiende `DomainException`) - Guard en los 3 handlers (`UpdateSeccion`, `DeactivateSeccion`, `ReactivateSeccion`): inyecta `IMedioRepository`, valida `medio.Activo` antes de cualquier mutación - `ExceptionFilter`: mapea `MedioInactivoException` → `409 { error: "medio_inactivo", message }` ### Frontend - Nuevo componente `MedioInactivoBanner` (Alert destructive) en `features/secciones/components/` - `SeccionesListPage`: cuando hay filtro por `medioId` y el medio está inactivo → muestra banner + deshabilita "Nueva sección" + deshabilita Editar/Desactivar/Reactivar en tabla - `SeccionDetailPage`: fetch del medio padre; si inactivo → muestra banner + deshabilita Editar/Desactivar/Reactivar - `DeactivateSeccionModal`: acepta prop `disabled` para bloquear el trigger ## Commits 1. `feat(domain): MedioInactivoException (issue #16)` 2. `feat(secciones): validar medio activo en update/deactivate/reactivate — issue #16` 3. `feat(api): mapping 409 medio_inactivo en ExceptionFilter — issue #16` 4. `feat(web): banner y disabled de secciones de medio inactivo — issue #16` 5. `test(secciones): cobertura cascada de inactividad — issue #16` ## Tests - **Unit (xUnit + NSubstitute):** +3 tests (uno por handler): `Handle_MedioInactivo_ThrowsMedioInactivoExceptionAndNoAuditLogged` — verifica que el throw ocurre ANTES del audit (fail-closed). Total: 38 → 41 passed. - **Integration (SIGCM2.Api.Tests):** +3 tests: `UpdateSeccion_WhenMedioInactive_Returns409`, `DeactivateSeccion_WhenMedioInactive_Returns409`, `ReactivateSeccion_WhenMedioInactive_Returns409` — verifican `error: "medio_inactivo"` + sin AuditEvent row. - **Frontend (Vitest + RTL):** +1 test en `SeccionesListPage.test.tsx` verificando que el banner NO aparece cuando no hay filtro de medio activo. Total: 208 passed. ## Test plan - [x] Crear un Medio, crear una Seccion, desactivar el Medio - [x] Intentar PUT `/api/v1/admin/secciones/{id}` → debe devolver `409 { error: "medio_inactivo" }` - [x] Intentar POST `.../deactivate` sobre la Seccion → 409 - [x] Intentar POST `.../reactivate` sobre la Seccion → 409 - [x] Reactivar el Medio → verificar que las 3 operaciones vuelven a funcionar - [x] UI: filtrar SeccionesListPage por ese medioId → debe verse el banner y botones deshabilitados - [x] UI: navegar al detalle de una Seccion de medio inactivo → banner presente, Editar/Desactivar/Reactivar deshabilitados - [x] `dotnet test` verde - [x] `npx vitest run` verde (208 tests)
dmolinari added 5 commits 2026-04-17 14:47:02 +00:00
dmolinari merged commit b7ac9831f9 into main 2026-04-17 14:50:03 +00:00
dmolinari deleted branch fix/adm-001-cascada-inactividad 2026-04-17 14:50:03 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dmolinari/SIG-CM2.0#18