Skip to content

Plataforma técnica · Sevastopol

Capital View Island

Islands Sevastopol Administración

CapitalViewIsland mantiene los movimientos de capital del tenant: aportes iniciales, aumentos, reducciones, capitalización de utilidades, revalorizaciones y ajustes. Cada movimiento atraviesa el workflow PENDIENTE_APROBACION → ACTIVO (con ANULADO como cierre). La aprobación ejecuta un asiento contable y emite un evento al outbox de Orchestrator.

  • Directorysevastopol/src/components/islands/admin/
    • CapitalViewIsland.tsx — componente único, internamente CapitalView
const { tenantId } = useActiveTenant((id) => {
void fetchData(id);
void fetchLegalRepresentatives(id);
void fetchChartOfAccounts(id);
});
const [entries, setEntries] = createSignal<CapitalEntry[]>([]);
const [legalReps, setLegalReps] = createSignal<LegalRep[]>([]);
const [chartAccounts, setChartAccounts] = createSignal<Account[]>([]);

Cambiar tenant carga 3 datasets en paralelo porque el modal de capital necesita los tres: cuentas para validar, representantes para el socio, movimientos para listar.

Signals adicionales:

SignalTipoUso
qEstado / qTipostringFiltros locales.
showModal / editingId / formDataediciónModal alta/edición.
isSubmitting / isLoadingbloqueosUI.
const TIPO_CAPITAL_OPTIONS = [
{ id: "APORTE_INICIAL", nombre: "Aporte Inicial" },
{ id: "AUMENTO_CAPITAL", nombre: "Aumento Capital" },
{ id: "REDUCCION_CAPITAL", nombre: "Reducción Capital" },
{ id: "CAPITALIZACION_UTILIDADES", nombre: "Capitalización Utilidades" },
{ id: "REVALORIZACION", nombre: "Revalorización" },
{ id: "AJUSTE_CAPITAL", nombre: "Ajuste Capital" },
];
TipoSigno en statsCuenta default
APORTE_INICIAL+2301001 (capital aportado)
AUMENTO_CAPITAL+2301001
REDUCCION_CAPITAL2301001
CAPITALIZACION_UTILIDADES+configurable
REVALORIZACION+configurable
AJUSTE_CAPITAL+configurable

REDUCCION_CAPITAL es la única que resta en el total de capital — el cálculo totalCapital aplica el signo:

totalCapital: activeList.reduce((sum, e) => {
const sign = e.tipo_capital === "REDUCCION_CAPITAL" ? -1 : 1;
return sum + e.monto * sign;
}, 0),
const ESTADO_OPTIONS = [
{ id: "ACTIVO", nombre: "Activo" },
{ id: "ANULADO", nombre: "Anulado" },
{ id: "PENDIENTE_APROBACION", nombre: "Pendiente Aprobación" },
];
EstadoSignificadoCuenta para el total
PENDIENTE_APROBACIONBorrador, sin asiento contable.No
ACTIVOAprobado, con asiento generado.
ANULADOMovimiento reversado/cancelado.No

El backend tiene un CHECK CONSTRAINT chk_capital_aprobacion que rechaza insertar/actualizar directamente a ACTIVO. El frontend resuelve esto haciendo dos llamadas secuenciales:

const shouldApprove = requestedEstado === "ACTIVO"
&& (!isEditing || currentEntry?.estado === "PENDIENTE_APROBACION");
const payloadToSave = {
...payload,
// Guardar primero en PENDIENTE y luego aprobar para cumplir chk_capital_aprobacion.
estado: shouldApprove ? "PENDIENTE_APROBACION" : requestedEstado,
};
// Paso 1: POST/PUT con estado PENDIENTE_APROBACION
const r = await authenticatedFetch(url, { method, body: JSON.stringify(payloadToSave) });
const saved = await r.json();
// Paso 2: si el usuario eligió ACTIVO, aprobar
if (shouldApprove) {
const targetId = isEditing ? String(editingId()) : String(saved?.id || "");
const approveResp = await authenticatedFetch(
`${API_BASE}/${targetId}/approve?tenant_id=${tenantId()}`,
{ method: "POST" },
);
}

