# Plan de construcción paso a paso — Plataforma SaaS de reportes multi-tenant

**Documento base:** `especificacion_tecnica_saas_reportes.md` (v3.0). Este plan no repite las reglas de negocio ni los esquemas ya definidos allí — los referencia por número de sección. Este documento responde una pregunta distinta: **en qué orden exacto construir cada pieza, y cómo verificar que cada una funciona antes de pasar a la siguiente.**

**Regla de ejecución para la IA que construya esto:** no avanzar a la fase siguiente si el "punto de verificación" de la fase actual no pasa. Cada fase depende de que la anterior esté funcionando, no solo escrita.

---

## Fase 0 — Preparación del entorno

**Objetivo:** tener el proyecto listo para empezar a escribir código, antes de tocar ninguna pantalla o endpoint.

1. Crear la estructura de carpetas completa descrita en la sección 6 de la especificación (todas las carpetas vacías primero, incluso las que se llenarán después: `Auth/`, `Tenants/`, `Connections/`, `SchemaIntrospection/`, `QueryEngine/`, `Reports/`, `RBAC/`, `Dashboard/`, `Audit/`, `Common/Middleware/`).
2. Crear `composer.json` en `/public_html/api/` con las dos únicas dependencias externas permitidas:
   ```json
   {
     "require": {
       "phpoffice/phpspreadsheet": "^1.29",
       "phpmailer/phpmailer": "^6.9"
     }
   }
   ```
   Ejecutar `composer install` (localmente; el directorio `vendor/` resultante se sube tal cual al servidor cPanel, ya que Composer no siempre está disponible ahí).
3. Crear `.env.example` con todas las variables que se necesitarán (sin valores reales):
   ```
   DB_PLATAFORMA_HOST=localhost
   DB_PLATAFORMA_NOMBRE=
   DB_PLATAFORMA_USUARIO=
   DB_PLATAFORMA_PASSWORD=
   CLAVE_MAESTRA_CIFRADO=
   MYSQL_ADMIN_HOST=localhost
   MYSQL_ADMIN_USUARIO=
   MYSQL_ADMIN_PASSWORD=
   SMTP_HOST=
   SMTP_PUERTO=587
   SMTP_USUARIO=
   SMTP_PASSWORD=
   URL_BASE_APP=
   ```
   Copiar a `.env` y llenar con valores reales de desarrollo. `CLAVE_MAESTRA_CIFRADO` se genera una sola vez con `bin2hex(random_bytes(32))` y nunca cambia después (cambiarla invalidaría todas las credenciales ya cifradas).
   `MYSQL_ADMIN_USUARIO`/`PASSWORD` son credenciales administrativas de MySQL (o de la cuenta cPanel) usadas **solo** para crear los usuarios de solo lectura de conexiones locales (sección 8.2) — nunca se usan para nada más y nunca se exponen a través de la API.
4. Crear `.htaccess` en `/public_html/` con la reescritura de `/api/*` hacia `api/index.php`, y bloqueo explícito de acceso directo a `/api/config/`, `/api/src/`, `.env` y `vendor/` desde el navegador.
5. Crear `api/config/env.php` (carga `.env` a variables de PHP) y dejarlo probado con un script temporal que solo imprima `"OK"` si logra leer `DB_PLATAFORMA_HOST` — borrar ese script antes de continuar.

**Punto de verificación:** visitar la URL del proyecto en el navegador no debe dar error 500 de configuración, y un intento de acceder directo a `https://dominio/.env` debe devolver 403.

---

## Fase 1 — Base de datos de la plataforma

**Objetivo:** la base de datos existe y está lista, antes de escribir una sola línea de PHP que la use.

