# Sprint 2 — Etapa 1: Migración del modelo de recursos > **Tipo:** dominio puro + persistencia. Sin UI. Sin cambios visuales. > **Objetivo:** migrar `ActivityResourceAssignment.utilizationPercent` → `utilizationMinutes + units`, actualizar la fórmula de simulación, incrementar Dexie a v3, y garantizar que los datos existentes se preservan correctamente. > **Resultado esperado:** tests verdes + reporte de archivos modificados. La UI en Etapas 2–3. --- ## Contexto imprescindible ### Estado actual del modelo (lo que vas a encontrar en el código) ```typescript // src/domain/types.ts — HOY interface ActivityResourceAssignment { resourceId: UUID utilizationPercent: number // 0–1, fracción del tiempo de la actividad } // Fórmula actual en src/domain/simulation.ts const hoursUsed = (executionTimeMinutes / 60) * assignment.utilizationPercent const cost = resource.costPerHour * hoursUsed * execProb ``` ```typescript // src/domain/schemas.ts — HOY export const ActivityResourceAssignmentSchema = z.object({ resourceId: z.string().uuid(), utilizationPercent: z.number().min(0).max(1), }) ``` ```typescript // src/persistence/db.ts — HOY // Dexie versión 2 (v1 original + v2 de Sprint 1) // La tabla 'resources' ya existe desde v1. // ActivityResourceAssignment está embebido en Activity (no es tabla propia). ``` ### Lo que NO existe todavía (no confundir con lo anterior) - No existe tabla `resourceAssignments` separada. Los assignments están embebidos en el array `Activity.assignedResources`. - No existe campo `utilizationMinutes`. Solo existe `utilizationPercent`. - No existe campo `units`. Hay que agregarlo. --- ## Cambios requeridos ### 1. `src/domain/types.ts` Reemplazar `ActivityResourceAssignment`: ```typescript // ANTES export interface ActivityResourceAssignment { resourceId: UUID utilizationPercent: number } // DESPUÉS export interface ActivityResourceAssignment { resourceId: UUID utilizationMinutes: number // minutos que este recurso dedica por ejecución (independiente del tiempo de la actividad) units: number // cantidad de unidades del recurso (ej: 2 impresoras). Default: 1. } ``` El tipo `Resource` NO cambia. Solo cambia `ActivityResourceAssignment`. ### 2. `src/domain/schemas.ts` Reemplazar `ActivityResourceAssignmentSchema`: ```typescript // ANTES export const ActivityResourceAssignmentSchema = z.object({ resourceId: z.string().uuid(), utilizationPercent: z.number().min(0).max(1), }) // DESPUÉS export const ActivityResourceAssignmentSchema = z.object({ resourceId: z.string().uuid(), utilizationMinutes: z.number().min(0, 'Los minutos no pueden ser negativos'), units: z.number().int().min(1, 'Las unidades deben ser al menos 1').default(1), }) ``` ### 3. `src/domain/simulation.ts` Actualizar la fórmula de costo de recursos. Buscar el bloque que calcula `hoursUsed` y reemplazarlo: ```typescript // ANTES const hoursUsed = (executionTimeMinutes / 60) * assignment.utilizationPercent const cost = resource.costPerHour * hoursUsed * execProb // DESPUÉS const hoursUsed = (assignment.utilizationMinutes / 60) * assignment.units const cost = resource.costPerHour * hoursUsed * execProb ``` El `execProb` se mantiene igual — sigue siendo la probabilidad de ejecución del nodo en el grafo BPMN. El costo esperado del recurso por ejecución del proceso es `costPerHour × utilizationMinutes/60 × units × execProb`. También actualizar la línea de acumulación de minutos por recurso (buscar `resourceTotals`): ```typescript // ANTES minutes: prev.minutes + activity.executionTimeMinutes * assignment.utilizationPercent * execProb, // DESPUÉS minutes: prev.minutes + assignment.utilizationMinutes * assignment.units * execProb, ``` ### 4. `src/persistence/db.ts` Agregar versión 3 con función de upgrade. La migración convierte los assignments embebidos en Activity: ```typescript this.version(3) .stores({ // Los stores no cambian — ActivityResourceAssignment está embebido en Activity, no es tabla propia processes: 'id, name, updatedAt', activities: 'id, processId, bpmnElementId', gateways: 'id, processId, bpmnElementId', resources: 'id, processId, name', simulations: 'id, processId, executedAt', }) .upgrade(async (tx) => { // Migrar assignedResources de cada actividad: utilizationPercent → utilizationMinutes + units await tx.table('activities').toCollection().modify((act) => { if (!Array.isArray(act.assignedResources)) { act.assignedResources = [] return } act.assignedResources = act.assignedResources.map((assignment: Record) => { if ('utilizationPercent' in assignment) { // Heurística de conversión: minutos = round(executionTimeMinutes × utilizationPercent) // Si executionTimeMinutes no está disponible o es 0, usar 0 (conservador) const execMinutes = typeof act.executionTimeMinutes === 'number' ? act.executionTimeMinutes : 0 const utilizationMinutes = Math.round(execMinutes * (assignment.utilizationPercent as number)) return { resourceId: assignment.resourceId, utilizationMinutes, units: 1, } } // Ya está en el nuevo formato (por si se corre la migración dos veces) if ('utilizationMinutes' in assignment) { return { resourceId: assignment.resourceId, utilizationMinutes: assignment.utilizationMinutes ?? 0, units: assignment.units ?? 1, } } // Fallback defensivo return { resourceId: assignment.resourceId, utilizationMinutes: 0, units: 1 } }) }) }) ``` **Importante:** no eliminar las versiones 1 y 2 anteriores. Dexie requiere el historial completo de versiones. ### 5. Puntos de inicialización en `src/features/import/ImportPage.tsx` Buscar donde se inicializa una nueva actividad (al importar BPMN). Hay una línea similar a: ```typescript assignedResources: [], ``` Esto está bien — un array vacío es válido con el nuevo modelo. No requiere cambio, pero verificar que no haya ningún lugar que inicialice con `utilizationPercent`. **Grep de verificación:** buscar en todo `src/` cualquier referencia a `utilizationPercent`. Al terminar la etapa, esta búsqueda debe devolver cero resultados. --- ## Tests requeridos ### Tests unitarios — `tests/domain/simulation.test.ts` Agregar los siguientes casos al archivo existente (no borrar los tests actuales): ``` describe('recursos con modelo utilizationMinutes + units', () => { test('actividad sin recursos: costo = directCostFixed') test('actividad con 1 recurso: costo = fijo + costPerHour × utilizationMinutes/60 × units × execProb') test('actividad con 2 recursos: costo = fijo + suma de costos de recursos') test('units = 2: costo de recurso se duplica') test('utilizationMinutes = 0: el recurso no agrega costo') test('resource no encontrado en el mapa: se ignora sin lanzar error') }) ``` ### Test de migración — `tests/integration/` (crear archivo nuevo `db-migration-v3.test.ts`) ``` test('migración v2→v3: actividad con utilizationPercent se convierte a utilizationMinutes correctamente') test('migración v2→v3: actividad sin assignedResources mantiene array vacío') test('migración v2→v3: actividad con assignedResources vacío no falla') test('migración v2→v3: el costo calculado después de la migración es aproximadamente igual al anterior') // El costo debe ser igual ± redondeo de Math.round() ``` --- ## Verificaciones finales obligatorias Antes de reportar la etapa como completa: 1. **Grep de limpieza:** `grep -r "utilizationPercent" src/` — debe devolver 0 resultados. 2. **Build limpio:** `npm run build` sin errores de TypeScript. 3. **Tests:** `npm run test` — todos los tests (existentes + nuevos) deben pasar. 4. **Smoke test manual:** cargar la app en dev (`npm run dev`), abrir un proceso existente con recursos, verificar que no crashea y que el workspace carga. --- ## Alcance estricto — lo que NO entra en esta etapa - ❌ Cambios en `ActivityPanel.tsx` (UI del panel de actividad) — Etapa 2 - ❌ Cambios en `ResourcesPanel.tsx` — Etapa 2 si necesita ajustes - ❌ Cambios en `ActivitiesTable.tsx` ni en el reporte — Etapa 3 - ❌ Cambios en PDFs — Etapa 5 - ❌ Edición inline de proceso/cliente — Etapa 4 - ❌ CumulativeSavingsChart, payback label, badge — Etapa 3 Si al hacer el grep de limpieza encontrás referencias a `utilizationPercent` en componentes UI, **no las corrijas en esta etapa**. Documentalas en el reporte para que Etapa 2 las tome. --- ## Reporte esperado al terminar ``` ## Etapa 1 completada ### Archivos modificados - src/domain/types.ts — ActivityResourceAssignment: utilizationPercent → utilizationMinutes + units - src/domain/schemas.ts — ActivityResourceAssignmentSchema actualizado - src/domain/simulation.ts — fórmula de costo de recursos actualizada - src/persistence/db.ts — versión 3 con migración ### Archivos creados - tests/integration/db-migration-v3.test.ts ### Tests - X tests unitarios nuevos (todos verdes) - X tests de migración (todos verdes) - X tests previos (todos siguen verdes) ### Grep de limpieza - "utilizationPercent" en src/: 0 resultados ✅ (o lista de archivos UI pendientes para Etapa 2) ### Build - npm run build: ✅ sin errores TypeScript ### Notas [cualquier hallazgo relevante para Etapas siguientes] ```