# Migracion final controlada a bases por cliente

La migracion tenant ya no esta limitada a los clientes 1 y 9. Cualquier cliente activo puede provisionarse, migrarse, validarse, activarse y volver a `legacy_shared` con guardas fuertes.

Nada de este flujo borra datos legacy de `mbinvcxp` ni de `_temporalescxp`.

## Flags seguros

Por defecto:

```dotenv
TENANT_DATABASE_MODE_ENABLED=0
TENANT_PROVISIONING_ENABLED=0
TENANT_DATABASE_LEGACY_FALLBACK=1
TENANT_SCHEMA_FILE=database/schema/mbinvcxp_schema.sql
TENANT_SEED_FILE=sql/tenant_base_seed.sql
TENANT_AUDIT_SCHEMA_FILE=sql/audit/001_create_audit_events.sql
```

Reglas de ejecucion:

- `--execute` en migracion/activacion requiere `ALLOW_TENANT_DATA_MIGRATION=1`.
- Provisionamiento real requiere `TENANT_PROVISIONING_ENABLED=1` y `ALLOW_TENANT_DATA_MIGRATION=1`.
- Sin `--execute`, los comandos se mantienen en dry-run.
- Rollback logico solo cambia `tenant_databases.modo` a `legacy_shared`; no borra bases tenant.

## Naming

Los nombres nuevos se generan con:

- `cliente_id` con padding de 3 digitos: `c001`, `c009`, `c011`.
- Nombre del cliente convertido a slug seguro: minusculas, sin tildes, espacios a `_`, solo `[a-z0-9_]`.
- DB operativa: `mbcxp_c{ID}_{slug}`.
- DB auditoria: `_temporalescxp_c{ID}_{slug}`.

Ejemplos:

| Cliente | DB operativa | DB auditoria | Confirm |
| --- | --- | --- | --- |
| `#1 macrobase` | `mbcxp_c001_macrobase` | `_temporalescxp_c001_macrobase` | `macrobase` |
| `#9 Reddit` | `mbcxp_c009_reddit` | `_temporalescxp_c009_reddit` | `reddit` |
| `#11 LA CHIMALTECA` | `mbcxp_c011_la_chimalteca` | `_temporalescxp_c011_la_chimalteca` | `la_chimalteca` |
| `#12 Christian` | `mbcxp_c012_christian` | `_temporalescxp_c012_christian` | `christian` |

Bases ya provisionadas como `mbcxp_cliente_1` y `_temporalescxp_cliente_1` se respetan si ya estan en `tenant_databases`. Este cambio no renombra bases existentes.

## Inicializar metadata

```bash
docker compose exec app php bin/console doctrine:migrations:migrate
docker compose exec app php bin/console app:tenants:init-metadata
```

El comando muestra:

- `cliente_id`
- nombre
- modo
- DB actual
- audit actual
- DB sugerida
- audit sugerida
- confirm esperado

Para un solo cliente:

```bash
docker compose exec app php bin/console app:tenants:init-metadata --cliente=11
```

## Flujo cliente 11

```bash
php bin/console app:tenant:provision --cliente=11 --dry-run
env TENANT_PROVISIONING_ENABLED=1 ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:provision --cliente=11 --execute --confirm=la_chimalteca
php bin/console app:tenant:repair-schema --cliente=11 --dry-run
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:repair-schema --cliente=11 --execute --confirm=la_chimalteca
php bin/console app:tenant:repair-catalogs --cliente=11 --dry-run
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:repair-catalogs --cliente=11 --execute --confirm=la_chimalteca
php bin/console app:tenant:data-migration-plan --cliente=11 --save-plan
php bin/console app:tenant:migrate-data --cliente=11 --dry-run
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:migrate-data --cliente=11 --execute --confirm=la_chimalteca
php bin/console app:tenant:validate-schema --cliente=11
php bin/console app:tenant:validate-migration --cliente=11
php bin/console app:tenant:activate-database --cliente=11 --dry-run
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:activate-database --cliente=11 --execute --confirm=la_chimalteca
```

## Flujo cliente 12

```bash
php bin/console app:tenant:provision --cliente=12 --dry-run
env TENANT_PROVISIONING_ENABLED=1 ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:provision --cliente=12 --execute --confirm=christian
php bin/console app:tenant:repair-schema --cliente=12 --dry-run
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:repair-schema --cliente=12 --execute --confirm=christian
php bin/console app:tenant:repair-catalogs --cliente=12 --dry-run
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:repair-catalogs --cliente=12 --execute --confirm=christian
php bin/console app:tenant:data-migration-plan --cliente=12 --save-plan
php bin/console app:tenant:migrate-data --cliente=12 --dry-run
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:migrate-data --cliente=12 --execute --confirm=christian
php bin/console app:tenant:validate-schema --cliente=12
php bin/console app:tenant:validate-migration --cliente=12
php bin/console app:tenant:activate-database --cliente=12 --dry-run
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:activate-database --cliente=12 --execute --confirm=christian
```

## Schema drift tenant