1. Crear la base de datos de la plataforma en cPanel (vía "Bases de datos MySQL").
2. Ejecutar el DDL completo de la sección 5 de la especificación, **en el orden exacto indicado ahí**: `tenants` → `roles` → `users` → `tenant_connections` → `reports` → `query_execution_log` → `sessions` → `auth_tokens` → `platform_admins`.
3. Insertar manualmente (una sola vez, vía phpMyAdmin) el primer tenant de prueba y su primer super-admin de plataforma:
   ```sql
   INSERT INTO tenants (nombre, slug, estado) VALUES ('Tenant de prueba', 'prueba', 'activo');
   INSERT INTO roles (tenant_id, nombre, nivel, es_rol_por_defecto) VALUES (1, 'Administrador', 100, TRUE);
   -- El password_hash de abajo corresponde a la contraseña "Prueba1234" solo para desarrollo local
   INSERT INTO users (tenant_id, nombre_completo, email, password_hash, rol_id, es_admin_tenant, estado)
     VALUES (1, 'Admin de prueba', 'admin@prueba.local', '$2y$10$...', 1, TRUE, 'activo');
   INSERT INTO platform_admins (user_id) VALUES (1);
   ```
   Este usuario de prueba es exclusivamente para verificar cada fase siguiente con `curl` o Postman; no debe existir en el entorno de producción real.

**Punto de verificación:** `SELECT * FROM tenants; SELECT * FROM users; SELECT * FROM roles;` devuelven las filas esperadas y todas las llaves foráneas se crearon sin error (revisar con `SHOW CREATE TABLE reports;` que las 3 FK de esa tabla existan).

---

## Fase 2 — Núcleo común del backend

**Objetivo:** las piezas que todos los endpoints van a reutilizar, construidas y probadas antes que el primer endpoint real.

**Orden de implementación:**
1. `api/config/database.php` — construye y expone el PDO de la plataforma (una sola conexión reutilizada, con `PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION`).
2. `api/src/Common/Response.php` — funciones `exito($datos)` y `error($codigo, $mensaje, $httpStatus)` que arman el JSON estándar de la sección 7 y fijan el `http_response_code`.
3. `api/src/Common/Request.php` — helper para leer el body JSON de la petición (`json_decode(file_get_contents('php://input'), true)`) y los parámetros de query string.
4. `api/src/Common/Validator.php` — implementa la tabla de convenciones de validación de la sección 7.0 como funciones reutilizables (`validarSlug()`, `validarEmail()`, `validarPassword()`, etc.), cada una devolviendo `true` o un mensaje de error específico.
5. `api/index.php` — router central: lee el método HTTP y el path después de `/api/`, y despacha a un controlador. Para el MVP, un `switch`/`match` explícito por ruta es preferible a una librería de ruteo externa (se evita una dependencia más).

**Punto de verificación:** crear temporalmente una ruta de prueba `GET /api/ping` que solo llame a `Response::exito(['mensaje' => 'pong'])` y confirmar con `curl` que responde `{"exito":true,"datos":{"mensaje":"pong"},"error":null}`. Eliminar esta ruta antes de la Fase 3.

---

## Fase 3 — Autenticación (sección 7.2 a 7.9)

**Por qué va antes que cualquier otra cosa:** todo lo demás requiere una sesión autenticada para probarse.

**Orden de implementación (cada endpoint se construye y se prueba con `curl` antes de pasar al siguiente):**

1. **`SessionManager.php`** — crear/leer/destruir filas de `sessions`, fijar y borrar la cookie httpOnly (sección 3.2). Probarlo con un script temporal que cree una sesión falsa para el usuario de prueba de la Fase 1 y confirme que la cookie llega en la respuesta HTTP.
2. **`TokenManager.php`** — generar tokens aleatorios, calcular su hash SHA-256, insertarlos/validarlos/marcarlos usados en `auth_tokens`.
3. **`POST /api/auth/registro-tenant`** (7.2) — probar con `curl` un registro nuevo; verificar en la base que se crearon las 4 filas (`tenants`, `roles`, `users`, `auth_tokens`) y que llegó el correo (en desarrollo, configurar PHPMailer para escribir a un buzón de pruebas tipo Mailtrap, nunca a un correo real).
4. **`GET /api/auth/verificar-correo`** (7.3) — usar el token recibido en el correo de prueba, confirmar que `tenants.estado` y `users.estado` cambian a `activo`.
5. **`POST /api/auth/login`** (7.4) — probar con el usuario recién verificado; confirmar que la cookie llega y que hay una fila nueva en `sessions`.
6. **`GET /api/auth/yo`** (7.6) y **`POST /api/auth/logout`** (7.5) — probar en secuencia con la cookie obtenida en el paso anterior.
7. **`POST /api/auth/recuperar-password`** (7.7) y **`POST /api/auth/restablecer-password`** (7.8) — probar el ciclo completo con el usuario de prueba.
8. **`POST /api/auth/aceptar-invitacion`** (7.9) — este requiere que exista un usuario en estado `invitado` con un token tipo `invitacion`; se puede simular insertando ambas filas manualmente en la base para esta prueba puntual (el endpoint real de invitación se construye en la Fase 6).

