Files
inq-roi-simulador-web/sprints/sprint-4/ETAPA_2_PROMPT.md

13 KiB

Sprint 4 — Etapa 2: Schema PostgreSQL + migración de repositorios (procesos + grupos + settings)

Contexto

InQ ROI es una herramienta web de análisis de procesos BPMN y ROI de automatización. Stack: React 18 + Vite + TypeScript + TanStack Router + Zustand + shadcn/ui + Tailwind. Etapa 1 (completada): auth con Google, rutas protegidas, tabla users en Supabase, AuthService abstraction.

Esta etapa migra la persistencia de procesos, grupos de biblioteca y settings de IndexedDB/Dexie a Supabase PostgreSQL. Las actividades, gateways, recursos y simulaciones siguen en Dexie hasta Etapas 3-4.


Prerequisitos — verificar antes de empezar

  • .env.local tiene VITE_SUPABASE_URL y VITE_SUPABASE_ANON_KEY
  • El script supabase/migrations/001_create_users.sql ya fue ejecutado en Supabase
  • src/auth/AuthContext.tsx y useAuth() hook existen y funcionan

Alcance estricto — qué entra en esta etapa

  1. Scripts SQL para processes, process_groups, app_settings en supabase/migrations/
  2. Nuevos repositorios Supabase para procesos, grupos y settings (reemplazando las implementaciones Dexie de esas entidades)
  3. Actualizar los stores (useProcessStore, useLibraryStore) para pasar userId a los repos en operaciones de escritura
  4. Limpieza de estado híbrido: el delete de proceso sigue limpiando Dexie (activities, gateways, resources, simulations) hasta que se migren en Etapa 3-4

Qué NO entra en esta etapa

  • Migración de actividades, gateways, resources, simulations → Etapa 3
  • Tabla user_groups y group_memberships (acceso a usuarios) → Etapa 5
  • Tabla process_access → Etapa 5
  • UI de visibilidad privado/compartido → Etapa 5
  • Avatar/nombre en AppHeader → Etapa 6
  • Eliminar Dexie del proyecto → Etapa 4 (cuando todo esté migrado)
  • Cambiar el tipo TypeScript Process.clientName — NO modificar src/domain/types.ts

Distinción importante: dos tipos de grupos

process_groups (esta etapa): grupos para organizar la biblioteca de procesos — "Cliente X", "Sin clasificar". Corresponde a ProcessGroup en src/domain/types.ts. Son compartidos por todo el equipo, sin control de acceso propio.

user_groups (Etapa 5): grupos de usuarios para control de acceso — "Equipo de consultoría", "Cliente Acme". Concepto nuevo, no existe en Dexie. No crear en esta etapa.


Scripts SQL — crear en supabase/migrations/

002_create_process_groups.sql

CREATE TABLE IF NOT EXISTS public.process_groups (
  id          uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
  name        text         NOT NULL,
  created_by  uuid         REFERENCES public.users(id),
  created_at  timestamptz  NOT NULL DEFAULT now(),
  updated_at  timestamptz  NOT NULL DEFAULT now()
);

ALTER TABLE public.process_groups ENABLE ROW LEVEL SECURITY;

-- Todos los autenticados pueden leer y escribir grupos (son recursos compartidos del equipo)
CREATE POLICY "all_process_groups" ON public.process_groups
  FOR ALL USING (auth.uid() IS NOT NULL)
  WITH CHECK (auth.uid() IS NOT NULL);

003_create_processes.sql

CREATE TABLE IF NOT EXISTS public.processes (
  id                      uuid         PRIMARY KEY DEFAULT gen_random_uuid(),
  name                    text         NOT NULL,
  client                  text         NOT NULL DEFAULT '',
  bpmn_xml                text         NOT NULL DEFAULT '',
  currency                text         NOT NULL DEFAULT 'USD',
  overhead_percentage     numeric      NOT NULL DEFAULT 0,
  annual_frequency        numeric      NOT NULL DEFAULT 1000,
  analysis_horizon_years  numeric      NOT NULL DEFAULT 3,
  automation_investment   numeric      NOT NULL DEFAULT 0,
  group_id                uuid         REFERENCES public.process_groups(id) ON DELETE SET NULL,
  tags                    text[]       NOT NULL DEFAULT '{}',
  owner_id                uuid         NOT NULL REFERENCES public.users(id),
  created_by              uuid         NOT NULL REFERENCES public.users(id),
  updated_by              uuid         NOT NULL REFERENCES public.users(id),
  created_at              timestamptz  NOT NULL DEFAULT now(),
  updated_at              timestamptz  NOT NULL DEFAULT now()
);

