Plataforma técnica · Orchestrator
Gastos
Orchestrator Gastos Categorias Gasto
El dominio Gastos registra hechos económicos de gasto que no pasan por el flujo estándar de compras → inventario. Cubre tres escenarios reales:
- Gastos manuales — el contador registra un gasto sin documento SII (provisión, ajuste, gasto sin factura).
- Gastos vinculados a compra SII — una compra existente en
operaciones_sii.compras_ventas_detallese clasifica como gasto en lugar de ingreso a inventario. - Importación masiva por rango — impuestos específicos (códigos 20-27 del SII) o IVA no recuperable se importan en bulk desde compras SII pendientes, según
gastos.fn_importar_impuestosygastos.fn_importar_iva_no_recuperable.
A diferencia del dominio inventario, gastos no genera stock ni costo de ventas. La contabilización emite un asiento simple (cargo a gasto + crédito a proveedor / IVA crédito si aplica) y opcionalmente emite compra:contabilizada cuando hay compra_detalle_id vinculado.
Ubicación
Section titled “Ubicación”orchestrator/src/domain/gastos/├── GastosService.ts # 14 métodos públicos├── GastosRepository.ts # queries + delegaciones a fn_* SQL├── types.ts # CategoriaGasto, Gasto, DTOs, resultados└── __tests__/Entidades
Section titled “Entidades”| Entidad | Tabla | Rol |
|---|---|---|
CategoriaGasto | gastos.categorias_gasto | Plantilla de cuenta de gasto + IVA crédito + concepto de impuesto vinculado. Reusable entre gastos del mismo tipo (e.g., “Arriendo oficina”, “Servicios básicos”). |
Gasto | gastos.gastos | Registro de gasto puntual con categoría, proveedor, monto, fecha. Opcionalmente vinculado a una compra SII (compra_detalle_id). |
GastoDetalle | gastos.gastos_detalle | Líneas contables generadas al contabilizar (cargo/crédito por línea). |
ConceptoOperacionImpuesto | operaciones_sii.conceptos_operaciones (vista) | Filtra conceptos válidos para gastos: tipo_concepto = 'OTRO' o id=30 (IVA no recuperable) o codigo=‘GASTO-IVA’. |
Flujos de creación
Section titled “Flujos de creación”flowchart TB
IN["createGasto(dto)"]
COMP{"vincular_sii && !compra_detalle_id?"}
MAS["createGastoMasivoDesdeSii<br/>(busca compras del proveedor en el período)"]
CDI{"compra_detalle_id?"}
CHK["Verifica que no esté ya<br/>asociada a otro gasto"]
CMP["findCompraDisponibleByDetalleId<br/>+ hydratePayloadFromCompra"]
HYD["hydratePeriodo (deriva año/mes<br/>desde fecha_documento)"]
VAL["validateMontos (neto > 0, iva >= 0)"]
INS["createGasto en gastos.gastos"]
OUT["Gasto"]
OUTM["CreateGastoMasivoResult"]
IN --> COMP
COMP -- sí --> MAS --> OUTM
COMP -- no --> CDI
CDI -- sí --> CHK --> CMP --> HYD
CDI -- no --> HYD
HYD --> VAL --> INS --> OUT 1. Manual (sin SII)
Section titled “1. Manual (sin SII)”createGasto({ categoria_id: 5, rut_proveedor: "76123456-7", fecha_documento: "2026-03-15", monto_neto: 250000, monto_iva: 47500, glosa: "Servicio mantención impresora",});Sin compra_detalle_id ni vincular_sii. El service hidrata año/mes desde fecha_documento y valida montos.
2. Vinculado a compra SII puntual
Section titled “2. Vinculado a compra SII puntual”createGasto({ categoria_id: 5, rut_proveedor: "76123456-7", compra_detalle_id: "550e8400-e29b-41d4-a716-446655440000",});El service:
- Verifica que la compra no esté ya asociada a otro gasto (idempotencia).
- Llama
findCompraDisponibleByDetalleIdque reagrega NETO+IVA+OTRO pordocumento_idy validaestado = 'PENDIENTE'. hydratePayloadFromComprareemplazarut_proveedor/tipo_dte/folio/fecha_documento/monto_neto/monto_iva/glosacon los datos del SII.
3. Masivo desde SII por proveedor (vincular_sii = true)
Section titled “3. Masivo desde SII por proveedor (vincular_sii = true)”createGasto({ categoria_id: 5, rut_proveedor: "76123456-7", año: 2026, mes: 3, vincular_sii: true,});createGastoMasivoDesdeSii busca todas las compras pendientes del proveedor en el período (mesExacto: true) y crea un gasto por cada una. Falla con ValidationError si no hay compras disponibles.
Retorna CreateGastoMasivoResult { vinculacion: "SII_MASIVO", creados, gastos[] }.
4. Importación por rango (sin proveedor específico)
Section titled “4. Importación por rango (sin proveedor específico)”Dos endpoints separados que cubren el caso “barré por fecha y traigo todo lo que aplique”:
| Método | Función SQL | Propósito |
|---|---|---|
importarImpuestosPorRango | gastos.fn_importar_impuestos(fecha_desde, fecha_hasta, codigo_desde, codigo_hasta, userId) | Importa impuestos específicos (códigos SII 20-27 por default: tabaco, bebidas, gasolina) desde compras pendientes en el rango. |
importarIvaNoRecuperablePorRango | gastos.fn_importar_iva_no_recuperable(fecha_desde, fecha_hasta, concepto_id, userId) | Importa IVA no recuperable (concepto_id=30 por default) desde compras pendientes en el rango. |
Ambos retornan { encontrados, creados, duplicados, sin_categoria, categorias_sin_mapear[] }. La lógica de matching concepto → categoría_gasto vive en SQL (las funciones SQL hacen el JOIN). categorias_sin_mapear indica qué conceptos no encontraron una categoría definida — accionable para parametrizar.
State machine del Gasto
Section titled “State machine del Gasto”stateDiagram-v2 [*] --> PENDIENTE : createGasto PENDIENTE --> CONTABILIZADO : contabilizarGasto (vía fn_contabilizar_gasto) CONTABILIZADO --> PENDIENTE : reversarGasto PENDIENTE --> [*] : deleteGasto
deleteGastosolo funciona enPENDIENTE(DELETE conWHERE estado = 'PENDIENTE'; retorna{ deleted: false }en otros estados sin error).reversarGastosolo funciona enCONTABILIZADO(UPDATE con guard); falla conValidationErrorsi no aplica.contabilizarGastodelega a la función SQLgastos.fn_contabilizar_gasto(id, userId, año?, mes?)que genera líneas engastos.gastos_detalley marca el estado. Period override permite re-asignar el período contable si difiere del original.
Hooks emitidos
Section titled “Hooks emitidos”| Evento | Trigger | Payload | Condición |
|---|---|---|---|
compra:contabilizada | contabilizarGasto y contabilizarPendientesGrupo post-COMMIT | { compraId, monto, formaPago } | Solo si el gasto tiene compra_detalle_id (no se emite para gastos manuales sin SII) |
findCompraContabilizadaEventPayload reagrega desde compras_ventas_detalle el documento_id, monto_total del gasto y forma_pago normalizada (lower-case, default 'otro'). El evento alimenta a cicloContable y financieros para conciliación bancaria.
Convenciones
Section titled “Convenciones”| Convención | Detalle |
|---|---|
| Cuenta proveedor default | 2102001 (acreedores varios). Sobreescribible vía categoría. |
| Conceptos válidos para gastos | tipo_concepto = 'OTRO' OR id = 30 (IVA no recuperable) OR codigo = 'GASTO-IVA'. Excluye conceptos NETO e IVA estándar — esos van por flujo de inventario. |
| Default impuestos específicos | Códigos 20-27 del SII (tabaco, gasolina, bebidas, etc.) |
| RUT normalización | regexp_replace(lower(rut), '[^0-9k]', '', 'g') aplicado en JOINs con proveedores_clasificacion para tolerar formatos con/sin guion/puntos. |
mesExacto en búsqueda masiva | findComprasDisponibles admite mesExacto: true (match exacto mes = $) o mesExacto: false (acumulado mes <= $). El flujo masivo usa exacto; el listado UI usa acumulado. |
Compatibilidad multi-tenant
Section titled “Compatibilidad multi-tenant”categoriasHasActivoColumn verifica con information_schema.columns si la tabla gastos.categorias_gasto tiene columna activo. Tenants viejos que aún no migraron tratan todas las categorías como activas (true::boolean AS activo). Es el mismo patrón de resilencia que usa cicloContable con 42P01 — el código tolera schemas en distintas versiones sin romperse.
Funciones SQL invocadas
Section titled “Funciones SQL invocadas”| Función | Rol |
|---|---|
gastos.fn_contabilizar_gasto(id, userId, año?, mes?) | Genera detalle contable y marca CONTABILIZADO. Acepta period override. |
gastos.fn_importar_impuestos(desde, hasta, cod_desde, cod_hasta, userId) | Bulk-insert de impuestos específicos. |
gastos.fn_importar_iva_no_recuperable(desde, hasta, concepto_id, userId) | Bulk-insert de IVA no recuperable. |
Estas funciones encapsulan la lógica de imputación que históricamente vivía en SPs. El service las consume sin replicar la lógica en TypeScript — coherente con la decisión opuesta tomada en F29GeneratorService (rewrite de SP a TS). La diferencia: fn_contabilizar_gasto y las dos fn_importar_* son operaciones bulk con muchas inserciones por llamada, donde el cost de roundtrip TS↔SQL excede el beneficio de testabilidad. F29GeneratorService calcula valores agregados de varias fuentes — testear esa lógica en TS aportó más.
Tabla de endpoints (referencia)
Section titled “Tabla de endpoints (referencia)”GET /api/gastos/categoriasPOST /api/gastos/categoriasPATCH /api/gastos/categorias/:idGET /api/gastos/conceptos-impuestos
GET /api/gastos # con filtros: año, mes, estado, categoria_idPOST /api/gastos # manual, vinculado o masivo según DTODELETE /api/gastos/:id # solo PENDIENTEPOST /api/gastos/:id/contabilizar # opcional periodo overridePOST /api/gastos/:id/reversarPOST /api/gastos/contabilizar-pendientes # grupo por año/mesHasta/categoria
POST /api/gastos/importar-impuestos # rango + códigos SIIPOST /api/gastos/importar-iva-no-recuperable
GET /api/gastos/resumen-anualGET /api/gastos/compras-disponibles # del SII, pendientes de gastoGET /api/gastos/proveedores # con compras_pendientes para gasto