**Punto de verificación de toda la fase:** un script de `curl` que ejecute, en orden, registro → verificación → login → `/auth/yo` → logout, contra un tenant y usuario nuevos generados en el momento (no el de prueba fijo de la Fase 1), sin ningún error.

---

## Fase 4 — Middlewares de autorización

**Objetivo:** centralizar las verificaciones que se repiten en casi todos los endpoints siguientes, para no reimplementarlas endpoint por endpoint.

1. **`RequireAuth.php`** — lee la cookie, busca la sesión en `sessions`, verifica `expira_en > NOW()`; si falla, responde `SESION_EXPIRADA` (401) y detiene la ejecución antes de llegar al controlador.
2. **`RequireTenantMatch.php`** — además de lo anterior, verifica que `tenants.estado='activo'` para el tenant de la sesión; si no, `TENANT_SUSPENDIDO` (403) — esto cubre el caso 12.10 de la especificación (sesión válida pero tenant recién suspendido).
3. **`RequireRole.php`** — recibe un parámetro (`admin_tenant`) y verifica `users.es_admin_tenant=true` para el usuario de la sesión; si no, un 403 genérico de permisos insuficientes.
4. **`RequireSuperAdmin.php`** — verifica que exista una fila en `platform_admins` para el usuario de la sesión, independientemente de cualquier `tenant_id`.

**Punto de verificación:** aplicar `RequireAuth` a la ruta de prueba `/api/ping` de la Fase 2 (reactivada temporalmente) y confirmar que sin cookie responde 401, y con la cookie de un login válido responde 200. Luego eliminar la ruta de prueba definitivamente.

---

## Fase 5 — Conexiones a bases de datos de tenants (sección 7.11 a 7.15, 8.2, 8.3)

**Por qué va antes que Reportes:** un reporte no puede existir sin una conexión válida a la que apuntar.

**Orden de implementación:**
1. **`ConnectionCrypto.php`** — cifrado/descifrado con `sodium_crypto_secretbox` (8.3). Probar de forma aislada: cifrar un texto, descifrarlo, confirmar que coincide, antes de conectarlo a ningún endpoint.
2. **`POST /api/conexiones`** (7.11) — implementar primero el caso `tipo=remota` (más simple: solo cifra y guarda lo que el usuario envía). Probar con `curl` contra una base MySQL de prueba accesible desde el entorno de desarrollo.
3. **`ConnectionTester.php`** y **`POST /api/conexiones/{id}/probar`** (7.12) — probar contra la conexión remota de prueba del paso anterior, y también forzar un caso de error (host inexistente) para confirmar que el mensaje clasificado de la sección 7.12 se genera correctamente, no un error genérico de PDO sin traducir.
4. **`SchemaReader.php`** y **`GET /api/conexiones/{id}/esquema`** (7.13) — probar contra la misma base de prueba, que debe tener al menos 2-3 tablas con columnas variadas para confirmar que la introspección las lista todas correctamente.
5. Ahora sí, implementar el caso `tipo=local` de `POST /api/conexiones`: el aprovisionamiento automático del usuario MySQL de solo lectura (script de 8.2), ejecutado con las credenciales administrativas de `.env`. Probar creando una base local de prueba en el mismo servidor y confirmando, con un cliente MySQL aparte, que el usuario generado solo tiene `SELECT`.
6. **`GET /api/conexiones`** (7.14) y **`DELETE /api/conexiones/{id}`** (7.15) — implementar y probar el caso de error `CONEXION_EN_USO` intentando borrar una conexión que ya tenga un reporte asociado (esto requiere adelantar levemente la Fase 6 para tener al menos un reporte de prueba, o insertar uno manualmente solo para esta verificación).