`doctrine:migrations:migrate` aplica las migraciones sobre la conexion default, es decir `mbinvcxp`. Las bases `database_per_client` ya provisionadas no reciben automaticamente esas migraciones porque son conexiones dinamicas resueltas por cliente en runtime.

Antes de activar o migrar un cliente con base preparada debe ejecutarse:

```bash
php bin/console app:tenant:repair-schema --cliente=ID --dry-run
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:repair-schema --cliente=ID --execute --confirm=slug
php bin/console app:tenant:validate-schema --cliente=ID
```

`repair-schema` compara tablas criticas de `mbinvcxp` contra la base tenant y solo aplica operaciones aditivas: `ADD COLUMN`, `ADD KEY`/`ADD UNIQUE KEY` y `ADD CONSTRAINT` cuando la FK es segura. No ejecuta `DROP`, no borra datos y no toca `mbinvcxp` como destino. FKs que requeririan cambiar datos existentes o nulabilidad se omiten y quedan documentadas en el reporte.

`validate-migration` tambien ejecuta el gate de schema critico; ya no debe considerarse OK solo porque los conteos origen/destino coinciden.

### Reparar todos los tenants de una vez

Ir cliente por cliente despues de cada deploy es facil de olvidar (paso justo lo que causo el error `documentos_cxp.IDX_DOC_CXP_ANIO_MES` tras la migracion `Version20260805170000`). `app:tenant:repair-all` recorre todos los `tenant_databases` con `modo=database_per_client`, `instalado=1` y `activo=1`, y corre `repair-schema` (y por defecto tambien `repair-catalogs`) en cada uno, reportando un resumen al final. Un tenant con error no detiene a los demas.

```bash
# Dry-run: solo diagnostico, no escribe nada.
docker compose exec app php bin/console app:tenant:repair-all

# Ejecuta cambios aditivos reales en todos los tenants elegibles.
docker compose exec -e ALLOW_TENANT_DATA_MIGRATION=1 app php bin/console app:tenant:repair-all --execute

# Solo schema, sin catalogos.
docker compose exec -e ALLOW_TENANT_DATA_MIGRATION=1 app php bin/console app:tenant:repair-all --execute --skip-catalogs

# Reintentar un solo cliente que quedo con error.
docker compose exec -e ALLOW_TENANT_DATA_MIGRATION=1 app php bin/console app:tenant:repair-all --execute --cliente=12
```

Recomendado: agregar el paso de dry-run (o `--execute` si ya se reviso el reporte) inmediatamente despues de `doctrine:migrations:migrate` en el flujo de deploy, para que el drift de schema/catalogos nunca se quede pendiente en silencio.

## Restart seguro

Si un provision falla y deja bases tenant parciales:

```bash
env TENANT_PROVISIONING_ENABLED=1 ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:provision --cliente=11 --execute --confirm=la_chimalteca --restart --confirm-restart=mbcxp_c011_la_chimalteca
```

`--restart` solo permite bases tenant con patron seguro. Nunca permite `mbinvcxp` ni `_temporalescxp`.

## Rollback logico

```bash
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:activate-legacy --cliente=11 --execute --confirm=la_chimalteca
env ALLOW_TENANT_DATA_MIGRATION=1 php bin/console app:tenant:activate-legacy --cliente=12 --execute --confirm=christian
```

Esto vuelve a `legacy_shared` y conserva las bases tenant.

## UI administrativa

Ruta:

```text
/admin/tenants
```

Solo `ROLE_SUPERADMIN`.

La pantalla muestra estado, DB actual, DB sugerida, confirm esperado, ultima migracion y acciones POST con CSRF:

- `Probar`
- `Plan`
- `Provisionar`
- `Validar`
- `Activar DB`
- `Volver legacy`

### Ejecucion desde UI

`/admin/tenants` permite ejecutar el flujo operativo sin entrar por consola para clientes normales.

Acciones en request normal:

- `Probar`: valida conexion.
- `Plan / dry-run`: genera plan y dry-run sin escribir datos tenant.
- `Validar`: encola validacion de schema y migracion.

Acciones background:

- `Preparar base tenant`: encola `tenant_provisioning_jobs.tipo=prepare_base` y ejecuta provision dry-run/execute, repair-schema dry-run/execute, repair-catalogs dry-run/execute y validate-schema. No activa el cliente.
- `Migrar datos`: encola `tenant_provisioning_jobs.tipo=migrate_validate` y ejecuta save-plan, migrate-data dry-run/execute, validate-schema y validate-migration.
- `Activar base tenant`: encola `tenant_provisioning_jobs.tipo=activate_database`; solo ejecuta si validate-schema y validate-migration no tienen errores.
- `Reparar schema`: encola `tenant_provisioning_jobs.tipo=repair_schema`.
- `Reparar catalogos`: encola `tenant_provisioning_jobs.tipo=repair_catalogs`.
- `Rollback legacy`: encola `tenant_provisioning_jobs.tipo=activate_legacy`; no borra bases.

