Plataforma técnica · Orchestrator
Plan de Cuentas Sync
Orchestrator Command Plan Cuentas
PlanCuentasSyncService es la herramienta operacional para mantener consistente el plan de cuentas template (la base de referencia que vive en accounting_template.plan_contable) con cada base de tenant. Resuelve preguntas como: “¿qué cuentas le faltan al tenant X?”, “¿qué tiene de más?”, “agregué una cuenta nueva al template, propágala a todos”.
Conceptos del Diff
Section titled “Conceptos del Diff”El servicio compara accounting_template.plan_contable contra nostromo_<rut>.plan_contable y clasifica cada código en una de cuatro categorías:
| Categoría | Definición | Acción típica |
|---|---|---|
| Faltantes | Están en template pero no en tenant. | insert_missing |
| Huérfanas | Están en tenant pero no en template y no son instanciables locales. | delete_orphans (con checks) |
| Locales | Están en tenant pero no en template y su padre está en INSTANCIABLE_PARENTS (actualmente { '2204000' }). | Se respetan — son legítimas. |
| Divergentes | Existen en ambos, pero algún campo de COMPARE_FIELDS difiere. | update_divergent |
COMPARE_FIELDS compara: codigo_padre, nombre, nombre_ifrs, tipo, naturaleza, nivel, es_imputable, requiere_tercero, requiere_cc, requiere_proyecto, orden_presentacion, categoria_ifrs.
Diff y Overview
Section titled “Diff y Overview”flowchart LR
T[("accounting_template<br/>plan_contable")]
TE1[("tenant 1")]
TE2[("tenant 2")]
TE3[("tenant N")]
S["PlanCuentasSyncService"]
RES["TenantSummary[]<br/>{ faltantes_count<br/>huerfanas_count<br/>divergentes_count<br/>locales_count }"]
T --> S
TE1 --> S
TE2 --> S
TE3 --> S
S --> RES getOverview() ejecuta el diff contra todos los tenants activos en paralelo (Promise.allSettled) y devuelve un resumen por base. getDiff(database) devuelve el diff detallado de un solo tenant con las listas completas de códigos en cada categoría.
Sincronización: syncTenant(database, options)
Section titled “Sincronización: syncTenant(database, options)”SyncOptions:
| Campo | Tipo | Significado |
|---|---|---|
dry_run | boolean | Si true, solo devuelve qué haría sin escribir. |
insert_missing | boolean | Inserta cuentas faltantes (ordenado por nivel ↑). |
update_divergent | boolean | Re-aplica campos divergentes desde el template. |
delete_orphans | boolean | Borra huérfanas (con verificación de seguridad). |
target_databases | string[] | Si está vacío, opera sobre todos los tenants activos. |
Orden de operaciones
Section titled “Orden de operaciones”flowchart TB A["syncTenant(db, opts)"] V["validateSyncOptions"] D["getDiff(db)"] M["insertMissing<br/>(ordenado por nivel)"] U["updateDivergent<br/>(re-aplica template)"] O["deleteOrphans<br/>(check dependencies)"] R["SyncResult"] A --> V --> D D -. dry_run = true .-> R D --> M --> U --> O --> R
Por qué ordenar por nivel para insertar: las cuentas tienen codigo_padre; insertar primero los padres evita violar foreign keys.
SyncResult
Section titled “SyncResult”| Campo | Tipo | Contenido |
|---|---|---|
inserted_count | number | Insertadas con éxito. |
inserted_codigos | string[] | Códigos insertados (o que se insertarían si dry_run). |
updated_count | number | Updates aplicados. |
updated_codigos | string[] | Códigos actualizados. |
deleted_count | number | Huérfanas eliminadas. |
deleted_codigos | string[] | Códigos eliminados. |
skipped_count | number | Borrados saltados por dependencias. |
skipped | Array<{ codigo, reason }> | Detalle de saltos: 'has_movements', 'has_children', o 'has_movements,has_children'. |
errors | Array<{ codigo, error }> | Errores por código durante el sync. |
error | string | null | Error fatal previo al sync. |
Verificación antes de borrar huérfanas
Section titled “Verificación antes de borrar huérfanas”deleteOrphans no es destructivo a ciegas. Para cada huérfana llama getOrphanCheck, que retorna:
| Campo | Significado |
|---|---|
movimientos_count | Cantidad de líneas contables que referencian la cuenta. |
has_children | Existen cuentas con codigo_padre = este código. |
can_delete | movimientos_count === 0 && !has_children. |
Si can_delete === false, la cuenta se salta y se reporta en skipped. Nunca se borra una cuenta con histórico de movimientos.
Mutar el Template
Section titled “Mutar el Template”El template es el punto de partida, no inmutable. Tres operaciones administrativas:
createCuentaInTemplate(cuenta, propagate)
Section titled “createCuentaInTemplate(cuenta, propagate)”Crea una cuenta en el template. Valida:
- Formato del código:
^[0-9]+$. nivel ∈ [1..6].naturaleza ∈ { 'D', 'C' }.tipo ∈ { 'ACTIVO', 'PASIVO', 'PATRIMONIO', 'INGRESO', 'GASTO' }.- Si
nivel > 1, el padre debe existir en el template.
Si propagate=true, intenta insertar la cuenta en cada tenant activo. Los errores por tenant no fallan el batch — se acumulan en errors[].
updateCuentaInTemplate(codigo, patch, propagate)
Section titled “updateCuentaInTemplate(codigo, patch, propagate)”Actualiza campos del template (no permite cambiar codigo). Si propagate=true, replica la actualización en cada tenant que ya tenga el código.
deleteCuentaInTemplate(codigo, propagate)
Section titled “deleteCuentaInTemplate(codigo, propagate)”Elimina una cuenta del template. Comportamiento:
- Si
propagate=falsey algún tenant la tiene, rechaza concuenta_in_use_in_tenantsincluyendo la lista de bases afectadas. - Si
propagate=true, intenta borrarla en cada tenant aplicando el mismogetOrphanCheck(no borra con movimientos / con hijos). - Borra del template al final, solo si la propia tabla del template pasa el check.
promoteCuentaToTemplate(database, codigo)
Section titled “promoteCuentaToTemplate(database, codigo)”Promueve una cuenta local (creada por un tenant bajo un padre instanciable) al template canónico. Validaciones:
- La cuenta debe existir en el tenant.
- No debe existir aún en el template.
- Si tiene
codigo_padre, el padre debe estar en el template.
No la propaga al resto de tenants — para distribuirla, correr syncTenant con insert_missing=true o createCuentaInTemplate({ propagate: true }) desde cero.
Patrón dry_run
Section titled “Patrón dry_run”Antes de cualquier sync destructivo, correr con dry_run: true:
POST /api/command/plan-cuentas/sync/nostromo_60004317{"dry_run": true,"insert_missing": true,"update_divergent": true,"delete_orphans": true,"target_databases": []}El SyncResult devuelve listas en inserted_codigos, updated_codigos, deleted_codigos sin tocar la base. Si todo se ve bien, re-ejecutar con dry_run: false.
Errores Estructurados
Section titled “Errores Estructurados”| Mensaje | Causa |
|---|---|
tenant_not_found | La base no está en listActiveTenants. |
cannot_sync_template | Se intentó pasar accounting_template como destino. |
cannot_change_codigo | Patch incluyó codigo. |
cannot_delete_with_dependencies | getOrphanCheck detectó movimientos o hijos. Trae movimientos_count y has_children adjuntos. |
cuenta_in_use_in_tenants | Delete sin propagate=true cuando hay tenants afectados. Trae tenants[] adjunto. |
cuenta_not_found_in_tenant | Promote desde una cuenta que no existe en el tenant. |
cuenta_already_in_template | Promote de un código que ya está en el template. |
orphan_padre_missing_in_template | Create/promote con codigo_padre que no existe. |
cuenta_not_orphan | removeOrphan sobre una cuenta que sí está en el template (o no en el tenant). |
Performance
Section titled “Performance”getOverview()corredumpPlanContablecontra todos los tenants enPromise.allSettled. Con 50 tenants y 1000 cuentas cada uno, ~5-10s.syncTenantes secuencial por categoría (insert, then update, then delete) y secuencial dentro de cada lista — diseñado para correctitud sobre velocidad. No es operación hot-path.
Si se vuelve un cuello de botella, hay dos caminos: paralelizar insertMissing por nivel (todos los del nivel N antes que N+1) o mover a un job en background con persistencia del SyncResult. Ninguno implementado.