**Punto de verificación:** con una conexión de prueba `tipo=remota` guardada y `estado_conexion='exitosa'`, `GET /api/conexiones/{id}/esquema` devuelve una lista de tablas coherente con la base real.

---

## Fase 6 — Motor de consultas (sección 7.18, 8.1)

**Objetivo:** la pieza de seguridad más crítica del sistema, construida y probada de forma exhaustiva antes de exponerla a través del CRUD de reportes.

1. **`QueryValidator.php`** — implementar el algoritmo completo de la sección 8.1, en su orden exacto (eliminar comentarios → verificar que empieza con `SELECT`/`WITH` → rechazar palabras clave de escritura → rechazar múltiples sentencias → rechazar `INTO OUTFILE`/`DUMPFILE`).
2. **Batería de pruebas obligatoria antes de continuar** (usar un script de pruebas unitarias, no solo `curl` manual): confirmar que se **aceptan** consultas `SELECT` normales, `SELECT` con subconsultas, y `WITH ... AS (...) SELECT ...`; y que se **rechazan**: `DELETE FROM x`, `UPDATE x SET...`, `SELECT * FROM x; DROP TABLE x`, `SELECT * FROM x INTO OUTFILE '/tmp/y'`, y el caso del punto 12.5 de la especificación (`SELECT * FROM productos; DROP TABLE productos;--`).
3. **`ParameterBinder.php`** — utilidades para detectar placeholders `:nombre` vía regex y hacer `bindValue()` tipado según la definición del parámetro.
4. **`QueryExecutor.php`** — ejecuta con `PDO::ATTR_TIMEOUT`, `SET SESSION MAX_EXECUTION_TIME`, aplica el `LIMIT 5001`/truncamiento a 5000 de la sección 8.5.
5. **`POST /api/reportes/validar-consulta`** (7.18) — conecta las piezas anteriores contra una conexión real de prueba (de la Fase 5) y confirma que devuelve `columnas_detectadas` y `parametros_detectados` correctos para la consulta de ejemplo de la sección 12.4 de la especificación.

**Punto de verificación:** la batería de pruebas del paso 2 pasa al 100% antes de tocar el CRUD de reportes — este es el único punto de todo el plan donde no se avanza ni con una sola prueba fallando, sin excepción.

---

## Fase 7 — Reportes (sección 7.16, 7.17, 7.19 a 7.22)

1. **`POST /api/reportes`** y **`PUT /api/reportes/{id}`** (7.19) — incluye la validación cruzada placeholder↔parámetro declarado. Probar creando el reporte de ejemplo de la sección 12.4 (ventas por mes con `:fecha_inicio`/`:fecha_fin`).
2. **`GET /api/reportes`** (7.16) — probar el filtro por nivel de acceso con al menos dos usuarios de prueba de niveles distintos (ver caso 12.6 de la especificación).
3. **`GET /api/reportes/{id}`** (7.17) — probar que un usuario no-admin no recibe `sql_consulta` en la respuesta.
4. **`POST /api/reportes/{id}/ejecutar`** (7.20) — esta es la operación más sensible del sistema (conecta cuatro entidades, ver punto 10.5 de la especificación); probar el camino feliz del caso 12.4, y también los casos de error: nivel insuficiente (12.6), parámetro inválido, y timeout forzando artificialmente una consulta lenta (`SELECT SLEEP(15)` bloqueada primero por `QueryValidator`... si no lo bloquea por no ser palabra prohibida, usar en su lugar un `JOIN` cartesiano de prueba que tarde más de 10 segundos en una base de prueba con datos suficientes).
5. **`ReportExporter.php`** y **`GET /api/reportes/{id}/exportar`** (7.21) — probar exportación a CSV primero (más simple), luego a XLSX con PhpSpreadsheet.
6. **`DELETE /api/reportes/{id}`** (7.22) — probar que `query_execution_log.report_id` queda en `NULL` tras el borrado, no se elimina el log.

