Plataforma técnica · Orchestrator
Operaciones Service
Orchestrator Operaciones Sii
OperacionesService coordina la generación del detalle contable para documentos del SII (ventas, compras y boletas). Toma documentos normalizados desde operaciones_sii, los clasifica por concepto contable según tipo de documento, y persiste líneas en operaciones_sii.compras_ventas_detalle.
Es el único servicio del dominio Operaciones: la lógica de clasificación es densa (notas de crédito invierten signos, facturas de compra en libro de ventas tratan IVA retenido, IVA no recuperable se separa como gasto, redondeos de 1–2 pesos se ajustan) y vive concentrada acá.
Qué hace, qué no
Section titled “Qué hace, qué no”| Hace | No hace |
|---|---|
| Genera detalle contable a partir de documentos SII pendientes. | No registra asientos definitivos en libro mayor. |
| Bloquea reprocesos accidentales por período/tipo. | No sincroniza documentos desde el SII (eso es upstream). |
Marca ingresos netos de ventas/boletas como CONTABILIZADO. | No paga, no anula, no concilia. |
Emite venta:contabilizada al confirmar ingresos. | No persiste pagos ni movimientos bancarios. |
| Expone consulta de documentos fuente y resumen por período. | No factura ni emite DTE. |
Dependencias
Section titled “Dependencias”flowchart LR
S["OperacionesService"]
R["OperacionesRepository"]
CP["operaciones_sii.conceptos_operaciones<br/>(catálogo: VENTA-PRD-NET, COMP-MER-IVA, ...)"]
DET["operaciones_sii.compras_ventas_detalle<br/>(destino del detalle generado)"]
V[("operaciones_sii.ventas")]
C[("operaciones_sii.compras + compras_otros_impuestos")]
B[("operaciones_sii.boletas")]
EB["DomainEventBus"]
S --> R
R --> CP
R --> V & C & B
R --> DET
S -. outbox post-COMMIT .-> EB Operaciones públicas
Section titled “Operaciones públicas”| Método | Para |
|---|---|
generate(ctx, params) | Procesa documentos del período y genera detalle. |
getDetails(ctx, filter) | Lista documentos fuente normalizados por tipo y período. |
estadoContabilizacion(ctx, desde, hasta) | Resumen por tipo (VENTA/COMPRA/BOLETA) del avance. |
reversarContabilizacionIngresos(ctx, params) | Revierte la marca CONTABILIZADO aplicada por generate. |
generate — flujo
Section titled “generate — flujo”flowchart TB
IN["generate(params)"]
V1["validateRequired<br/>(tipo_documento)"]
V2["contabilizar_ingresos solo<br/>aplica a VENTA o BOLETA"]
TX["withTransaction"]
RP{"forzar_reproceso?"}
DEL["deleteDetails<br/>(tipo, periodo)"]
CHK["countExistingDetails > 0<br/>→ ValidationError"]
CONC["fetchConcepts<br/>(codigo → id map)"]
DISP{"tipo_documento?"}
PVE["processVentas"]
PCO["processCompras"]
PBO["processBoletas"]
PER["createBulkDetails"]
CI{"contabilizar_ingresos?"}
MK["contabilizarIngresosNetos<br/>+ outbox.queue<br/>(venta:contabilizada × N)"]
COMMIT["COMMIT → flushOutbox"]
IN --> V1 --> V2 --> TX
TX --> RP
RP -- sí --> DEL --> CONC
RP -- no --> CHK --> CONC
CONC --> DISP
DISP -- VENTA --> PVE
DISP -- COMPRA --> PCO
DISP -- BOLETA --> PBO
PVE & PCO & PBO --> PER
PER --> CI
CI -- sí --> MK --> COMMIT
CI -- no --> COMMIT Parámetros
Section titled “Parámetros”interface OperationsGenerateParams {tipo_documento: 'COMPRA' | 'VENTA' | 'BOLETA';periodo_desde?: string; // YYYY-MMperiodo_hasta?: string; // YYYY-MMforzar_reproceso?: boolean;contabilizar_ingresos?: boolean;}Clasificación por tipo de documento
Section titled “Clasificación por tipo de documento”Cada processX lee documentos del período y genera 1–2 líneas por documento (neto + IVA). La clasificación depende del tipo_doc (factura normal vs NC vs ND vs exenta vs factura de compra).
processVentas lee desde operaciones_sii.ventas y produce:
| Caso del documento | Concepto neto | Concepto IVA | Lógica |
|---|---|---|---|
| Venta afecta | VENTA-PRD-NET | VENTA-PRD-IVA | Neto e IVA débito tal cual. |
Venta exenta (monto_exento > 0 && monto_neto === 0) | VENTA-EXE-NET | — | Usa monto_exento. |
| Nota de crédito de venta | NC-VENTA-NET | NC-VENTA-IVA | Signos invertidos (montos negativos). |
| Nota de débito de venta | ND-VENTA-NET | ND-VENTA-IVA | Signos positivos (asegura Math.abs). |
| Factura de compra en libro de ventas | FACT-COMP-NET | FACT-COMP-IVA-RET | RUT cliente como emisor; IVA = monto_iva - retenido_total - retenido_parcial. |
Detección de NC/ND/exenta es por substring case-insensitive de tipo_doc:
const esNC = v.tipo_doc?.toLowerCase().includes('nota de crédito') || v.tipo_doc.includes('nc');const esND = v.tipo_doc?.toLowerCase().includes('nota de débito') || v.tipo_doc.includes('nd');const esExento = Number(v.monto_exento) > 0 && Number(v.monto_neto) === 0;const esFacturaCompra = v.tipo_doc?.toLowerCase().includes('factura de compra');Factura de compra — IVA retenido:
const montoIva = Number(v.monto_iva || 0);const ivaRetenido = Number(v.iva_retenido_total || 0) + Number(v.iva_retenido_parcial || 0);return Math.max(0, montoIva - ivaRetenido); // nunca negativoprocessCompras lee desde operaciones_sii.compras con LEFT JOIN + json_agg a compras_otros_impuestos. Genera hasta 5 tipos de línea por documento:
| Línea | Concepto | Cuándo se genera |
|---|---|---|
| Neto | COMP-MER-NET / NC-COMPRA-NET / ND-COMPRA-NET | Siempre. |
| IVA recuperable | COMP-MER-IVA / NC-COMPRA-IVA / ND-COMPRA-IVA | Si monto_iva_recuperable !== 0. |
| IVA no recuperable | ID fijo 30 (“Gasto IVA No Recuperable”) | Si monto_iva_no_recuperable !== 0. |
| Otros impuestos | COMP-OI-{código} o COMP-OI (fallback) | Por cada fila de compras_otros_impuestos con valor ≠ 0. |
| Redondeo | COMP-RDO | Si Math.abs(monto_total - sum(componentes)) ∈ {1, 2}. |
Otros impuestos — lookup en dos pasos (específico, luego genérico):
try { oiConceptId = getConceptId(`COMP-OI-${oi.codigo_impuesto}`); // ej COMP-OI-27 (tabaco)} catch { try { oiConceptId = getConceptId('COMP-OI'); // genérico } catch { continue; // sin concepto configurado → omitir silenciosamente }}Redondeo — solo se registra si Math.abs(diff) ∈ {1, 2}:
const expected = monto_neto + monto_iva_recuperable + monto_iva_no_recuperable + monto_neto_activo_fijo + iva_activo_fijo + monto_exento + sumOtros;const diff = Math.round(monto_total - expected);if (diff === 0 || Math.abs(diff) > 2) return; // ignoradoDiferencias > 2 pesos indican un problema real (concepto faltante, error de carga); no son redondeo.
processBoletas es el caso más simple — siempre 2 líneas:
| Línea | Concepto | Monto |
|---|---|---|
| Neto | VENTA-BOL-NET | b.neto |
| IVA | VENTA-BOL-IVA | b.iva |
Normalizaciones:
tipo_dtecae a'39'si la DB traeNULL.razon_socialsiempre'Consumidor Final'.rut_emisor: null,rut_receptordeb.rut_receptor.
Contabilización de ingresos (la opción contabilizar_ingresos)
Section titled “Contabilización de ingresos (la opción contabilizar_ingresos)”contabilizar_ingresos: true no genera nuevas líneas — busca las líneas netas de venta/boleta ya insertadas en compras_ventas_detalle (en estado PENDIENTE) y las marca como contabilizadas.
Códigos considerados “contabilizables” (constantes en ventasContabilizables.ts):
export const VENTAS_CONTABILIZABLES_CODES = [ 'VENTA-PRD-NET', 'VENTA-BOL-NET', 'VENTA-EXE-NET', 'NC-VENTA-NET', 'ND-VENTA-NET', 'FACT-COMP-NET',] as const;Cambios aplicados a la fila
Section titled “Cambios aplicados a la fila”| Campo | Valor nuevo |
|---|---|
estado | 'CONTABILIZADO' |
referencia | 'EXISTENCIAS' |
fecha_contabilizacion | fecha_documento |
updated_by / updated_at | usuario / NOW() |
Hook emitido
Section titled “Hook emitido”Por cada documento marcado, el repository devuelve un payload y el service lo encola al outbox:
outbox.queue('venta:contabilizada', {ventaId: '...',documentoTipo: 'BOLETA' | 'FACTURA',rutCliente: '...',monto: 0,formaPago: '...',});El payload se enriquece con tenantDb, userId, requestId al hacer flushOutbox post-COMMIT — ver BaseService › Outbox Transaccional. Catálogo completo en Domain Events.
reversarContabilizacionIngresos
Section titled “reversarContabilizacionIngresos”Inverso del paso anterior — vuelve las líneas a PENDIENTE. Solo aplica a VENTA y BOLETA.
-
Valida
tipo_documento ∈ { VENTA, BOLETA }(rechaza COMPRA). -
Busca líneas con
estado IN ('CONTABILIZADO', 'PAGADO')yreferencia = 'EXISTENCIAS'. -
UPDATE:
SET estado = 'PENDIENTE',referencia = NULL,fecha_contabilizacion = NULL,updated_by = $user,updated_at = CURRENT_TIMESTAMP -
Retorna
{ revertidos: <count> }.
Consultas
Section titled “Consultas”getDetails(ctx, filter)
Section titled “getDetails(ctx, filter)”Lista documentos fuente normalizados (no el detalle generado). Acepta:
interface OperationsFilter { tipo_documento?: 'COMPRA' | 'VENTA' | 'BOLETA'; periodo?: string; // YYYY-MM (atajo) periodo_desde?: string; // YYYY-MM periodo_hasta?: string; limit?: number; offset?: number;}| Tipo | Fuente | Normalización |
|---|---|---|
VENTA | fetchVentas | Campos directos; rut_cliente queda como emisor lógico. |
COMPRA | fetchCompras | monto_iva se expone desde monto_iva_recuperable. |
BOLETA | fetchBoletas | documento_tipo = 'Boleta', razon_social_emisor = 'Cliente Boleta'. |
estadoContabilizacion(ctx, desde, hasta)
Section titled “estadoContabilizacion(ctx, desde, hasta)”Resumen por tipo (VENTA/COMPRA/BOLETA) que compara documentos fuente vs documentos con detalle generado:
| Estado general | Condición |
|---|---|
VACIO | Sin documentos fuente ni detalle. |
PENDIENTE | Hay fuentes, sin detalle generado. |
VALIDADO | Todos los documentos tienen detalle en PENDIENTE. |
CONTABILIZADO | Todos los detalles CONTABILIZADO o PAGADO. |
PAGADO | Todos los detalles PAGADO. |
PARCIAL | Mezcla de estados o avance incompleto. |
INCONSISTENTE | Hay faltantes o sobrantes entre origen y detalle. |
Cada bucket incluye contadores: total_origen, total_detalle, validados, contabilizados, pagados, anulados, faltantes, sobrantes, procesados, pendientes.
Esquema de persistencia
Section titled “Esquema de persistencia”compras_ventas_detalle — destino del detalle
Section titled “compras_ventas_detalle — destino del detalle”| Campo | Notas |
|---|---|
tipo_documento | VENTA | COMPRA | BOLETA. |
documento_id | FK al documento fuente. |
tipo_dte | Tipo DTE SII o normalizado ('39' boletas). |
folio | Folio del documento. |
fecha_documento | Del documento fuente. |
rut_emisor | Proveedor (compras), cliente en facturas de compra, NULL en boletas. |
rut_receptor | Cliente (ventas/boletas), NULL en compras. |
razon_social | Nombre asociado al documento. |
año, mes | Período contable. |
concepto_id | FK a conceptos_operaciones. |
monto | Con signo según documento (NC negativo, ND positivo). |
glosa | Generada por el service. |
origen | 'AUTOMATICO' (este service). |
estado | PENDIENTE → CONTABILIZADO → PAGADO. |
referencia | Marca usada por flujos posteriores ('EXISTENCIAS'). |
Catálogo conceptos_operaciones
Section titled “Catálogo conceptos_operaciones”Cada concepto tiene id y codigo semántico. El service resuelve por código, no por id (excepción: IVA no recuperable). Esto permite que el catálogo evolucione sin tocar el service.
| Campo conceptual | Uso |
|---|---|
id | Se persiste en compras_ventas_detalle.concepto_id. |
codigo | COMP-MER-NET, VENTA-PRD-IVA, etc. |
activo | Solo activo = true participa. |
Reglas de diseño
Section titled “Reglas de diseño”| Regla | Motivo |
|---|---|
| Resolver conceptos por código (no por id). | Catálogo legible, configurable por tenant, robusto a migraciones de IDs. |
Bloquear reprocesos sin forzar_reproceso. | Evita duplicar detalle operacional por el mismo período. |
Mantener compras/ventas/boletas en compras_ventas_detalle. | Simplifica reportes y conciliación por período. |
| Emitir hook solo al contabilizar ingresos netos. | Separa “preparar detalle” (idempotente, mecánico) de “señal contable” (reactiva). |
| Omitir otros impuestos sin concepto configurado. | Configuraciones parciales no bloquean el período completo. |
| Registrar redondeos solo hasta 2 pesos. | Distingue ajustes menores de errores reales que requieren revisión. |
| IVA no recuperable como línea separada (no parte del IVA). | Se contabiliza como gasto, no se acredita en F29. |
Endpoints
Section titled “Endpoints”Documentados en Operaciones API. Resumen:
| Método | Ruta | Service call |
|---|---|---|
GET | /api/operaciones/documentos | getDetails(ctx, filter) |
GET | /api/operaciones/estado-contabilizacion | estadoContabilizacion(ctx, desde, hasta) |
POST | /api/operaciones/generate | generate(ctx, params) |
POST | /api/operaciones/reversar-contabilizacion | reversarContabilizacionIngresos(ctx, ...) |