Skip to content

Plataforma técnica · Orchestrator

Capital Service

Orchestrator Administración Capital

CapitalService gestiona los movimientos de capital social: aportes iniciales, aumentos, reducciones, capitalización de utilidades, revalorizaciones y ajustes. Es el único servicio del dominio Administración que emite hooks de dominio — su evento capital:movimiento alimenta listeners que reaccionan a cambios de patrimonio.

CapitalEntry:

CampoTipoNotas
idUUID
tipo_capitalenum CapitalTypeVer tabla Tipos.
fecha_movimientoDATE
montonumericValidado positivo según tipo_capital.
monedastringDefault CLP.
estadoenum ACTIVO | ANULADO | PENDIENTE_APROBACIONDefault ACTIVO.
socio_idUUID?
porcentaje_participacionnumeric?Validado ∈ [0, 100].
representante_legal_idUUID?FK opcional a representantes.
cuenta_contable_codigostring?Default 2301001 si no se especifica.
cuenta_contrapartida_codigostring?Validada contra plan_contable.
documento_respaldostring?Tipo de documento (escritura, etc.).
numero_documentostring?
descripcion / observacionesstring?
aprobado_porUUID?Lleno tras approve.
fecha_aprobacionDATE?
created_by / updated_byUUID?
tipo_capitalCategoría hookCuándo
APORTE_INICIALAPORTECapital fundacional.
AUMENTO_CAPITALAPORTEIncremento de capital social.
CAPITALIZACION_UTILIDADESAPORTEUtilidades reinvertidas.
REDUCCION_CAPITALRETIRODisminución de capital.
REVALORIZACIONAJUSTECorrección monetaria.
AJUSTE_CAPITALAJUSTEOtros ajustes contables.

Lista paginada con filtros opcionales (estado, tipo_capital, fecha_desde, fecha_hasta, limit, offset). Default limit=100, ordenado por fecha_movimiento DESC, created_at DESC.

Lookup. Lanza NotFoundError("Capital entry", id) si no existe.

Validaciones (antes de abrir TX):

  1. validateRequired(data, ['tipo_capital', 'fecha_movimiento', 'monto']).
  2. Si tipo_capital ∈ { APORTE_INICIAL, AUMENTO_CAPITAL, CAPITALIZACION_UTILIDADES, REVALORIZACION }: monto > 0.
  3. Si porcentaje_participacion definido: ∈ [0, 100].

Dentro de la TX:

  1. Si cuenta_contable_codigo definido: existe en administracion.plan_contable.
  2. Si cuenta_contrapartida_codigo definido: existe en administracion.plan_contable.
  3. INSERT con default cuenta_contable_codigo = '2301001' si no se pasó.
  4. outbox.queue('capital:movimiento', { capitalId, tipoMovimiento, monto }).

Tras el COMMIT, el outbox vacía el evento al DomainEventBus. Si la TX rollback, el evento no se emite.

Lógica diferenciada según estado del registro:

flowchart TB
  IN["update(id, updates)"]
  EX["findById(id)"]
  AP{"existing.aprobado_por != null ?"}
  SAFE["¿updates solo toca<br/>cuenta_contable_codigo,<br/>cuenta_contrapartida_codigo,<br/>descripcion, observaciones?"]
  ERR["ValidationError<br/>'Asiento aprobado: solo se permiten<br/>cambios en safeFields. Bloqueados: ...'"]
  FILTER["filtrar updates a safeFields<br/>(si quedó vacío → no-op return)"]
  VAL["validar monto/porcentaje/cuentas<br/>(si aplican)"]
  UPD["UPDATE en DB"]

  IN --> EX --> AP
  AP -- sí --> SAFE
  SAFE -- no --> ERR
  SAFE -- sí --> FILTER --> UPD
  AP -- no --> VAL --> UPD

Safe fields sobre un asiento aprobado: cuenta_contable_codigo, cuenta_contrapartida_codigo, descripcion, observaciones. Cualquier otro campo lanza ValidationError listando los bloqueados.