ALTER TABLE public.processes ENABLE ROW LEVEL SECURITY;

-- En Sprint 4 todos los usuarios son platform_admin: ven todo.
-- Las políticas granulares (process_access) se agregan en Etapa 5.
-- Por ahora: cualquier usuario autenticado puede leer y escribir todos los procesos.
CREATE POLICY "authenticated_all_processes" ON public.processes
  FOR ALL USING (auth.uid() IS NOT NULL)
  WITH CHECK (auth.uid() IS NOT NULL);

004_create_app_settings.sql

-- Settings globales del equipo (singleton compartido)
CREATE TABLE IF NOT EXISTS public.app_settings (
  id           text         PRIMARY KEY DEFAULT 'singleton',
  group_label  text         NOT NULL DEFAULT 'Cliente',
  updated_at   timestamptz  NOT NULL DEFAULT now()
);

ALTER TABLE public.app_settings ENABLE ROW LEVEL SECURITY;

-- Todos los autenticados pueden leer y actualizar settings
CREATE POLICY "all_app_settings" ON public.app_settings
  FOR ALL USING (auth.uid() IS NOT NULL)
  WITH CHECK (auth.uid() IS NOT NULL);

-- Insertar el singleton con defaults
INSERT INTO public.app_settings (id, group_label)
VALUES ('singleton', 'Cliente')
ON CONFLICT (id) DO NOTHING;

Estos scripts se ejecutan manualmente en Supabase Dashboard → SQL Editor, en orden numérico.


Mapeo de tipos TypeScript ↔ PostgreSQL

Los repositorios Supabase deben mapear entre los tipos del dominio (camelCase, timestamps como number ms Unix) y las columnas de PostgreSQL (snake_case, timestamptz).

NO modificar src/domain/types.ts. El mapeo es responsabilidad del repo.

Process → processes

TypeScript (Process) PostgreSQL (processes) Notas
id id igual
name name igual
clientName client rename solo en DB
bpmnXml bpmn_xml snake_case
currency currency igual
overheadPercentage overhead_percentage snake_case
annualFrequency annual_frequency snake_case
analysisHorizonYears analysis_horizon_years snake_case
automationInvestment automation_investment snake_case
groupId group_id snake_case
tags tags igual (array)
createdAt (number ms) created_at (timestamptz) new Date(createdAt).toISOString()
updatedAt (number ms) updated_at (timestamptz) igual conversión
owner_id nuevo campo, inyectado por el store
created_by nuevo campo, inyectado en primer save
updated_by nuevo campo, inyectado en cada save

Para from_db (PostgreSQL → TypeScript):

  • createdAt: new Date(row.created_at).getTime()
  • updatedAt: new Date(row.updated_at).getTime()
  • clientName: row.client

ProcessGroup → process_groups

TypeScript (ProcessGroup) PostgreSQL (process_groups)
id id
name name
createdAt (number ms) created_at (timestamptz)
updatedAt (number ms) updated_at (timestamptz)

AppSettings → app_settings

TypeScript (AppSettings) PostgreSQL (app_settings)
id ('singleton') id
groupLabel group_label

Nuevos repositorios — estructura de archivos

Crear src/persistence/supabase/ con:

src/persistence/supabase/
├── process-repo.ts
├── group-repo.ts
└── settings-repo.ts

Cada archivo importa supabase de src/lib/supabase.ts y expone una interfaz idéntica al repo Dexie correspondiente, con la firma extendida donde sea necesario para userId.

src/persistence/supabase/process-repo.ts

// Contrato (misma interface que el repo Dexie, más userId donde aplica)
export const supabaseProcessRepo = {
  async save(process: Process, userId: string): Promise<void>
  async getById(id: string): Promise<Process | undefined>
  async getAll(): Promise<Process[]>
  async delete(id: string): Promise<void>  // ver nota sobre estado híbrido
}

Nota sobre delete en estado híbrido: La tabla processes está en Supabase. Las tablas activities, gateways, resources, simulations todavía están en Dexie. El delete debe:

  1. Eliminar el proceso de Supabase
  2. Importar db de src/persistence/db.ts y limpiar las tablas Dexie relacionadas

Es feo pero temporal. Dejar un comentario // TODO Sprint 4 Etapa 3-4: eliminar limpieza Dexie cuando actividades migren a Supabase.