**Punto de verificación:** repetir literalmente, paso a paso con `curl`, el caso de uso 12.4 completo de la especificación de principio a fin, sin intervención manual en la base de datos entre pasos.

---

## Fase 8 — RBAC: roles y usuarios (sección 7.23, 7.24)

1. **`GET/POST/PUT/DELETE /api/roles`** (7.23) — probar los dos bloqueos de borrado: rol por defecto (`ROL_POR_DEFECTO_NO_ELIMINABLE`) y rol en uso (`ROL_EN_USO`).
2. **`GET /api/usuarios`**, **`POST /api/usuarios/invitar`**, **`PUT /api/usuarios/{id}/estado`**, **`PUT /api/usuarios/{id}/rol`** (7.24) — probar el flujo de invitación real (no simulado como en la Fase 3) de principio a fin: invitar → recibir correo de prueba → aceptar invitación → login. Probar también el caso 12.7 (cambio de rol en caliente): confirmar que una segunda llamada a `GET /api/reportes` con la misma cookie ya refleja el nuevo nivel, sin necesidad de un nuevo login.

**Punto de verificación:** el caso de uso 12.8 (invitación vencida) se puede simular manipulando `expira_en` directamente en la base para forzar el vencimiento sin esperar 7 días reales, y debe responder `TOKEN_INVALIDO_O_EXPIRADO`.

---

## Fase 9 — Dashboard y super-administración (sección 7.10, 7.25)

1. **`GET /api/dashboard/resumen`** (7.10) — probar que respeta el mismo filtro de nivel que `GET /api/reportes`.
2. **`GET /api/plataforma/tenants`** y **`PUT /api/plataforma/tenants/{id}/estado`** (7.25) — probar el caso 12.10 completo: suspender un tenant con una sesión activa de prueba y confirmar que la siguiente petición de esa sesión responde `SESION_EXPIRADA`, y que un login posterior responde `TENANT_SUSPENDIDO`.

**Punto de verificación:** con esto, la totalidad de los 25 endpoints de la sección 7 de la especificación están implementados y probados individualmente. Antes de pasar al frontend, ejecutar de nuevo la batería completa de la Fase 6 (QueryValidator) para confirmar que ningún cambio posterior la rompió.

---

## Fase 10 — Auditoría transversal

**Objetivo:** confirmar, ahora que todos los endpoints existen, que ninguno de los que debía loguear en `query_execution_log` quedó sin hacerlo.

1. Revisar que **todas** las llamadas a `POST /api/reportes/{id}/ejecutar` y `GET /api/reportes/{id}/exportar` insertan su fila correspondiente, exitosa o fallida, **antes** de responder al cliente (sección 8.6) — esto se verifica leyendo el código de `QueryExecutor.php`/`ReportExporter.php`, no solo probando por fuera.
2. Confirmar que ninguna respuesta de ningún endpoint de toda la API incluye, en ningún campo, una contraseña en texto plano ni el contenido de `password_cifrado`/`nonce` — hacer una búsqueda de texto en todo el código (`grep -r "password_cifrado"` en los controladores) para confirmar que esos campos nunca se incluyen en un `Response::exito()`.

**Punto de verificación:** ejecutar 5 reportes de prueba (variando usuarios, algunos forzando error de parámetro inválido) y confirmar con una consulta SQL directa que hay exactamente 5 filas nuevas en `query_execution_log`, ni una menos.

---

## Fase 11 — Frontend

**Por qué va al final:** todo el frontend es un consumidor de una API que ya debe estar completamente funcional y probada; construirlo antes solo generaría retrabajo si un endpoint cambia de forma.

**Orden de implementación (de lo que no requiere sesión a lo que sí):**

