Zentto Migrator — Arquitectura y despliegue
Referencia técnica del migrador de sistemas legacy a Zentto. Repo: zentto-erp/zentto-migrator. CLI Node + TypeScript con arquitectura de núcleo + adaptadores y una UI web para técnicos.
Arquitectura de núcleo + adaptadores
Todo lo que sabe de Zentto (carga idempotente, checkpoints, seed, coerción a la BD viva) vive en src/core/ y no se toca al agregar un sistema origen. Cada ERP legacy es un plugin en src/adapters/<sistema>/.
DatQbox / SQL Server] --> ADP[Adaptador
src/adapters/datqbox] ADP -->|modelo canónico| CORE[Núcleo
pipeline + loaders] CORE -->|upsert idempotente| PG[(Zentto
PostgreSQL tenant)] CORE --> CK[Checkpoints
runs/*.json] TPL[(zentto_dev
empresa 1)] -.seed config.-> CORE style ADP fill:#6C63FF,color:#fff style CORE fill:#10b981,color:#fff style PG fill:#374151,color:#fff style SRC fill:#FF6584,color:#fff
Para migrar desde otro ERP: se implementa la interfaz MigrationSourceAdapter en src/adapters/<sistema>/ (conexión de solo lectura, validación de contrato, extracción → modelo canónico) y se registra en src/adapters/index.ts. El CLI lo expone con --adapter <nombre>. Guía: docs/ADAPTERS.md del repo.
Garantías de datos
- Solo lectura sobre el origen: el conector jamás ejecuta DML/DDL.
- Idempotente:
INSERT ... ON CONFLICTsobre claves naturales (CompanyId + Código); re-ejecutar no duplica. - Reanudable: checkpoint por entidad/cursor tras cada commit de lote;
--resume <runId>retoma exacto. - Fuente de verdad = BD viva del destino: introspección por
information_schema; columnas inexistentes se omiten; booleanos/char se coercionan al tipo real. - Documentos con número repetido (serial fiscal) → sufijo determinístico
NUM@SERIAL. Cuentas huérfanas → placeholder inactivo "(solo histórico)".
UI y modos de ejecución
| Modo | Bind | Auth | Uso |
|---|---|---|---|
Escritorio (ZenttoMigrador.exe) | 127.0.0.1 | ninguna | PC del técnico en la red del cliente |
| Servidor (migrator.zentto.net) | 0.0.0.0 tras nginx | login OTP obligatorio | app administrativa del equipo |
App de escritorio (.NET + WebView2)
Para el técnico en el local del cliente: app nativa Windows que no requiere instalar Node/git ni comandos. No reimplementa el motor — empaqueta el servidor Node ya validado como sidecar (esbuild bundle CJS + pkg → zentto-migrator-server.exe) y lo envuelve en una ventana WinForms con WebView2 apuntando a 127.0.0.1:4499. Instancia única (mutex), termina el sidecar al cerrar, datos por-usuario en %LOCALAPPDATA%\ZenttoMigrador.
.NET WinForms] -->|lanza| SC[runtime/server.exe
motor Node] EXE -->|WebView2| UI[wizard 127.0.0.1:4499] SC --> UI SC --> SQL[(SQL Server
del cliente)] SC --> ZEN[(Zentto
PostgreSQL)] style EXE fill:#6C63FF,color:#fff style SC fill:#10b981,color:#fff
Distribuible: desktop/build.ps1 (bundle → pkg → dotnet publish single-file self-contained → copia runtime + zip). El workflow desktop-release.yml lo compila en un runner Windows y publica ZenttoMigrador.zip como asset de Release; el técnico lo descarga, descomprime y doble clic. Sin Node, sin git, sin comandos.
Autenticación (modo servidor)
Login por correo autorizado + código de un solo uso, estilo Cloudflare Access:
- El correo debe estar en
MIG_ALLOWED_EMAILS(Vaultzentto/migrator). - Código de 6 dígitos enviado por zentto-notify (
/api/email/send). Vence en 10 min. - Se canjea por una sesión de 12 h.
MIG_UI_TOKENqueda como clave maestra de emergencia.
Defensas: rate limit 5/h por correo, no revela si el correo está autorizado, timingSafeEqual, máx. 5 intentos por código.
Asistente IA
src/server/assistant.ts reutiliza la IA ya desplegada en zentto-notify (/api/support/chat con RAG) con framing de experto en migraciones y contexto vivo del wizard (BD origen, empresa, entidades, errores). Las respuestas con bloques SQL se ejecutan con botón contra el destino conectado (/api/assistant/run-sql): SELECT devuelve preview; un script que modifica exige confirmación; todo en transacción con rollback ante error.
Despliegue (CI/CD)
docker build] BUILD --> GHCR[push ghcr.io
GITHUB_TOKEN] BUILD --> SAVE[docker save + scp] SAVE --> LOAD[docker load en server] LOAD --> UP[compose up -d
zentto-migrator] VAULT[(Vault
zentto/migrator)] -.AppRole.-> UP style BUILD fill:#6C63FF,color:#fff style UP fill:#10b981,color:#fff style VAULT fill:#374151,color:#fff
La imagen se transfiere por SSH (save→scp→load) porque GHCR rechaza los PATs fine-grained para pull desde el server y no se dejan PATs clásicos allí. Contingencia si se agota el límite de Actions: poner el repo público temporalmente, dejar correr el deploy y volverlo privado.
Secretos (todos en Vault)
| Ruta Vault | Campos |
|---|---|
zentto/migrator | MIG_UI_TOKEN, MIG_ALLOWED_EMAILS, MIG_TARGETS_JSON, NOTIFY_API_URL, NOTIFY_API_KEY |
zentto/github | GHCR_PAT |
zentto/migrator-deploy | SSH_PRIVATE_KEY (deploy key dedicada) |
El contenedor solo lleva bootstrap AppRole en /opt/zentto/.env.migrator (VAULT_ADDR + VAULT_ROLE_ID/VAULT_SECRET_ID + VAULT_KV_PATH); al arrancar lee el resto de Vault.
Estabilidad del servidor
Para que una migración jamás comprometa la producción: límites cpus 1.0 / mem 768m en compose, pausa entre lotes (MIG_BATCH_PAUSE_MS=150), una migración a la vez y pool PG máx. 4. Checkpoints persistentes en el volumen /opt/zentto/migrator-runs.
Recuperar la clave maestra
ssh root@178.104.56.185
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=<root> \
zentto-vault vault kv get -field=MIG_UI_TOKEN zentto/migrator
# Agregar un correo autorizado:
docker exec ... vault kv patch zentto/migrator \
MIG_ALLOWED_EMAILS="lista,actual,+nuevo@zentto.net"
docker restart zentto-migrator Entidades migradas (adaptador DatQbox)
Catálogos (líneas, categorías, marcas, unidades, almacenes, vendedores, tasas), clientes, proveedores, productos, facturas + detalle + pagos, notas de crédito, compras + detalle, CxC/CxP, bancos/cuentas/movimientos. No migra usuarios, permisos ni seguridad del legacy.
Adaptador Zentto→Zentto (tenant→tenant / servidor del cliente)
Además de DatQbox, el migrador copia datos entre dos instancias Zentto: de un tenant a otro, o desde el servidor Zentto privado de un cliente hacia el destino. Se elige "Zentto" como sistema origen en el Paso 1; la conexión es un PostgreSQL (host o connection string) y se elige la empresa origen a copiar.
- Requisito — esquemas iguales primero: si el origen está en una versión de esquema más vieja, hay que correr las migraciones (goose) para alinearlo antes de copiar. El migrador compara la versión goose de origen y destino: avisa al conectar el destino y bloquea la copia en
/api/runsi difieren. - Cómo funciona: el adaptador
src/adapters/zentto/lee las tablas Zentto del origen filtradas por la empresa origen → modelo canónico → los mismos loaders idempotentes del núcleo escriben en la empresa destino. Reanudable y sin duplicar, igual que DatQbox. - Ventas/compras: vienen NO seleccionadas por defecto — el único de
ar."SalesDocument"es(DocumentNumber, OperationType)sin CompanyId (modelo BD-por-tenant), así que copiar documentos es seguro solo hacia un tenant destino vacío. Maestros, CxC/CxP y bancos se copian sin restricción (clave(CompanyId, Código)).
otro tenant / cliente)] -->|por empresa| ZAD[Adaptador zentto] ZAD -->|canónico| CORE[Loaders idempotentes] CORE --> DST[(Zentto destino)] GUARD{¿misma versión
goose?} -.bloquea si no.-> CORE style ZAD fill:#6C63FF,color:#fff style CORE fill:#10b981,color:#fff style GUARD fill:#e8b339,color:#111