# Sprint 4 — BRIEF

> **Naturaleza del sprint:** refactorización arquitectural mayor.
> No hay features de producto nuevas visibles. El objetivo es habilitar colaboración multi-usuario sobre una base correcta.
> Este sprint es una excepción justificada al rebalanceo web/PDF/infra — es 100% infraestructura.

---

## Contexto y motivación

InQ ROI nació como herramienta single-user con persistencia local (IndexedDB/Dexie). El equipo de InQuality lo usa actualmente de forma individual y quiere pasar a un modelo colaborativo donde:

- Múltiples consultores pueden trabajar sobre procesos compartidos
- Existe identidad de usuario (quién creó, quién actualizó)
- Algunos elementos son privados del consultor, otros compartidos con el equipo
- Los recursos son un catálogo global del equipo, no duplicados por proceso

Para esto se requiere reemplazar IndexedDB con una base de datos centralizada, agregar autenticación, y rediseñar el modelo de datos de recursos.

**Dato importante:** no hay datos en producción que valgan la pena migrar. Se abandona IndexedDB limpiamente y se empieza de cero en la nueva base de datos.

---

## Decisiones estratégicas — NO reabrir en esta sprint

### Auth: Supabase ahora, Keycloak después

Se usa **Supabase** como BaaS para Sprint 4: auth + PostgreSQL + RLS. La razón es velocidad de implementación.

El código debe construir una **interfaz `AuthService`** que abstraiga el proveedor de autenticación. El objetivo es que la migración futura a Keycloak (servidor de identidad InQuality para múltiples productos) sea cambio de implementación, no reescritura de la app.

InQuality tiene infraestructura VPS (desa + prod) y la visión es Keycloak self-hosted como plataforma de identidad para todos sus productos. Eso es trabajo de un sprint futuro.

### Modelo de plataforma: InQuality como operador (Modelo A)

InQ ROI es una **plataforma de consultoría operada por InQuality**, no un SaaS peer-to-peer. InQuality controla quién entra, a qué tiene acceso, y qué puede hacer. Los clientes y consultores externos son guests invitados por InQuality.

Esto se implementa con un campo `platform_role` en la tabla de usuarios:

- `platform_admin` → staff InQuality, puede ver todos los procesos de todos los usuarios/grupos
- `member` → usuario estándar (consultor InQuality sin acceso global, o reservado para futuro)
- `guest` → cliente externo o consultor externo, solo ve lo que se le comparte explícitamente

En Sprint 4, todos los usuarios de InQuality reciben `platform_admin`. El rol `guest` se usa cuando se agregue el primer cliente externo.

**Migración a Modelo B (SaaS peer-to-peer) — posible, no planificada.**
Cuando InQuality decida que cada organización cliente debe ser autónoma (administrar sus propios usuarios, sin acceso implícito de InQuality), la migración consiste en: eliminar el bypass de `platform_admin`, agregar self-service de org, y cambiar las políticas RLS. El schema de Sprint 4 ya lo soporta sin reestructurar datos.

### Auth: providers múltiples según rol

- **Staff InQuality** (`platform_admin`): Google Workspace `@inquality.com.py`. Solo este dominio puede obtener `platform_admin`.
- **Guests** (clientes, consultores externos): cualquier email. Magic link o email+contraseña. No requieren Google Workspace.

Supabase soporta múltiples providers simultáneamente. En Sprint 4 solo se implementa Google OAuth para staff. La infraestructura para guest auth (magic link) se deja preparada pero no activada hasta que haya un cliente real.

### Online-only

Se elimina el requerimiento de offline. La herramienta requiere conectividad. Offline + sync se agrega al roadmap futuro.

### Sin auditoría en Sprint 4

La auditoría de cambios (quién cambió qué campo y cuándo) se difiere a un sprint dedicado. En Sprint 4 solo se captura `owner_id`, `created_by`, `updated_by`, `created_at`, `updated_at` en las tablas principales — suficiente para mostrar identidad en la UI sin el costo de instrumentar un audit log completo.

---

## Modelo de datos objetivo

### Entidades centrales y sus relaciones