El runner interno reutiliza `TenantDatabaseProvisioner`, `TenantDataMigrator`, `TenantSchemaRepairer`, `TenantCatalogRepairer` y `TenantMigrationGuard`. La UI no duplica la logica de migracion. El backend habilita internamente las flags necesarias solo dentro del runner, despues de validar `ROLE_SUPERADMIN`, CSRF, confirm escrito y ausencia de job activo.

La tabla muestra el workflow, ultimo job, ultima accion, error corto y enlace `Ver log`. Los logs se leen desde `tenant_provisioning_jobs.payload`, `resultado`, `error`, `iniciado_en`, `finalizado_en`, `duracion_segundos` y `ultimo_paso`.

### Worker tenant

Los jobs tenant no dependen de procesos `nohup` lanzados desde PHP-FPM. En produccion se debe levantar el worker explicito:

```bash
docker compose up -d tenant-worker
```

Para procesar un unico job pendiente manualmente:

```bash
docker compose exec app php bin/console app:tenant:jobs:work --once
```

Opciones utiles:

```bash
php bin/console app:tenant:jobs:work --sleep=5 --limit=10 --timeout=1800
php bin/console app:tenant:jobs:work --once --cliente=12
php bin/console app:tenant:jobs:work --once --type=prepare_base
```

`app:tenant:run-job --job=ID` sigue disponible como compatibilidad manual, pero usa el mismo procesador interno del worker.

### Flujo recomendado de produccion

1. Pull del codigo.
2. Restaurar `.env` productivo.
3. Build de imagenes.
4. Ejecutar migraciones.
5. Reparar drift en todos los tenants (ver "Reparar todos los tenants de una vez"):

```bash
docker compose exec app php bin/console app:tenant:repair-all
docker compose exec -e ALLOW_TENANT_DATA_MIGRATION=1 app php bin/console app:tenant:repair-all --execute
```

6. Inicializar metadata:

```bash
docker compose exec app php bin/console app:tenants:init-metadata
```

7. Levantar worker:

```bash
docker compose up -d tenant-worker
```

8. Verificar worker:

```bash
docker compose exec app php bin/console app:tenant:jobs:work --once
```

9. Probar `/admin/tenants`.

### Flujo obligatorio por cliente

1. `provision --dry-run`
2. `provision --execute`
3. `repair-schema --dry-run`
4. `repair-schema --execute`
5. `repair-catalogs --dry-run`
6. `repair-catalogs --execute`
7. `data-migration-plan --save-plan`
8. `migrate-data --dry-run`
9. `migrate-data --execute`
10. `validate-schema`
11. `validate-migration`
12. `activate-database --dry-run`
13. `activate-database --execute` solo con validacion OK

### Cache Symfony y permisos en Docker

En produccion, `var/cache`, `var/log` y `public/uploads` deben quedar escribibles por `www-data`. No ejecutes cache Symfony como root si el contenedor PHP-FPM corre como `www-data`.

Correcto:

```bash
docker compose exec -u www-data app php bin/console cache:clear --env=prod --no-debug
docker compose exec -u www-data app php bin/console cache:warmup --env=prod --no-debug
```

Evita:

```bash
docker compose exec app php bin/console cache:clear
```

Ese comando puede ejecutarse como root segun la imagen/compose y dejar `var/cache/prod` con ownership incorrecto.

Si aparece el error `proxy directory must be writable`:

```bash
docker compose exec -u root app sh -lc '
set -e
rm -rf var/cache/prod
mkdir -p var/cache/prod/doctrine/orm/Proxies var/log public/uploads/adjuntos
chown -R www-data:www-data var/cache var/log public/uploads
chmod -R ug+rwX var/cache var/log public/uploads
'

docker compose exec -u www-data app php bin/console cache:clear --env=prod --no-debug
docker compose exec -u www-data app php bin/console cache:warmup --env=prod --no-debug

docker compose exec -u root app sh -lc '
chown -R www-data:www-data var/cache var/log public/uploads
chmod -R ug+rwX var/cache var/log public/uploads
'
```

El entrypoint actual ejecuta la preparacion minima de runtime incluso con `SKIP_BOOTSTRAP=1`: crea directorios, corrige permisos, valida que `var/cache/prod/doctrine/orm/Proxies` sea escribible por `www-data` y solo despues inicia PHP-FPM.

### Ruido externo `/SDK/webLanguage`

Un request como `GET /SDK/webLanguage` no pertenece al sistema CxP y no hay referencias a esa ruta en plantillas, assets ni comandos del proyecto. Si aparece en logs como 404, tratalo como trafico externo, extension/navegador o scanner; no es la causa del 500 de Symfony.

Para evitar doble ejecucion, antes de encolar una accion background se bloquea si el cliente tiene un job `pendiente` o `procesando`. Mientras exista job activo, la UI deshabilita acciones destructivas.

## Que no hacer

- No ejecutar `DROP DATABASE mbinvcxp`.
- No ejecutar `DROP DATABASE _temporalescxp`.
- No ejecutar `TRUNCATE` ni deletes masivos en legacy.
- No activar `TENANT_DATABASE_MODE_ENABLED=1` antes de validar migracion.
- No ejecutar comandos con `--execute` sin backup.
- No renombrar bases existentes en esta tarea.
