Skip to content

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á.

HaceNo 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.
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
MétodoPara
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.
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
OperationsGenerateParams
interface OperationsGenerateParams {
tipo_documento: 'COMPRA' | 'VENTA' | 'BOLETA';
periodo_desde?: string; // YYYY-MM
periodo_hasta?: string; // YYYY-MM
forzar_reproceso?: boolean;
contabilizar_ingresos?: boolean;
}

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 documentoConcepto netoConcepto IVALógica
Venta afectaVENTA-PRD-NETVENTA-PRD-IVANeto e IVA débito tal cual.
Venta exenta (monto_exento > 0 && monto_neto === 0)VENTA-EXE-NETUsa monto_exento.
Nota de crédito de ventaNC-VENTA-NETNC-VENTA-IVASignos invertidos (montos negativos).
Nota de débito de ventaND-VENTA-NETND-VENTA-IVASignos positivos (asegura Math.abs).
Factura de compra en libro de ventasFACT-COMP-NETFACT-COMP-IVA-RETRUT 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 negativo

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;
CampoValor nuevo
estado'CONTABILIZADO'
referencia'EXISTENCIAS'
fecha_contabilizacionfecha_documento
updated_by / updated_atusuario / NOW()

Por cada documento marcado, el repository devuelve un payload y el service lo encola al outbox:

Payload del hook
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.

Inverso del paso anterior — vuelve las líneas a PENDIENTE. Solo aplica a VENTA y BOLETA.

  1. Valida tipo_documento ∈ { VENTA, BOLETA } (rechaza COMPRA).

  2. Busca líneas con estado IN ('CONTABILIZADO', 'PAGADO') y referencia = 'EXISTENCIAS'.

  3. UPDATE:

    SET estado = 'PENDIENTE',
    referencia = NULL,
    fecha_contabilizacion = NULL,
    updated_by = $user,
    updated_at = CURRENT_TIMESTAMP
  4. Retorna { revertidos: <count> }.

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;
}
TipoFuenteNormalización
VENTAfetchVentasCampos directos; rut_cliente queda como emisor lógico.
COMPRAfetchComprasmonto_iva se expone desde monto_iva_recuperable.
BOLETAfetchBoletasdocumento_tipo = 'Boleta', razon_social_emisor = 'Cliente Boleta'.

Resumen por tipo (VENTA/COMPRA/BOLETA) que compara documentos fuente vs documentos con detalle generado:

Estado generalCondición
VACIOSin documentos fuente ni detalle.
PENDIENTEHay fuentes, sin detalle generado.
VALIDADOTodos los documentos tienen detalle en PENDIENTE.
CONTABILIZADOTodos los detalles CONTABILIZADO o PAGADO.
PAGADOTodos los detalles PAGADO.
PARCIALMezcla de estados o avance incompleto.
INCONSISTENTEHay faltantes o sobrantes entre origen y detalle.

Cada bucket incluye contadores: total_origen, total_detalle, validados, contabilizados, pagados, anulados, faltantes, sobrantes, procesados, pendientes.

compras_ventas_detalle — destino del detalle

Section titled “compras_ventas_detalle — destino del detalle”
CampoNotas
tipo_documentoVENTA | COMPRA | BOLETA.
documento_idFK al documento fuente.
tipo_dteTipo DTE SII o normalizado ('39' boletas).
folioFolio del documento.
fecha_documentoDel documento fuente.
rut_emisorProveedor (compras), cliente en facturas de compra, NULL en boletas.
rut_receptorCliente (ventas/boletas), NULL en compras.
razon_socialNombre asociado al documento.
año, mesPeríodo contable.
concepto_idFK a conceptos_operaciones.
montoCon signo según documento (NC negativo, ND positivo).
glosaGenerada por el service.
origen'AUTOMATICO' (este service).
estadoPENDIENTECONTABILIZADOPAGADO.
referenciaMarca usada por flujos posteriores ('EXISTENCIAS').

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 conceptualUso
idSe persiste en compras_ventas_detalle.concepto_id.
codigoCOMP-MER-NET, VENTA-PRD-IVA, etc.
activoSolo activo = true participa.
ReglaMotivo
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.

Documentados en Operaciones API. Resumen:

MétodoRutaService call
GET/api/operaciones/documentosgetDetails(ctx, filter)
GET/api/operaciones/estado-contabilizacionestadoContabilizacion(ctx, desde, hasta)
POST/api/operaciones/generategenerate(ctx, params)
POST/api/operaciones/reversar-contabilizacionreversarContabilizacionIngresos(ctx, ...)