El modelo tiene tres entidades de identidad/acceso y las entidades de negocio del producto. La separación es importante: `client` en un proceso es un atributo de texto, **no** una entidad con acceso al sistema.

```
Usuarios (identidad)
  ↓ pertenecen a
Grupos de usuario (agrupación de acceso)
  ↓ se usan en
Acceso a procesos (tabla de permisos)
  ↓ determina quién ve
Procesos → Actividades → Simulaciones (entidades de negocio)
           ↑
Recursos (catálogo global, sin process_id)
```

### Usuarios

La tabla de usuarios combina Supabase Auth (fuente de verdad para credenciales) con una tabla `users` en la DB de la app para atributos de la plataforma.

```
users
  id               uuid PK (mismo UUID que auth.users de Supabase)
  email            text NOT NULL
  name             text
  avatar_url       text
  platform_role    'platform_admin' | 'member' | 'guest'
  created_at       timestamptz DEFAULT now()
```

**Reglas de `platform_role`:**
- `platform_admin` → staff InQuality. Bypass de todos los controles de acceso: ve y puede editar todos los procesos. Solo usuarios con dominio `@inquality.com.py` obtienen este rol.
- `member` → usuario estándar. Ve solo lo que se le comparte explícitamente. Reservado para consultores InQuality sin acceso global o para uso futuro.
- `guest` → cliente externo o consultor externo. Idéntico a `member` en permisos. Se usa para distinguir visualmente en la UI quién es externo.

En Sprint 4, todos los usuarios de InQuality reciben `platform_admin`. El rol `guest` entra cuando llegue el primer cliente externo.

**El campo `platform_role` nunca hardcodea "inquality".** Si en el futuro InQuality decide operar el Modelo B (cada org autónoma), el bypass se elimina cambiando las políticas RLS — el campo permanece genérico.

### Grupos de usuario

Permiten compartir un proceso con un conjunto de personas sin listar a cada una individualmente. Útil para "todos los del cliente Acme" o "equipo de consultoría junior".

```
user_groups
  id               uuid PK
  name             text NOT NULL
  created_by       uuid → users.id
  created_at       timestamptz DEFAULT now()

group_memberships
  user_id          uuid → users.id
  group_id         uuid → user_groups.id
  role             'owner' | 'member'
  PRIMARY KEY (user_id, group_id)
```

### Acceso a procesos

Reemplaza el campo `visibility: 'private' | 'team'` del modelo anterior. El acceso es explícito: el owner de un proceso elige con quién compartirlo (usuarios individuales o grupos).

```
process_access
  process_id       uuid → processes.id
  grantee_type     'user' | 'group'
  grantee_id       uuid  (→ users.id si 'user', → user_groups.id si 'group')
  permission       'view' | 'edit'
  PRIMARY KEY (process_id, grantee_type, grantee_id)
```

Un proceso sin entradas en `process_access` es privado del owner. Un proceso compartido con el grupo "Equipo InQuality" es efectivamente de visibilidad de equipo. La tabla `process_access` es suficientemente flexible para Modelo A y Modelo B sin reestructurar.

### Procesos

```
processes
  id          uuid PK
  name        text
  client      text          ← atributo descriptivo, NO entidad con acceso
  currency    text
  frequency   numeric
  bpmn_xml    text
  owner_id    uuid → users.id
  created_by  uuid → users.id
  updated_by  uuid → users.id
  created_at  timestamptz
  updated_at  timestamptz
```

Sin campo `visibility`. El acceso se determina completamente por `process_access` y por `platform_role` del usuario.

### Recursos — catálogo global

Este es el cambio más significativo de modelo. Los recursos dejan de ser por proceso y pasan a ser un catálogo global del equipo.

```
resources
  id             uuid PK
  name           text
  type           'human' | 'system' | 'tool'
  cost_per_hour  numeric
  created_by     uuid → users.id
  updated_by     uuid → users.id
  created_at     timestamptz
  updated_at     timestamptz
```

Sin `process_id`. Sin control de acceso propio — los recursos son siempre visibles para todos los usuarios autenticados. Cualquier consultor puede crear, ver y editar recursos del catálogo.