src/persistence/supabase/group-repo.ts

export const supabaseGroupRepo = {
  async save(group: ProcessGroup, userId: string): Promise<void>
  async getAll(): Promise<ProcessGroup[]>
  async getById(id: string): Promise<ProcessGroup | undefined>
  async delete(id: string): Promise<void>  // debe hacer SET NULL en processes.group_id (la FK ya tiene ON DELETE SET NULL)
}

src/persistence/supabase/settings-repo.ts

export const supabaseSettingsRepo = {
  async get(): Promise<AppSettings>
  async update(updates: Partial<Omit<AppSettings, 'id'>>): Promise<void>
}

Actualización de stores

Los stores Zustand llaman a los repos. Con el cambio a Supabase, los repos de escritura necesitan el userId. Los stores obtienen el usuario actual del AuthContext.

Patrón recomendado en los stores:

// En el store, acceder al userId desde el contexto de auth
// Los stores son módulos, no React components — no pueden usar hooks directamente.
// Opciones:
// A) Pasar userId como parámetro a las acciones del store que lo necesitan
// B) Leer el usuario del AuthContext en el componente que llama al store y pasarlo
// C) Usar un getter del store de auth

// Opción recomendada: exponer un authStore simple con el userId,
// o pasar userId como parámetro a las acciones de escritura del store.

Claude Code debe elegir el patrón más limpio dado el código existente de los stores. La regla es: ningún componente llama al repo directamente — todo pasa por el store.


Criterios de validación — esta etapa está completa cuando

  • npm run build sin errores TypeScript
  • npm run test — 561 tests siguen verdes (los tests de dominio no tocan los repos)
  • En la biblioteca se pueden crear y listar procesos (datos vienen de Supabase, no de Dexie)
  • En Supabase Dashboard → Table Editor → processes: aparecen los procesos creados
  • En Supabase Dashboard → Table Editor → processes: las columnas owner_id, created_by, updated_by tienen el UUID del usuario que los creó
  • Crear un grupo de biblioteca → aparece en Supabase tabla process_groups
  • Setting groupLabel → aparece/actualiza en Supabase tabla app_settings
  • Eliminar un proceso → desaparece de Supabase y sus actividades/gateways/resources/simulations desaparecen de Dexie
  • No hay imports directos de la tabla Dexie processes/groups/settings fuera de db.ts (los repos Supabase no usan esas tablas)
  • Validación visual del director: biblioteca cargando procesos desde Supabase

Reporte esperado al completar

  1. Lista de archivos creados/modificados
  2. Resultado de npm run test (número de tests, pass/fail)
  3. Resultado de npm run build
  4. Captura de pantalla de la biblioteca con al menos un proceso cargado
  5. Screenshot de Supabase Table Editor mostrando processes con owner_id populado
  6. Descripción de cómo se resolvió el patrón userId en los stores
  7. Cualquier decisión de iniciativa fuera del scope (clasificar nivel 1/2/3)

Referencia de archivos existentes

Archivo Relevancia
src/persistence/db.ts Schema Dexie actual — referencia para entender el modelo
src/persistence/repositories.ts Repos Dexie actuales — base para los nuevos repos Supabase
src/domain/types.ts Tipos TypeScript — NO modificar
src/store/ Stores Zustand a actualizar
src/lib/supabase.ts Cliente Supabase singleton
src/auth/AuthContext.tsx Fuente del userId actual
supabase/migrations/001_create_users.sql Referencia de estilo para los nuevos scripts

Notas arquitectónicas

Estado híbrido es intencional

Al terminar esta etapa, la app tiene:

  • Procesos, grupos, settings → Supabase PostgreSQL
  • Actividades, gateways, recursos, simulaciones → Dexie (hasta Etapa 3-4)

Esto es correcto. No anticipar la migración de las tablas restantes.

Los tests de dominio no se rompen

Los tests en src/domain/ son funciones puras sin persistencia. No dependen de Dexie ni de Supabase. Deben seguir verdes sin cambios.

Los tests de integración que usan los repos directamente (si los hay) pueden necesitar mocks de Supabase — evaluar caso por caso.

Supabase genera updated_at automáticamente

Para que updated_at se actualice automáticamente en cada UPDATE, se puede agregar un trigger PostgreSQL, o simplemente incluir updated_at: new Date().toISOString() en cada UPDATE desde el repo. Usar el approach del repo (más simple, sin trigger en esta etapa).