La detección es semántica (compara contra el valor actual, normaliza Date a toISOString), no por mera presencia del key — pasar el mismo valor no se considera intento de cambio.

update no emite hook. Solo create y approve (vía create si el flujo de aprobación llega ahí; ver nota más abajo).

Bloqueado si el registro está aprobado (aprobado_por != null) — lanza ValidationError("Cannot delete approved capital entry").

Transiciona PENDIENTE_APROBACION → ACTIVO:

UPDATE administracion.capital
SET estado = 'ACTIVO',
aprobado_por = $userId,
fecha_aprobacion = CURRENT_DATE,
updated_by = $userId,
updated_at = CURRENT_TIMESTAMP
WHERE id = $id AND estado = 'PENDIENTE_APROBACION'
RETURNING *

La cláusula AND estado = 'PENDIENTE_APROBACION' actúa como guarda — si el row ya no está en ese estado el UPDATE no afecta filas. El servicio igualmente valida antes con findById y lanza ValidationError("Capital entry is not pending approval").

Encolado dentro de la TX de create, emitido tras COMMIT vía BaseService.flushOutbox.

Payload tipado (DomainEventMap['capital:movimiento'] + EventContext):

Payload
{
capitalId: string;
tipoMovimiento: 'APORTE' | 'RETIRO' | 'AJUSTE';
monto: number;
// Auto-añadidos por flushOutbox:
tenantDb: string;
userId: string;
requestId?: string;
}

tipo_capital (granularidad de negocio) se reduce a tipoMovimiento (granularidad del hook) por mapTipoMovimiento:

tipo_capitaltipoMovimiento
APORTE_INICIALAPORTE
AUMENTO_CAPITALAPORTE
CAPITALIZACION_UTILIDADESAPORTE
REDUCCION_CAPITALRETIRO
REVALORIZACIONAJUSTE
AJUSTE_CAPITALAJUSTE

Los listeners no necesitan conocer el detalle de CAPITALIZACION_UTILIDADES vs AUMENTO_CAPITAL — para cualquiera, el patrimonio sube. Si un listener requiere el detalle, debe consultar el row vía capitalId.

Ninguno con efectos reales todavía. El evento se persiste en MongoDB vía MongoEventSink (audit trail), pero no hay listeners de negocio enganchados. Documentado para que cuando se agregue CashFlowService o BalanceProyectadoService puedan suscribirse sin tocar CapitalService.

flowchart LR
  START(("●<br/>start"))
  ACT["ACTIVO"]
  PND["PENDIENTE_APROBACION"]
  ANU["ANULADO"]
  END_OK(("◉<br/>delete OK"))
  END_BLOCK(("⊘<br/>delete bloqueado<br/>si aprobado_por"))

  START -->|create default| ACT
  START -->|create con estado explícito| PND
  PND -->|approve| ACT
  ACT -->|update estado=ANULADO| ANU
  PND -->|update estado=ANULADO| ANU
  ACT -.->|delete| END_BLOCK
  PND -->|delete| END_OK
  ANU -->|delete| END_OK

Una vez aprobado_por está set (lo que ocurre en approve), delete queda bloqueado y update solo permite safe fields. El registro es esencialmente inmutable salvo por glosa contable.

CapitalRepositoryImpl está envuelto con wrapStaticRepository (decorator común) con slowQueryMs: 500 — queries que pasen ese umbral se loguean.

export const CapitalRepository = wrapStaticRepository(CapitalRepositoryImpl, {
label: "CapitalRepository",
slowQueryMs: 500,
});

Los update usan buildUpdate (de domain/common/sqlBuilders) para construir SET dinámico, con updatedAtSql: 'CURRENT_TIMESTAMP'.

Documentados en Administración API. Resumen:

MétodoRutaService call
GET/api/admin/capitallist(ctx, filters)
GET/api/admin/capital/:idgetById(ctx, id)
POST/api/admin/capitalcreate(ctx, data)
PUT/api/admin/capital/:idupdate(ctx, id, updates)
DELETE/api/admin/capital/:iddelete(ctx, id)
POST/api/admin/capital/:id/approveapprove(ctx, id)