Las asignaciones de recursos a actividades (`activity_resource_assignments`) referencian el `resource_id` del catálogo global.

### Actividades

Sin cambio de modelo respecto a Sprint 3. Se agrega `created_by`, `updated_by`, `created_at`, `updated_at`.

### Simulaciones

No tienen control de acceso propio — la regla de acceso se deriva del proceso al que pertenecen.

### Grupos de procesos y tags

Compartidos por todo el equipo. Sin campo de acceso. Sin `owner_id`.

---

## Interfaz AuthService — contrato obligatorio

Claude Code debe definir y respetar esta interfaz. No llamar a Supabase directamente desde componentes o stores.

```typescript
interface AuthUser {
  id: string
  email: string
  name: string
  avatarUrl?: string
}

interface AuthService {
  signInWithGoogle(): Promise<void>
  signOut(): Promise<void>
  getCurrentUser(): Promise<AuthUser | null>
  onAuthStateChange(callback: (user: AuthUser | null) => void): () => void
}
```

La implementación `SupabaseAuthService` implementa esta interfaz y es la única que toca Supabase directamente. Cuando llegue Keycloak, se escribe `KeycloakAuthService` que implementa la misma interfaz. El resto de la app no cambia.

---

## Capa de repositorios — contrato obligatorio

Actualmente existe `src/persistence/repositories.ts` con Dexie. Se reemplaza por implementaciones Supabase manteniendo el mismo patrón de repositorios.

```typescript
// El contrato (interface) no cambia, la implementación sí
interface ProcessRepository {
  getAll(): Promise<Process[]>
  getById(id: string): Promise<Process | undefined>
  save(process: Process): Promise<void>
  delete(id: string): Promise<void>
  // ...
}
```

Los stores de Zustand llaman al repositorio. Los repositorios llaman a Supabase. Ningún componente toca Supabase directamente.

---

## Row-Level Security (RLS)

Las políticas de RLS de Supabase deben ser equivalentes a políticas PostgreSQL estándar. No usar features exclusivas de Supabase que no puedan recrearse en un PostgreSQL standalone cuando se migre a VPS.

### Helper function (crear una vez)

```sql
-- Retorna true si el usuario actual es platform_admin
CREATE OR REPLACE FUNCTION is_platform_admin()
RETURNS boolean AS $$
  SELECT EXISTS (
    SELECT 1 FROM users
    WHERE id = auth.uid() AND platform_role = 'platform_admin'
  );
$$ LANGUAGE sql SECURITY DEFINER;
```

### Políticas para `processes`

```sql
-- Ver: platform_admin ve todo | owner ve los suyos | acceso explícito
CREATE POLICY "select_processes" ON processes FOR SELECT
  USING (
    is_platform_admin()
    OR owner_id = auth.uid()
    OR EXISTS (
      SELECT 1 FROM process_access
      WHERE process_id = processes.id
        AND (
          (grantee_type = 'user'  AND grantee_id = auth.uid())
          OR
          (grantee_type = 'group' AND grantee_id IN (
            SELECT group_id FROM group_memberships WHERE user_id = auth.uid()
          ))
        )
    )
  );

-- Insertar: cualquier usuario autenticado
CREATE POLICY "insert_processes" ON processes FOR INSERT
  WITH CHECK (auth.uid() IS NOT NULL);

-- Actualizar: platform_admin | owner | quien tenga permiso 'edit'
CREATE POLICY "update_processes" ON processes FOR UPDATE
  USING (
    is_platform_admin()
    OR owner_id = auth.uid()
    OR EXISTS (
      SELECT 1 FROM process_access
      WHERE process_id = processes.id
        AND permission = 'edit'
        AND (
          (grantee_type = 'user'  AND grantee_id = auth.uid())
          OR
          (grantee_type = 'group' AND grantee_id IN (
            SELECT group_id FROM group_memberships WHERE user_id = auth.uid()
          ))
        )
    )
  );

-- Eliminar: solo el owner (platform_admin puede, por ser owner o por override)
CREATE POLICY "delete_processes" ON processes FOR DELETE
  USING (is_platform_admin() OR owner_id = auth.uid());
```