El paso 2 es el que dispara la contabilización en el backend y el evento capital:movimiento al outbox.

ANULADO sólo se permite sobre registros ya aprobados:

if (requestedEstado === "ANULADO"
&& (!isEditing || currentEntry?.estado === "PENDIENTE_APROBACION")) {
toast.push("No se puede dejar en Anulado antes de estar aprobado", "error");
return;
}

Anular un borrador no tiene sentido — se elimina.

El modal incluye un selector de representante legal. La lógica fuerza que socio_id siga al representante seleccionado:

const selectedRepresentativeId = payload.representante_legal_id || null;
payload.representante_legal_id = selectedRepresentativeId;
payload.socio_id = selectedRepresentativeId; // En este flujo, socio_id refleja el representante seleccionado.

Las opciones se ordenan por apellido + nombre, con etiqueta Nombres Apellidos (RUT) [No vigente] si aplica:

const legalRepOptions = createMemo(() =>
[...legalReps()]
.sort((a, b) => `${a.apellidos} ${a.nombres}`.localeCompare(`${b.apellidos} ${b.nombres}`))
.map(rep => ({
id: rep.id,
nombre: `${rep.nombres} ${rep.apellidos}${rep.rut ? ` (${rep.rut})` : ""}${rep.vigente ? "" : " [No vigente]"}`,
})),
);

El movimiento exige cuenta_contable_codigo (default 2301001 en el formulario). Opcionalmente puede indicar cuenta_contrapartida_codigo — sino el backend usa la contrapartida por defecto (capital pagado vs banco/caja).

Ambas se validan contra el plan cargado en chartAccounts. La validación es frontend-only (existencia); el backend revalida.

PillCálculo
Totalentries.length
Activosfilter(estado === "ACTIVO").length
Pendientesfilter(estado === "PENDIENTE_APROBACION").length
Total CapitalSuma firmada de monto solo para ACTIVO, con REDUCCION_CAPITAL en negativo.
return list.sort((a, b) =>
new Date(b.fecha_movimiento).getTime() - new Date(a.fecha_movimiento).getTime(),
);

Más recientes primero — el operador suele revisar el último movimiento ingresado.

MétodoRutaOperación
GET/api/admin/capital?tenant_id=...Lista de movimientos.
POST/api/admin/capital?tenant_id=...Alta como PENDIENTE_APROBACION.
PUT/api/admin/capital/:id?tenant_id=...Edición (preserva estado salvo aprobación).
POST/api/admin/capital/:id/approve?tenant_id=...Aprobación — dispara asiento y evento capital:movimiento.
DELETE/api/admin/capital/:id?tenant_id=...Eliminación (rechazada si el movimiento tiene asientos aprobados).
GET/api/admin/representatives?tenant_id=...Catálogo de socios.
GET/api/admin/chart-of-accounts?tenant_id=...Catálogo de cuentas para validar.
ReglaMotivo
Aprobación en dos pasosEl CHECK CONSTRAINT chk_capital_aprobacion impide setear ACTIVO directo; la UI emula la transición esperada.
Anular solo a partir de ACTIVOAnular un borrador no tiene reverso contable — se elimina.
REDUCCION_CAPITAL con signo negativo en statsEs el único tipo que resta del capital; mostrar +monto sería engañoso.
socio_id = representante_legal_idEl flujo actual no distingue socio de representante; mantener la simetría evita ambigüedad.
Default cuenta 2301001Cuenta de capital aportado del plan estándar; el operador puede sobreescribir.
Tres fetches en paralelo al cambiar tenantCuentas, reps y movimientos son independientes; serializar es latencia visible.
Ordenar por fecha descendenteLas decisiones recientes pesan más en este dominio.
Confirmación en eliminaciónEl backend rechaza si tiene asientos; el confirm evita el viaje y aclara la consecuencia.
Etiqueta del rep con [No vigente]El operador puede asociar a un rep histórico para cuadres antiguos; el sufijo evita confusión.