[PRD-002 / Domain] Rubro deactivation guard — rechazar desactivar Rubro con Productos activos #41

Closed
opened 2026-04-19 16:50:20 +00:00 by dmolinari · 0 comments
Owner

Descripción del problema

Con PRD-002 merged (Product CRUD activado), una brecha semántica quedó expuesta:

Hoy: DeactivateRubroCommandHandler solo valida si un Rubro tiene hijos Rubros activos. Si tiene Productos activos referenciándolo vía Product.RubroId, la desactivación procede sin error → los Productos quedan con referencias a un Rubro inactivo (inconsistencia semántica).

Escenario:

  1. Admin desactiva un Rubro que tiene Productos activos.
  2. No hay error (409). El Rubro se desactiva.
  3. Los Productos siguen activos, pero su RubroId ahora apunta a un Rubro con Activo=0.
  4. Queries que filtran Rubro.Activo=1 ya no devuelven el nombre del Rubro para esos Productos.

Root cause: PRD-002 diff deferred este guard a follow-up (scope discipline — PRD-002 ya entregaba ≥60 tests). Decision D5 en proposal (#467) documentó explícitamente la diferencia.

Solución propuesta

1. Extender validación en DeactivateRubroCommandHandler:

  • Cargar ProductQueryRepository vía DI (ya existe, activado en PRD-002)
  • Query: ProductQueryRepository.ExistsActiveByRubroAsync(rubroId) (NEW METHOD)
  • Si retorna true → lanzar RubroEnUsoPorProductosException (nueva excepción de dominio)
  • ExceptionFilter mapea a 409 Conflict (estado violation, no validation error)

2. Implementar ProductQueryRepository.ExistsActiveByRubroAsync(int rubroId):

SELECT COUNT(1) FROM dbo.Product WHERE RubroId = @rubroId AND IsActive = 1

3. Nueva excepción:

public sealed class RubroEnUsoPorProductosException(int rubroId) 
    : Exception($"No se puede desactivar el rubro {rubroId} porque tiene productos activos. Primero desactivá todos los productos asociados.");

4. ExceptionFilter entry:

case RubroEnUsoPorProductosException ex:
    context.Result = new ObjectResult(new { 
        error = "rubro_en_uso_por_productos", 
        message = ex.Message 
    }) { StatusCode = StatusCodes.Status409Conflict };

5. Tests:

  • Application handler test: DeactivateRubroCommandHandlerTests + nuevo scenario Deactivate_WithActiveProducts_Returns409EnUso
  • API integration test: CategoriesControllerTests (o RubrosControllerTests?) + scenario DELETE Rubro con Product activo → 409

Archivos afectados

  • src/api/SIGCM2.Domain/Exceptions/RubroEnUsoPorProductosException.cs (NUEVA)
  • src/api/SIGCM2.Infrastructure/Persistence/ProductQueryRepository.cs — agregar ExistsActiveByRubroAsync(int rubroId)
  • src/api/SIGCM2.Application/Categories/Deactivate/DeactivateCategoryCommandHandler.cs — agregar validación
  • src/api/SIGCM2.Api/Filters/ExceptionFilter.cs — agregar case RubroEnUsoPorProductosException → 409
  • Test updates: DeactivateCategoryCommandHandlerTests.cs, API controller tests
  • Frontend: CategoryTree Delete button ya deshabilitado si hay Productos (implementado durante W5 fixes en PRD-002). Si no, agregar inline 409 handling en DeactivateCategoryDialog.

Estimación

  • Complexity: LOW (pattern claro: copiar flujo de ProductTypeEnUsoException/IProductQueryRepository)
  • Effort: ~3 horas (handler + exception + test + ExceptionFilter + frontend polish)
  • Blocking: NONE (seguimiento a PRD-002, no bloquea PRD-003+)
  • Testing: Strict TDD (RED → GREEN por cada cambio)

Priority

MEDIUM — Semánticamente importante (integridad de referencias), pero la ventana de exposición es limitada (entre merge PRD-002 y merge de este fix). Sin datos reales en STAGING, el riesgo es bajo.

Target: Resolver antes de PRD-008 (seed de 12 productos legacy). Si un producto queda huérfano en seed, el issue sigue de cerca.


Assignee: dmolinari
Label: followup
Related: PR #40 (PRD-002 merged), decision D5 proposal #467, spec R6 deactivate
Engram: sdd/prd-002-product-crud/proposal (D5 — Rubro deactivation guard deferred)
Next UDT: PRD-003

## Descripción del problema Con PRD-002 merged (Product CRUD activado), una brecha semántica quedó expuesta: **Hoy**: `DeactivateRubroCommandHandler` solo valida si un Rubro tiene **hijos Rubros activos**. Si tiene **Productos activos** referenciándolo vía `Product.RubroId`, la desactivación procede sin error → los Productos quedan con referencias a un Rubro inactivo (inconsistencia semántica). **Escenario**: 1. Admin desactiva un Rubro que tiene Productos activos. 2. No hay error (409). El Rubro se desactiva. 3. Los Productos siguen activos, pero su `RubroId` ahora apunta a un Rubro con `Activo=0`. 4. Queries que filtran `Rubro.Activo=1` ya no devuelven el nombre del Rubro para esos Productos. **Root cause**: PRD-002 diff deferred este guard a follow-up (scope discipline — PRD-002 ya entregaba ≥60 tests). Decision D5 en proposal (#467) documentó explícitamente la diferencia. ## Solución propuesta **1. Extender validación en `DeactivateRubroCommandHandler`:** - Cargar ProductQueryRepository vía DI (ya existe, activado en PRD-002) - Query: `ProductQueryRepository.ExistsActiveByRubroAsync(rubroId)` (NEW METHOD) - Si retorna true → lanzar `RubroEnUsoPorProductosException` (nueva excepción de dominio) - ExceptionFilter mapea a **409 Conflict** (estado violation, no validation error) **2. Implementar `ProductQueryRepository.ExistsActiveByRubroAsync(int rubroId)`:** ```sql SELECT COUNT(1) FROM dbo.Product WHERE RubroId = @rubroId AND IsActive = 1 ``` **3. Nueva excepción:** ```csharp public sealed class RubroEnUsoPorProductosException(int rubroId) : Exception($"No se puede desactivar el rubro {rubroId} porque tiene productos activos. Primero desactivá todos los productos asociados."); ``` **4. ExceptionFilter entry:** ```csharp case RubroEnUsoPorProductosException ex: context.Result = new ObjectResult(new { error = "rubro_en_uso_por_productos", message = ex.Message }) { StatusCode = StatusCodes.Status409Conflict }; ``` **5. Tests:** - Application handler test: `DeactivateRubroCommandHandlerTests` + nuevo scenario `Deactivate_WithActiveProducts_Returns409EnUso` - API integration test: `CategoriesControllerTests` (o `RubrosControllerTests`?) + scenario DELETE Rubro con Product activo → 409 ## Archivos afectados - `src/api/SIGCM2.Domain/Exceptions/RubroEnUsoPorProductosException.cs` (NUEVA) - `src/api/SIGCM2.Infrastructure/Persistence/ProductQueryRepository.cs` — agregar `ExistsActiveByRubroAsync(int rubroId)` - `src/api/SIGCM2.Application/Categories/Deactivate/DeactivateCategoryCommandHandler.cs` — agregar validación - `src/api/SIGCM2.Api/Filters/ExceptionFilter.cs` — agregar case `RubroEnUsoPorProductosException → 409` - Test updates: `DeactivateCategoryCommandHandlerTests.cs`, API controller tests - Frontend: CategoryTree `Delete` button ya deshabilitado si hay Productos (implementado durante W5 fixes en PRD-002). Si no, agregar inline 409 handling en `DeactivateCategoryDialog`. ## Estimación - **Complexity**: LOW (pattern claro: copiar flujo de ProductTypeEnUsoException/IProductQueryRepository) - **Effort**: ~3 horas (handler + exception + test + ExceptionFilter + frontend polish) - **Blocking**: NONE (seguimiento a PRD-002, no bloquea PRD-003+) - **Testing**: Strict TDD (RED → GREEN por cada cambio) ## Priority **MEDIUM** — Semánticamente importante (integridad de referencias), pero la ventana de exposición es limitada (entre merge PRD-002 y merge de este fix). Sin datos reales en STAGING, el riesgo es bajo. **Target**: Resolver antes de PRD-008 (seed de 12 productos legacy). Si un producto queda huérfano en seed, el issue sigue de cerca. --- **Assignee**: dmolinari **Label**: followup **Related**: PR #40 (PRD-002 merged), decision D5 proposal #467, spec R6 deactivate **Engram**: `sdd/prd-002-product-crud/proposal` (D5 — Rubro deactivation guard deferred) **Next UDT**: PRD-003
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dmolinari/SIG-CM2.0#41