### Políticas para `resources` (catálogo global)

```sql
-- Todos los autenticados pueden leer y escribir
CREATE POLICY "all_resources" ON resources
  USING (auth.uid() IS NOT NULL)
  WITH CHECK (auth.uid() IS NOT NULL);
```

### Políticas para `users`, `user_groups`, `group_memberships`, `process_access`

```sql
-- users: cualquier autenticado puede leer (para mostrar nombres), solo el propio usuario puede actualizar
CREATE POLICY "read_users" ON users FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "update_own_user" ON users FOR UPDATE USING (id = auth.uid());

-- user_groups: todos los autenticados leen; platform_admin o owner del grupo puede modificar
CREATE POLICY "read_groups" ON user_groups FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "manage_groups" ON user_groups FOR ALL
  USING (is_platform_admin() OR created_by = auth.uid());

-- group_memberships: todos los autenticados leen; platform_admin o owner del grupo gestiona
CREATE POLICY "read_memberships" ON group_memberships FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "manage_memberships" ON group_memberships FOR ALL
  USING (
    is_platform_admin()
    OR EXISTS (
      SELECT 1 FROM user_groups
      WHERE id = group_memberships.group_id AND created_by = auth.uid()
    )
  );

-- process_access: owner del proceso o platform_admin gestiona los accesos
CREATE POLICY "manage_process_access" ON process_access FOR ALL
  USING (
    is_platform_admin()
    OR EXISTS (
      SELECT 1 FROM processes WHERE id = process_access.process_id AND owner_id = auth.uid()
    )
  );
CREATE POLICY "read_process_access" ON process_access FOR SELECT
  USING (auth.uid() IS NOT NULL);
```

### Nota sobre migración a Modelo B

Para migrar de Modelo A a Modelo B (orgs autónomas), los únicos cambios son:
1. Eliminar o condicionar el bypass de `is_platform_admin()` en las políticas
2. Agregar tabla `organizations` y `org_memberships`
3. Ajustar las políticas para que el scope sea `org_id` en lugar de plataforma global

El schema de datos no cambia. Las tablas `user_groups`, `group_memberships` y `process_access` son compatibles con ambos modelos.

---

## UX — cambios de superficie

### Login

Pantalla de login antes de cualquier contenido. Dos flujos:

**Staff InQuality** → Botón "Iniciar sesión con Google". Solo cuentas `@inquality.com.py` obtienen acceso. Al autenticarse, la app verifica el dominio y asigna `platform_role = 'platform_admin'` si no existe registro previo en la tabla `users`.

**Guests** (clientes, consultores externos) → En Sprint 4 este flujo no está activado. La infraestructura (campo `platform_role = 'guest'`, tabla `users`) se construye lista para habilitarlo. El botón de magic link / email no aparece en la UI de Sprint 4. Se activa cuando haya un cliente real que incorporar.

Al completar login, la app redirige al workspace. Sin login, cualquier ruta protegida redirige a `/login`.

### Biblioteca (LibraryPage)

- Toggle de filtro: "Todos" / "Míos"
- Badge o ícono que indica visibilidad (privado 🔒 / compartido)
- En cada tarjeta de proceso: "Actualizado por [nombre]" + timestamp relativo
- Botón para cambiar visibilidad de un proceso propio

### ResourcesPanel (en workspace)

El panel de recursos ya no es "crear recursos para este proceso". Pasa a ser "seleccionar desde catálogo global" + "agregar nuevo al catálogo". El catálogo vive en una nueva sección de la app (Settings o panel dedicado).

### AppHeader

Mostrar avatar / nombre del usuario logueado. Botón de logout.

---

## Stack — cambios

| Agregado | Propósito |
| --- | --- |
| `@supabase/supabase-js` | Cliente Supabase (auth + db) |
| Variables de entorno `VITE_SUPABASE_URL` y `VITE_SUPABASE_ANON_KEY` | Configuración por entorno |

| Eliminado | Motivo |
| --- | --- |
| `dexie` | Reemplazado por Supabase PostgreSQL |
| `src/persistence/db.ts` | Reemplazado por cliente Supabase |
| Migraciones Dexie (v1-v4) | Ya no aplican |