1. `assets/js/api.js` — wrapper único de `fetch()` con `credentials:'include'`, manejo centralizado de la forma de error estándar (sección 7), y redirección automática a `#/login` ante cualquier `SESION_EXPIRADA`.
2. `assets/js/app.js` — router de hash mínimo (mapa de rutas → función de render), implementando el mapa de navegación completo de la sección 9.1 de la especificación.
3. Pantallas sin sesión, en este orden: Registro (9.2) → Verificación de correo (9.3) → Login (9.4) → Recuperar contraseña (9.5) → Restablecer contraseña (9.6) → Aceptar invitación (9.7). Probar manualmente en el navegador el flujo del caso 12.1 de principio a fin.
4. Layout general autenticado (9.0): encabezado + barra lateral, antes de construir ninguna pantalla que lo use.
5. Dashboard (9.8) — la primera pantalla que ve cualquier usuario tras el login, conviene tenerla lista para poder navegar visualmente al resto durante el desarrollo.
6. Conexiones: listado (9.10) y wizard (9.9). Probar en el navegador el caso 12.3 completo, incluyendo el estado fallido y su reintento.
7. Reportes: listado (9.11), ejecución (9.12), editor (9.13). Probar visualmente el caso 12.4 completo, confirmando que el gráfico de Chart.js se renderiza correctamente con datos reales.
8. Administración: usuarios (9.14), roles (9.15). Probar visualmente el caso 12.7 (cambio de rol en caliente) refrescando una segunda pestaña.
9. Panel de super-administración (9.16) — construido al final por ser el de menor uso diario.

**Punto de verificación:** un usuario nuevo puede completar en el navegador, sin ayuda de `curl` ni de la base de datos directamente, el ciclo completo: registrarse → verificar correo → login → crear una conexión → crear un reporte → ejecutarlo → ver el resultado → exportarlo a Excel.

---

## Fase 12 — Despliegue en cPanel

1. En "MultiPHP Manager" de cPanel, fijar PHP 8.1 (o superior) para el dominio/subdominio del proyecto.
2. Subir todo `/public_html/` vía FTP/File Manager, **incluyendo** la carpeta `vendor/` ya generada localmente (no ejecutar `composer install` en el servidor si Composer no está disponible ahí).
3. Crear la base de datos de producción y repetir la Fase 1 (DDL completo) contra ella — **nunca** reutilizar datos de prueba de desarrollo en producción.
4. Crear el `.env` de producción con credenciales reales, una `CLAVE_MAESTRA_CIFRADO` nueva generada específicamente para producción (no reutilizar la de desarrollo), y los datos SMTP reales del cPanel.
5. Confirmar, con un cliente de correo real (no Mailtrap), que el flujo de registro envía el correo de verificación correctamente usando el SMTP del propio cPanel.
6. Verificar permisos de archivos: `.env` no debe ser accesible públicamente (probar `https://dominio/.env` y confirmar 403), y la carpeta `api/vendor/` tampoco.
7. Ejecutar manualmente, contra el entorno de producción ya desplegado, el caso de uso 12.1 completo (registro real) con una cuenta de prueba desechable, para confirmar que todo el stack funciona igual que en desarrollo.

**Punto de verificación final:** repasar uno por uno todos los criterios de aceptación de la sección 14 de la especificación contra el entorno de producción ya desplegado, no solo contra el de desarrollo.

---

## Resumen del orden de fases (para referencia rápida)

```
0. Entorno  →  1. Base de datos  →  2. Núcleo común  →  3. Autenticación  →  4. Middlewares
→  5. Conexiones  →  6. Motor de consultas  →  7. Reportes  →  8. RBAC (roles/usuarios)
→  9. Dashboard y super-admin  →  10. Auditoría transversal  →  11. Frontend  →  12. Despliegue
```

Cada flecha representa una dependencia real: no es posible construir Reportes (7) sin Conexiones (5) y el Motor de consultas (6) ya funcionando; no es posible construir el Frontend (11) contra una API que todavía está cambiando de forma en fases anteriores.