Las variables de entorno deben existir en `.env.local` (ignorado por git) y documentarse en `.env.example`.

---

## Etapas tentativas

El orden importa — cada etapa habilita la siguiente.

| Etapa | Descripción |
| --- | --- |
| 1 | Supabase setup + AuthService + login page + Google OAuth |
| 2 | Schema PostgreSQL + migración de repositorios (procesos + grupos + tags) |
| 3 | Migración de actividades + recursos globales (nuevo modelo) |
| 4 | Migración de simulaciones + limpieza de Dexie |
| 5 | Modelo de privacidad: RLS + UI de visibilidad en biblioteca |
| 6 | Identidad en UI: header con usuario + "actualizado por" en biblioteca y workspace |
| 7 | ResourcesPanel: UX de catálogo global |
| 8 | Tests de integración + polish + validación E2E de auth |

Cada etapa tiene su propio `ETAPA_N_PROMPT.md` antes de arrancar. No improvisar orden ni scope entre etapas.

---

## Criterios de aceptación del sprint

- [ ] Login con Google Workspace (`@inquality.com.py`) funciona en desa y prod
- [ ] Ruta protegida — sin login no se accede a nada de la app
- [ ] Procesos se guardan y recuperan de Supabase (no de IndexedDB)
- [ ] Control de acceso funciona — un proceso sin `process_access` no lo ve otro usuario (salvo `platform_admin`)
- [ ] Recursos son globales — visibles para todos los usuarios autenticados
- [ ] En la biblioteca aparece "Actualizado por [nombre]" en cada proceso
- [ ] `AuthService` es una interface con implementación `SupabaseAuthService`
- [ ] No hay llamadas directas a Supabase fuera de repositorios y `AuthService`
- [ ] Dexie eliminado del proyecto (no solo sin uso — removido del package.json)
- [ ] Build limpio sin errores TypeScript
- [ ] Tests de dominio previos siguen verdes (simulación, ROI, PDF)
- [ ] Validación visual del director en cada etapa con output visual

---

## Riesgos y mitigaciones

**Google Workspace OAuth requiere configuración en Google Cloud Console.**
Acción: crear el proyecto OAuth antes de que Claude Code escriba una línea de código. El `client_id` y `client_secret` tienen que estar disponibles. Se configura también en Supabase Dashboard → Auth → Providers.

**El cambio de recursos globales rompe el UX actual completamente.**
Acción: el prompt de Etapa 7 (ResourcesPanel) debe incluir un mockup del nuevo UX aprobado antes de implementar. Aplicar Patrón C.5 (mockup como contrato visual).

**Múltiples etapas tienen riesgo de regresión en simulación/PDF.**
Acción: correr `npm run test` al final de cada etapa y verificar que los 561 tests previos siguen verdes. Si baja el count, parar y reportar antes de continuar.

---

## Fuera del scope de Sprint 4 — explícito

- Auditoría de cambios (audit log) — sprint dedicado futuro
- Keycloak / migración de auth provider — sprint futuro
- Offline mode / sync — roadmap
- Acceso de clientes externos — Fase 2
- Versionado de procesos — Fase 2
- Export a Excel — BACKLOG sin sprint
- Cualquier feature de PDF o simulación nueva

---

## Referencias

| Recurso | Propósito |
| --- | --- |
| `simulador-web/CLAUDE.md` | Reglas de implementación para Claude Code |
| `simulador-web/src/persistence/` | Código Dexie actual a reemplazar |
| `simulador-web/src/store/` | Stores Zustand que llaman a repositorios |
| `simulador-web/src/domain/types.ts` | Tipos de dominio (base del schema PostgreSQL) |
| `methodology/PATTERNS.md` | C.5 (mockup como contrato), C.7 (git diff preventivo) |
| `methodology/ANTI_PATTERNS.md` | AP.8 (adelanto silencioso) |
| Supabase docs — RLS | https://supabase.com/docs/guides/auth/row-level-security |
| Supabase docs — Google OAuth | https://supabase.com/docs/guides/auth/social-login/auth-google |
