paperclip-plugin-autopilot-ops 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/manifest.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/manifest.ts
2
2
  var PLUGIN_ID = "alephoo.autopilot-ops";
3
- var PLUGIN_VERSION = "0.1.19";
3
+ var PLUGIN_VERSION = "0.1.21";
4
4
  var DB_NAMESPACE_SLUG = "autopilot_ops";
5
5
  var PAGE_ROUTE = "autopilot";
6
6
  var TOOL_NAMES = {
@@ -113,7 +113,7 @@ var manifest = {
113
113
  {
114
114
  name: TOOL_NAMES.abrirCaso,
115
115
  displayName: "Abrir caso",
116
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`actualizacion_arancel`, `tarea_operativa`, `recupero_turno` (datos: fecha AAAA-MM-DD, hora HH:MM, profesional, especialidad del turno libre; referencia "AAAA-MM-DD HH:MM \xB7 Profesional"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
116
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `circular_aranceles` (una circular de aranceles entera; referencia obligatoria `circular:<obra social>:<hash corto del archivo>`; datos: obra_social, convenio (nombre en el HIS), archivo {nombre, hash, paginas}, vigencia_desde AAAA-MM-DD, porcentaje_declarado y filas_declaradas si la circular los dice), `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`actualizacion_arancel`, `tarea_operativa`, `recupero_turno` (datos: fecha AAAA-MM-DD, hora HH:MM, profesional, especialidad del turno libre; referencia "AAAA-MM-DD HH:MM \xB7 Profesional"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
117
117
  parametersSchema: {
118
118
  type: "object",
119
119
  properties: {
@@ -130,7 +130,7 @@ var manifest = {
130
130
  {
131
131
  name: TOOL_NAMES.transicionar,
132
132
  displayName: "Transicionar caso",
133
- description: 'Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona de la instituci\xF3n carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`. En `recupero_turno`: `proponer` con solicitud_id, solicitud_estado, solicitud_especialidad, cola (la lista de espera pendiente de esa especialidad tal como se vio: [{solicitud_id, prioridad, alta}]; el plugin la ordena y exige justificaci\xF3n para no elegir la primera) y fuente; si la instituci\xF3n pide consulta previa (ops_leer_caso lo dice), `ofrecer` con canal y esperar `aceptar`/`no_acepta`; **antes de escribir en el sistema, `autorizar`** (sin esa autorizaci\xF3n no se escribe); despu\xE9s de escribir, `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo. En `cuenta_completa` el agente solo lee: `observar` con bono_id, bono_estado (no_confirmado, confirmado o prefacturado), faltantes [{regla, detalle, item?}] con reglas del cat\xE1logo (autorizacion_faltante, validacion_faltante, afiliado_faltante, orden_faltante, prestacion_sin_valor, epicrisis_faltante), lectura_id (la lectura registrada con ops_registrar_accion, conector "base") y fuente; `comprobar` con faltantes_leidos (lista vac\xEDa si ya est\xE1 completo), bono_estado_leido y lectura_id; `descartar` con motivo si el bono se elimin\xF3 o anul\xF3. En `facturacion_pendiente` (solo lee): `observar` con bono_id, bono_estado, fecha_prestacion, obra_social, lectura_id (conector "base") y fuente; `comprobar` con bono_estado_leido y lectura_id (facturado/refacturado \u2192 facturada; prefacturado \u2192 en prefactura; si no, sigue por presentar); `descartar` con motivo. En `demanda_pendiente`: `contactar` (nivel ejecuta; datos: canal igual al de la regla y accion_id de la escritura registrada; el plugin controla regla activa, intentos, espera entre intentos y horario), `responder` (respuesta: acepta|rechaza|no_contesta), `comprobar` (lectura_id de la relectura del HIS, turno_id y turno_estado del turno encontrado), `cerrar` (motivo; solo con los intentos agotados o el plazo vencido), `descartar` (motivo). `credencial_profesional` (solo lee): `verificar` con profesional_id, matricula y tipo_matricula (nacional|provincial) si hay matr\xEDcula, resultado = el objeto que devuelve verificar-matriculas.mjs tal cual ({sin_matricula, encontrada, coincide_nombre, situacion, sancion, profesion, provincia, emisor}), lectura_id (conector "api", la consulta al registro) y fuente; el plugin decide vigente u observada y fija la observaci\xF3n (sin_matricula, no_figura, otra_persona, inhabilitada, sancion); `descartar` con motivo si el profesional ya no est\xE1 activo.',
133
+ description: 'Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona de la instituci\xF3n carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`. En `recupero_turno`: `proponer` con solicitud_id, solicitud_estado, solicitud_especialidad, cola (la lista de espera pendiente de esa especialidad tal como se vio: [{solicitud_id, prioridad, alta}]; el plugin la ordena y exige justificaci\xF3n para no elegir la primera) y fuente; si la instituci\xF3n pide consulta previa (ops_leer_caso lo dice), `ofrecer` con canal y esperar `aceptar`/`no_acepta`; **antes de escribir en el sistema, `autorizar`** (sin esa autorizaci\xF3n no se escribe); despu\xE9s de escribir, `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo. En `cuenta_completa` el agente solo lee: `observar` con bono_id, bono_estado (no_confirmado, confirmado o prefacturado), faltantes [{regla, detalle, item?}] con reglas del cat\xE1logo (autorizacion_faltante, validacion_faltante, afiliado_faltante, orden_faltante, prestacion_sin_valor, epicrisis_faltante), lectura_id (la lectura registrada con ops_registrar_accion, conector "base") y fuente; `comprobar` con faltantes_leidos (lista vac\xEDa si ya est\xE1 completo), bono_estado_leido y lectura_id; `descartar` con motivo si el bono se elimin\xF3 o anul\xF3. En `facturacion_pendiente` (solo lee): `observar` con bono_id, bono_estado, fecha_prestacion, obra_social, lectura_id (conector "base") y fuente; `comprobar` con bono_estado_leido y lectura_id (facturado/refacturado \u2192 facturada; prefacturado \u2192 en prefactura; si no, sigue por presentar); `descartar` con motivo. En `circular_aranceles`: `extraer` con extraccion_a (lo que ley\xF3 el agente) y extraccion_b (la salida de leer-circular.mjs, tal cual) como [{codigo, nombre, valor_nuevo}], actual (la salida de leer-convenio.mjs: [{codigo, nombre, componentes:[{arancel, monto}], copago, tipo_copago}]), lectura_id (lectura del convenio, conector base o api), lectura_circular_id (lectura de la circular, conector archivo) y fuente; el plugin cruza las tres fuentes y decide qu\xE9 se propone, qu\xE9 se revisa y qu\xE9 es excepci\xF3n (el agente no decide eso); `proponer` sin datos; despu\xE9s de la carga, `verificar` con leidos (la salida de leer-convenio.mjs tras la carga) y lectura_id; `cancelar` con motivo. En `demanda_pendiente`: `contactar` (nivel ejecuta; datos: canal igual al de la regla y accion_id de la escritura registrada; el plugin controla regla activa, intentos, espera entre intentos y horario), `responder` (respuesta: acepta|rechaza|no_contesta), `comprobar` (lectura_id de la relectura del HIS, turno_id y turno_estado del turno encontrado), `cerrar` (motivo; solo con los intentos agotados o el plazo vencido), `descartar` (motivo). `credencial_profesional` (solo lee): `verificar` con profesional_id, matricula y tipo_matricula (nacional|provincial) si hay matr\xEDcula, resultado = el objeto que devuelve verificar-matriculas.mjs tal cual ({sin_matricula, encontrada, coincide_nombre, situacion, sancion, profesion, provincia, emisor}), lectura_id (conector "api", la consulta al registro) y fuente; el plugin decide vigente u observada y fija la observaci\xF3n (sin_matricula, no_figura, otra_persona, inhabilitada, sancion); `descartar` con motivo si el profesional ya no est\xE1 activo.',
134
134
  parametersSchema: {
135
135
  type: "object",
136
136
  properties: {
package/dist/ui/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  // src/manifest.ts
10
10
  var PLUGIN_ID = "alephoo.autopilot-ops";
11
- var PLUGIN_VERSION = "0.1.19";
11
+ var PLUGIN_VERSION = "0.1.21";
12
12
  var DB_NAMESPACE_SLUG = "autopilot_ops";
13
13
  var PAGE_ROUTE = "autopilot";
14
14
  var TOOL_NAMES = {
@@ -121,7 +121,7 @@ var manifest = {
121
121
  {
122
122
  name: TOOL_NAMES.abrirCaso,
123
123
  displayName: "Abrir caso",
124
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`actualizacion_arancel`, `tarea_operativa`, `recupero_turno` (datos: fecha AAAA-MM-DD, hora HH:MM, profesional, especialidad del turno libre; referencia "AAAA-MM-DD HH:MM \xB7 Profesional"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
124
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `circular_aranceles` (una circular de aranceles entera; referencia obligatoria `circular:<obra social>:<hash corto del archivo>`; datos: obra_social, convenio (nombre en el HIS), archivo {nombre, hash, paginas}, vigencia_desde AAAA-MM-DD, porcentaje_declarado y filas_declaradas si la circular los dice), `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`actualizacion_arancel`, `tarea_operativa`, `recupero_turno` (datos: fecha AAAA-MM-DD, hora HH:MM, profesional, especialidad del turno libre; referencia "AAAA-MM-DD HH:MM \xB7 Profesional"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
125
125
  parametersSchema: {
126
126
  type: "object",
127
127
  properties: {
@@ -138,7 +138,7 @@ var manifest = {
138
138
  {
139
139
  name: TOOL_NAMES.transicionar,
140
140
  displayName: "Transicionar caso",
141
- description: 'Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona de la instituci\xF3n carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`. En `recupero_turno`: `proponer` con solicitud_id, solicitud_estado, solicitud_especialidad, cola (la lista de espera pendiente de esa especialidad tal como se vio: [{solicitud_id, prioridad, alta}]; el plugin la ordena y exige justificaci\xF3n para no elegir la primera) y fuente; si la instituci\xF3n pide consulta previa (ops_leer_caso lo dice), `ofrecer` con canal y esperar `aceptar`/`no_acepta`; **antes de escribir en el sistema, `autorizar`** (sin esa autorizaci\xF3n no se escribe); despu\xE9s de escribir, `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo. En `cuenta_completa` el agente solo lee: `observar` con bono_id, bono_estado (no_confirmado, confirmado o prefacturado), faltantes [{regla, detalle, item?}] con reglas del cat\xE1logo (autorizacion_faltante, validacion_faltante, afiliado_faltante, orden_faltante, prestacion_sin_valor, epicrisis_faltante), lectura_id (la lectura registrada con ops_registrar_accion, conector "base") y fuente; `comprobar` con faltantes_leidos (lista vac\xEDa si ya est\xE1 completo), bono_estado_leido y lectura_id; `descartar` con motivo si el bono se elimin\xF3 o anul\xF3. En `facturacion_pendiente` (solo lee): `observar` con bono_id, bono_estado, fecha_prestacion, obra_social, lectura_id (conector "base") y fuente; `comprobar` con bono_estado_leido y lectura_id (facturado/refacturado \u2192 facturada; prefacturado \u2192 en prefactura; si no, sigue por presentar); `descartar` con motivo. En `demanda_pendiente`: `contactar` (nivel ejecuta; datos: canal igual al de la regla y accion_id de la escritura registrada; el plugin controla regla activa, intentos, espera entre intentos y horario), `responder` (respuesta: acepta|rechaza|no_contesta), `comprobar` (lectura_id de la relectura del HIS, turno_id y turno_estado del turno encontrado), `cerrar` (motivo; solo con los intentos agotados o el plazo vencido), `descartar` (motivo). `credencial_profesional` (solo lee): `verificar` con profesional_id, matricula y tipo_matricula (nacional|provincial) si hay matr\xEDcula, resultado = el objeto que devuelve verificar-matriculas.mjs tal cual ({sin_matricula, encontrada, coincide_nombre, situacion, sancion, profesion, provincia, emisor}), lectura_id (conector "api", la consulta al registro) y fuente; el plugin decide vigente u observada y fija la observaci\xF3n (sin_matricula, no_figura, otra_persona, inhabilitada, sancion); `descartar` con motivo si el profesional ya no est\xE1 activo.',
141
+ description: 'Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona de la instituci\xF3n carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`. En `recupero_turno`: `proponer` con solicitud_id, solicitud_estado, solicitud_especialidad, cola (la lista de espera pendiente de esa especialidad tal como se vio: [{solicitud_id, prioridad, alta}]; el plugin la ordena y exige justificaci\xF3n para no elegir la primera) y fuente; si la instituci\xF3n pide consulta previa (ops_leer_caso lo dice), `ofrecer` con canal y esperar `aceptar`/`no_acepta`; **antes de escribir en el sistema, `autorizar`** (sin esa autorizaci\xF3n no se escribe); despu\xE9s de escribir, `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo. En `cuenta_completa` el agente solo lee: `observar` con bono_id, bono_estado (no_confirmado, confirmado o prefacturado), faltantes [{regla, detalle, item?}] con reglas del cat\xE1logo (autorizacion_faltante, validacion_faltante, afiliado_faltante, orden_faltante, prestacion_sin_valor, epicrisis_faltante), lectura_id (la lectura registrada con ops_registrar_accion, conector "base") y fuente; `comprobar` con faltantes_leidos (lista vac\xEDa si ya est\xE1 completo), bono_estado_leido y lectura_id; `descartar` con motivo si el bono se elimin\xF3 o anul\xF3. En `facturacion_pendiente` (solo lee): `observar` con bono_id, bono_estado, fecha_prestacion, obra_social, lectura_id (conector "base") y fuente; `comprobar` con bono_estado_leido y lectura_id (facturado/refacturado \u2192 facturada; prefacturado \u2192 en prefactura; si no, sigue por presentar); `descartar` con motivo. En `circular_aranceles`: `extraer` con extraccion_a (lo que ley\xF3 el agente) y extraccion_b (la salida de leer-circular.mjs, tal cual) como [{codigo, nombre, valor_nuevo}], actual (la salida de leer-convenio.mjs: [{codigo, nombre, componentes:[{arancel, monto}], copago, tipo_copago}]), lectura_id (lectura del convenio, conector base o api), lectura_circular_id (lectura de la circular, conector archivo) y fuente; el plugin cruza las tres fuentes y decide qu\xE9 se propone, qu\xE9 se revisa y qu\xE9 es excepci\xF3n (el agente no decide eso); `proponer` sin datos; despu\xE9s de la carga, `verificar` con leidos (la salida de leer-convenio.mjs tras la carga) y lectura_id; `cancelar` con motivo. En `demanda_pendiente`: `contactar` (nivel ejecuta; datos: canal igual al de la regla y accion_id de la escritura registrada; el plugin controla regla activa, intentos, espera entre intentos y horario), `responder` (respuesta: acepta|rechaza|no_contesta), `comprobar` (lectura_id de la relectura del HIS, turno_id y turno_estado del turno encontrado), `cerrar` (motivo; solo con los intentos agotados o el plazo vencido), `descartar` (motivo). `credencial_profesional` (solo lee): `verificar` con profesional_id, matricula y tipo_matricula (nacional|provincial) si hay matr\xEDcula, resultado = el objeto que devuelve verificar-matriculas.mjs tal cual ({sin_matricula, encontrada, coincide_nombre, situacion, sancion, profesion, provincia, emisor}), lectura_id (conector "api", la consulta al registro) y fuente; el plugin decide vigente u observada y fija la observaci\xF3n (sin_matricula, no_figura, otra_persona, inhabilitada, sancion); `descartar` con motivo si el profesional ya no est\xE1 activo.',
142
142
  parametersSchema: {
143
143
  type: "object",
144
144
  properties: {
package/dist/worker.js CHANGED
@@ -39932,7 +39932,7 @@ function startWorkerRpcHost(options) {
39932
39932
 
39933
39933
  // src/manifest.ts
39934
39934
  var PLUGIN_ID = "alephoo.autopilot-ops";
39935
- var PLUGIN_VERSION = "0.1.19";
39935
+ var PLUGIN_VERSION = "0.1.21";
39936
39936
  var DB_NAMESPACE_SLUG = "autopilot_ops";
39937
39937
  var PAGE_ROUTE = "autopilot";
39938
39938
  var TOOL_NAMES = {
@@ -40045,7 +40045,7 @@ var manifest = {
40045
40045
  {
40046
40046
  name: TOOL_NAMES.abrirCaso,
40047
40047
  displayName: "Abrir caso",
40048
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`actualizacion_arancel`, `tarea_operativa`, `recupero_turno` (datos: fecha AAAA-MM-DD, hora HH:MM, profesional, especialidad del turno libre; referencia "AAAA-MM-DD HH:MM \xB7 Profesional"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
40048
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `circular_aranceles` (una circular de aranceles entera; referencia obligatoria `circular:<obra social>:<hash corto del archivo>`; datos: obra_social, convenio (nombre en el HIS), archivo {nombre, hash, paginas}, vigencia_desde AAAA-MM-DD, porcentaje_declarado y filas_declaradas si la circular los dice), `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`actualizacion_arancel`, `tarea_operativa`, `recupero_turno` (datos: fecha AAAA-MM-DD, hora HH:MM, profesional, especialidad del turno libre; referencia "AAAA-MM-DD HH:MM \xB7 Profesional"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos, internacion_id opcional; en faltantes, `responsable` {tipo profesional, id, nombre} opcional para que el plugin avise a quien resuelve), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal de la instituci\xF3n), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
40049
40049
  parametersSchema: {
40050
40050
  type: "object",
40051
40051
  properties: {
@@ -40062,7 +40062,7 @@ var manifest = {
40062
40062
  {
40063
40063
  name: TOOL_NAMES.transicionar,
40064
40064
  displayName: "Transicionar caso",
40065
- description: 'Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona de la instituci\xF3n carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`. En `recupero_turno`: `proponer` con solicitud_id, solicitud_estado, solicitud_especialidad, cola (la lista de espera pendiente de esa especialidad tal como se vio: [{solicitud_id, prioridad, alta}]; el plugin la ordena y exige justificaci\xF3n para no elegir la primera) y fuente; si la instituci\xF3n pide consulta previa (ops_leer_caso lo dice), `ofrecer` con canal y esperar `aceptar`/`no_acepta`; **antes de escribir en el sistema, `autorizar`** (sin esa autorizaci\xF3n no se escribe); despu\xE9s de escribir, `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo. En `cuenta_completa` el agente solo lee: `observar` con bono_id, bono_estado (no_confirmado, confirmado o prefacturado), faltantes [{regla, detalle, item?}] con reglas del cat\xE1logo (autorizacion_faltante, validacion_faltante, afiliado_faltante, orden_faltante, prestacion_sin_valor, epicrisis_faltante), lectura_id (la lectura registrada con ops_registrar_accion, conector "base") y fuente; `comprobar` con faltantes_leidos (lista vac\xEDa si ya est\xE1 completo), bono_estado_leido y lectura_id; `descartar` con motivo si el bono se elimin\xF3 o anul\xF3. En `facturacion_pendiente` (solo lee): `observar` con bono_id, bono_estado, fecha_prestacion, obra_social, lectura_id (conector "base") y fuente; `comprobar` con bono_estado_leido y lectura_id (facturado/refacturado \u2192 facturada; prefacturado \u2192 en prefactura; si no, sigue por presentar); `descartar` con motivo. En `demanda_pendiente`: `contactar` (nivel ejecuta; datos: canal igual al de la regla y accion_id de la escritura registrada; el plugin controla regla activa, intentos, espera entre intentos y horario), `responder` (respuesta: acepta|rechaza|no_contesta), `comprobar` (lectura_id de la relectura del HIS, turno_id y turno_estado del turno encontrado), `cerrar` (motivo; solo con los intentos agotados o el plazo vencido), `descartar` (motivo). `credencial_profesional` (solo lee): `verificar` con profesional_id, matricula y tipo_matricula (nacional|provincial) si hay matr\xEDcula, resultado = el objeto que devuelve verificar-matriculas.mjs tal cual ({sin_matricula, encontrada, coincide_nombre, situacion, sancion, profesion, provincia, emisor}), lectura_id (conector "api", la consulta al registro) y fuente; el plugin decide vigente u observada y fija la observaci\xF3n (sin_matricula, no_figura, otra_persona, inhabilitada, sancion); `descartar` con motivo si el profesional ya no est\xE1 activo.',
40065
+ description: 'Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona de la instituci\xF3n carga a mano lo aprobado, el caso queda en `aplicado`: el agente lo comprueba releyendo el sistema y llamando `verificar` con `datos.valor_leido`. En `recupero_turno`: `proponer` con solicitud_id, solicitud_estado, solicitud_especialidad, cola (la lista de espera pendiente de esa especialidad tal como se vio: [{solicitud_id, prioridad, alta}]; el plugin la ordena y exige justificaci\xF3n para no elegir la primera) y fuente; si la instituci\xF3n pide consulta previa (ops_leer_caso lo dice), `ofrecer` con canal y esperar `aceptar`/`no_acepta`; **antes de escribir en el sistema, `autorizar`** (sin esa autorizaci\xF3n no se escribe); despu\xE9s de escribir, `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo. En `cuenta_completa` el agente solo lee: `observar` con bono_id, bono_estado (no_confirmado, confirmado o prefacturado), faltantes [{regla, detalle, item?}] con reglas del cat\xE1logo (autorizacion_faltante, validacion_faltante, afiliado_faltante, orden_faltante, prestacion_sin_valor, epicrisis_faltante), lectura_id (la lectura registrada con ops_registrar_accion, conector "base") y fuente; `comprobar` con faltantes_leidos (lista vac\xEDa si ya est\xE1 completo), bono_estado_leido y lectura_id; `descartar` con motivo si el bono se elimin\xF3 o anul\xF3. En `facturacion_pendiente` (solo lee): `observar` con bono_id, bono_estado, fecha_prestacion, obra_social, lectura_id (conector "base") y fuente; `comprobar` con bono_estado_leido y lectura_id (facturado/refacturado \u2192 facturada; prefacturado \u2192 en prefactura; si no, sigue por presentar); `descartar` con motivo. En `circular_aranceles`: `extraer` con extraccion_a (lo que ley\xF3 el agente) y extraccion_b (la salida de leer-circular.mjs, tal cual) como [{codigo, nombre, valor_nuevo}], actual (la salida de leer-convenio.mjs: [{codigo, nombre, componentes:[{arancel, monto}], copago, tipo_copago}]), lectura_id (lectura del convenio, conector base o api), lectura_circular_id (lectura de la circular, conector archivo) y fuente; el plugin cruza las tres fuentes y decide qu\xE9 se propone, qu\xE9 se revisa y qu\xE9 es excepci\xF3n (el agente no decide eso); `proponer` sin datos; despu\xE9s de la carga, `verificar` con leidos (la salida de leer-convenio.mjs tras la carga) y lectura_id; `cancelar` con motivo. En `demanda_pendiente`: `contactar` (nivel ejecuta; datos: canal igual al de la regla y accion_id de la escritura registrada; el plugin controla regla activa, intentos, espera entre intentos y horario), `responder` (respuesta: acepta|rechaza|no_contesta), `comprobar` (lectura_id de la relectura del HIS, turno_id y turno_estado del turno encontrado), `cerrar` (motivo; solo con los intentos agotados o el plazo vencido), `descartar` (motivo). `credencial_profesional` (solo lee): `verificar` con profesional_id, matricula y tipo_matricula (nacional|provincial) si hay matr\xEDcula, resultado = el objeto que devuelve verificar-matriculas.mjs tal cual ({sin_matricula, encontrada, coincide_nombre, situacion, sancion, profesion, provincia, emisor}), lectura_id (conector "api", la consulta al registro) y fuente; el plugin decide vigente u observada y fija la observaci\xF3n (sin_matricula, no_figura, otra_persona, inhabilitada, sancion); `descartar` con motivo si el profesional ya no est\xE1 activo.',
40066
40066
  parametersSchema: {
40067
40067
  type: "object",
40068
40068
  properties: {
@@ -40375,6 +40375,258 @@ var manifest_default = manifest;
40375
40375
  // src/ops.ts
40376
40376
  import { randomUUID as randomUUID2 } from "node:crypto";
40377
40377
 
40378
+ // src/aranceles.ts
40379
+ import { createHash } from "node:crypto";
40380
+ var TOLERANCIA_EXTRACCIONES = 5e-3;
40381
+ var TOLERANCIA_FILAS = 0.1;
40382
+ var DESVIO_COHERENCIA = 15;
40383
+ var PROPORCION_INCOHERENTE = 0.1;
40384
+ var TOPE_VARIACION_POR_DEFECTO = 50;
40385
+ var LIMITES_TOPE = [10, 200];
40386
+ var VIGENCIA_DIAS_ATRAS = 60;
40387
+ var VIGENCIA_DIAS_ADELANTE = 120;
40388
+ var TOLERANCIA_RELECTURA = 0.01;
40389
+ var redondear = (n, dec = 2) => Math.round(n * 10 ** dec) / 10 ** dec;
40390
+ var pesos = (n) => `$ ${n.toLocaleString("es-AR", { maximumFractionDigits: 2 })}`;
40391
+ var porc = (n) => `${n.toLocaleString("es-AR", { maximumFractionDigits: 1 })} %`;
40392
+ function normalizarCodigo(v) {
40393
+ const s = typeof v === "number" ? String(v) : typeof v === "string" ? v : "";
40394
+ return s.toUpperCase().replace(/[^A-Z0-9]/g, "").replace(/^0+(?=.)/, "");
40395
+ }
40396
+ function leerMonto(v) {
40397
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
40398
+ if (typeof v !== "string") return null;
40399
+ let s = v.replace(/[$\s]/g, "").replace(/ARS/i, "");
40400
+ if (!s || !/^-?[\d.,]+$/.test(s)) return null;
40401
+ const coma = s.lastIndexOf(",");
40402
+ const punto = s.lastIndexOf(".");
40403
+ if (coma > -1 && punto > -1) s = coma > punto ? s.replace(/\./g, "").replace(",", ".") : s.replace(/,/g, "");
40404
+ else if (coma > -1) s = /,\d{3}$/.test(s) && s.split(",").length > 2 ? s.replace(/,/g, "") : s.replace(",", ".");
40405
+ else if (punto > -1 && s.split(".").length > 2) s = s.replace(/\./g, "");
40406
+ else if (punto > -1 && /\.\d{3}$/.test(s)) s = s.replace(".", "");
40407
+ const n = Number(s);
40408
+ return Number.isFinite(n) ? n : null;
40409
+ }
40410
+ function leerFilas(v, nombre) {
40411
+ if (!Array.isArray(v)) return { error: `${nombre} tiene que ser una lista [{codigo, nombre, valor_nuevo}]` };
40412
+ const filas = [];
40413
+ const vistos = /* @__PURE__ */ new Set();
40414
+ for (const [i, e] of v.entries()) {
40415
+ if (!e || typeof e !== "object") return { error: `${nombre}[${i}] no es una fila` };
40416
+ const r = e;
40417
+ const codigo = normalizarCodigo(r.codigo);
40418
+ if (!codigo) return { error: `${nombre}[${i}] no tiene c\xF3digo` };
40419
+ if (vistos.has(codigo)) return { error: `${nombre} tiene el c\xF3digo ${codigo} repetido` };
40420
+ vistos.add(codigo);
40421
+ const valor = leerMonto(r.valor_nuevo);
40422
+ if (valor === null || valor <= 0) return { error: `${nombre}[${i}] (c\xF3digo ${codigo}): valor_nuevo tiene que ser un n\xFAmero mayor a 0` };
40423
+ filas.push({ codigo, codigo_original: String(r.codigo).trim(), nombre: typeof r.nombre === "string" && r.nombre.trim() ? r.nombre.trim().slice(0, 200) : null, valor_nuevo: redondear(valor) });
40424
+ }
40425
+ return { filas };
40426
+ }
40427
+ function leerActual(v) {
40428
+ if (!Array.isArray(v)) return { error: "actual tiene que ser una lista [{codigo, nombre, componentes: [{arancel, monto}], copago, tipo_copago}]" };
40429
+ const practicas = [];
40430
+ const vistos = /* @__PURE__ */ new Set();
40431
+ for (const [i, e] of v.entries()) {
40432
+ if (!e || typeof e !== "object") return { error: `actual[${i}] no es una pr\xE1ctica` };
40433
+ const r = e;
40434
+ const codigo = normalizarCodigo(r.codigo);
40435
+ if (!codigo) return { error: `actual[${i}] no tiene c\xF3digo` };
40436
+ if (vistos.has(codigo)) continue;
40437
+ vistos.add(codigo);
40438
+ const comps = Array.isArray(r.componentes) ? r.componentes : [];
40439
+ const componentes = [];
40440
+ for (const c of comps) {
40441
+ const cc = c;
40442
+ const arancel = typeof cc.arancel === "string" ? cc.arancel.trim() : "";
40443
+ const monto = leerMonto(cc.monto);
40444
+ if (!arancel || monto === null || monto < 0) return { error: `actual[${i}] (c\xF3digo ${codigo}): cada componente necesita arancel y monto` };
40445
+ componentes.push({ arancel, monto: redondear(monto) });
40446
+ }
40447
+ if (componentes.length === 0) return { error: `actual[${i}] (c\xF3digo ${codigo}): no tiene componentes de arancel` };
40448
+ practicas.push({
40449
+ codigo,
40450
+ codigo_his: typeof r.codigo_his === "string" && r.codigo_his.trim() ? r.codigo_his.trim() : String(r.codigo).trim(),
40451
+ nombre: typeof r.nombre === "string" && r.nombre.trim() ? r.nombre.trim() : codigo,
40452
+ componentes,
40453
+ copago: r.copago === null || r.copago === void 0 ? null : String(r.copago),
40454
+ tipo_copago: r.tipo_copago === null || r.tipo_copago === void 0 ? null : String(r.tipo_copago)
40455
+ });
40456
+ }
40457
+ return { practicas };
40458
+ }
40459
+ function repartir(componentes, nuevo) {
40460
+ const total = componentes.reduce((a, c) => a + c.monto, 0);
40461
+ if (componentes.length === 1) return [{ arancel: componentes[0].arancel, monto: redondear(nuevo) }];
40462
+ if (total <= 0) return null;
40463
+ const out = componentes.map((c) => ({ arancel: c.arancel, monto: redondear(c.monto / total * nuevo) }));
40464
+ const resto = redondear(nuevo - out.reduce((a, c) => a + c.monto, 0));
40465
+ if (resto !== 0) {
40466
+ const mayor = out.reduce((m, c, i) => c.monto > out[m].monto ? i : m, 0);
40467
+ out[mayor].monto = redondear(out[mayor].monto + resto);
40468
+ }
40469
+ return out;
40470
+ }
40471
+ function mediana(v) {
40472
+ if (v.length === 0) return null;
40473
+ const s = [...v].sort((a, b) => a - b);
40474
+ const m = Math.floor(s.length / 2);
40475
+ return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
40476
+ }
40477
+ function sumarDias(fecha, dias) {
40478
+ const d = /* @__PURE__ */ new Date(`${fecha}T00:00:00Z`);
40479
+ d.setUTCDate(d.getUTCDate() + dias);
40480
+ return d.toISOString().slice(0, 10);
40481
+ }
40482
+ function cruzarCircular(input2) {
40483
+ const errores = [];
40484
+ const a = leerFilas(input2.extraccionA, "extraccion_a");
40485
+ const b = leerFilas(input2.extraccionB, "extraccion_b");
40486
+ const act = leerActual(input2.actual);
40487
+ if ("error" in a) errores.push(a.error);
40488
+ if ("error" in b) errores.push(b.error);
40489
+ if ("error" in act) errores.push(act.error);
40490
+ const vigencia = typeof input2.vigenciaDesde === "string" && /^\d{4}-\d{2}-\d{2}$/.test(input2.vigenciaDesde) ? input2.vigenciaDesde : null;
40491
+ if (!vigencia) errores.push("vigencia_desde: la circular tiene que decir desde cu\xE1ndo rige (AAAA-MM-DD)");
40492
+ else if (vigencia < sumarDias(input2.hoy, -VIGENCIA_DIAS_ATRAS) || vigencia > sumarDias(input2.hoy, VIGENCIA_DIAS_ADELANTE)) {
40493
+ errores.push(`vigencia_desde ${vigencia} fuera de rango: se aceptan circulares que rigen desde ${VIGENCIA_DIAS_ATRAS} d\xEDas atr\xE1s hasta ${VIGENCIA_DIAS_ADELANTE} d\xEDas adelante`);
40494
+ }
40495
+ if ("error" in a || "error" in b || "error" in act) return { ok: false, errores };
40496
+ const filasA = a.filas;
40497
+ const filasB = b.filas;
40498
+ const nA = filasA.length;
40499
+ const nB = filasB.length;
40500
+ if (nA === 0 || nB === 0) errores.push("las dos extracciones tienen que traer al menos una fila");
40501
+ else if (Math.abs(nA - nB) / Math.max(nA, nB) > TOLERANCIA_FILAS) {
40502
+ errores.push(`las dos extracciones no coinciden en cantidad de filas (A ${nA}, B ${nB}): hay que volver a leer la circular`);
40503
+ }
40504
+ const declaradas = typeof input2.filasDeclaradas === "number" && input2.filasDeclaradas > 0 ? Math.round(input2.filasDeclaradas) : null;
40505
+ if (declaradas !== null) {
40506
+ for (const [n, cual] of [[nA, "A"], [nB, "B"]]) {
40507
+ if (n > 0 && Math.abs(n - declaradas) / declaradas > TOLERANCIA_FILAS) errores.push(`la extracci\xF3n ${cual} tiene ${n} filas y la circular declara ${declaradas}`);
40508
+ }
40509
+ }
40510
+ if (errores.length) return { ok: false, errores };
40511
+ const tope = typeof input2.topeVariacion === "number" && input2.topeVariacion >= LIMITES_TOPE[0] && input2.topeVariacion <= LIMITES_TOPE[1] ? input2.topeVariacion : TOPE_VARIACION_POR_DEFECTO;
40512
+ const porA = new Map(filasA.map((f) => [f.codigo, f]));
40513
+ const porB = new Map(filasB.map((f) => [f.codigo, f]));
40514
+ const porHis = new Map(act.practicas.map((p) => [p.codigo, p]));
40515
+ const codigos = [.../* @__PURE__ */ new Set([...filasA.map((f) => f.codigo), ...filasB.map((f) => f.codigo)])];
40516
+ const items = [];
40517
+ for (const codigo of codigos) {
40518
+ const fa = porA.get(codigo);
40519
+ const fb = porB.get(codigo);
40520
+ const his = porHis.get(codigo);
40521
+ const nombre = his?.nombre ?? fb?.nombre ?? fa?.nombre ?? codigo;
40522
+ const valorActual = his ? redondear(his.componentes.reduce((s, c) => s + c.monto, 0)) : null;
40523
+ const base = { codigo, codigo_his: his?.codigo_his ?? fb?.codigo_original ?? fa?.codigo_original ?? codigo, nombre, valor_a: fa?.valor_nuevo ?? null, valor_b: fb?.valor_nuevo ?? null, valor_actual: valorActual, valor_nuevo: null, variacion: null, estado: "revisar", motivo: null, componentes_nuevos: null };
40524
+ if (!fa || !fb) {
40525
+ items.push({ ...base, motivo: fa ? "solo la ley\xF3 el agente: el programa no la encontr\xF3 en la circular" : "solo la ley\xF3 el programa: el agente no la inform\xF3" });
40526
+ continue;
40527
+ }
40528
+ if (Math.abs(fa.valor_nuevo - fb.valor_nuevo) / Math.max(fa.valor_nuevo, fb.valor_nuevo) > TOLERANCIA_EXTRACCIONES) {
40529
+ items.push({ ...base, motivo: `las dos lecturas no coinciden: el agente ley\xF3 ${pesos(fa.valor_nuevo)} y el programa ${pesos(fb.valor_nuevo)}` });
40530
+ continue;
40531
+ }
40532
+ const nuevo = fb.valor_nuevo;
40533
+ if (!his) {
40534
+ items.push({ ...base, valor_nuevo: nuevo, estado: "sin_convenio", motivo: "la circular la trae y el convenio del HIS no la tiene: no se carga" });
40535
+ continue;
40536
+ }
40537
+ const componentes = repartir(his.componentes, nuevo);
40538
+ if (!componentes) {
40539
+ items.push({ ...base, valor_nuevo: nuevo, motivo: "tiene varios componentes de arancel en $0: no se puede repartir el valor nuevo" });
40540
+ continue;
40541
+ }
40542
+ const variacion = valorActual && valorActual > 0 ? redondear((nuevo - valorActual) / valorActual * 100, 1) : null;
40543
+ items.push({ ...base, valor_nuevo: nuevo, variacion, estado: "propuesta", componentes_nuevos: componentes });
40544
+ }
40545
+ const conVariacion = items.filter((i) => i.estado === "propuesta" && i.variacion !== null);
40546
+ const declarado = typeof input2.porcentajeDeclarado === "number" && Number.isFinite(input2.porcentajeDeclarado) ? input2.porcentajeDeclarado : null;
40547
+ const referencia = declarado ?? mediana(conVariacion.map((i) => i.variacion));
40548
+ let desviadas = 0;
40549
+ for (const i of items) {
40550
+ if (i.estado !== "propuesta") continue;
40551
+ const motivos = [];
40552
+ if (i.valor_actual === 0 || i.variacion === null) motivos.push("el HIS tiene hoy $0: no hay contra qu\xE9 comparar");
40553
+ else {
40554
+ if (i.variacion < 0) motivos.push(`baja el valor (${porc(i.variacion)})`);
40555
+ if (i.variacion > tope) motivos.push(`sube ${porc(i.variacion)}, m\xE1s que el tope de ${porc(tope)}`);
40556
+ if (referencia !== null && Math.abs(i.variacion - referencia) > DESVIO_COHERENCIA) {
40557
+ desviadas += 1;
40558
+ motivos.push(`se aparta ${redondear(Math.abs(i.variacion - referencia), 1).toLocaleString("es-AR")} puntos de la variaci\xF3n ${declarado !== null ? "que declara la circular" : "del resto"} (${porc(redondear(referencia, 1))})`);
40559
+ }
40560
+ }
40561
+ if (motivos.length) {
40562
+ i.estado = "excepcion";
40563
+ i.motivo = motivos.join("; ");
40564
+ }
40565
+ }
40566
+ const variaciones = items.filter((i) => i.variacion !== null && (i.estado === "propuesta" || i.estado === "excepcion")).map((i) => i.variacion);
40567
+ const resumen = {
40568
+ filas_a: nA,
40569
+ filas_b: nB,
40570
+ filas_declaradas: declaradas,
40571
+ coinciden: items.filter((i) => i.valor_a !== null && i.valor_b !== null && (i.estado !== "revisar" || i.valor_nuevo !== null)).length,
40572
+ propuestas: items.filter((i) => i.estado === "propuesta").length,
40573
+ revisar: items.filter((i) => i.estado === "revisar").length,
40574
+ sin_convenio: items.filter((i) => i.estado === "sin_convenio").length,
40575
+ excepciones: items.filter((i) => i.estado === "excepcion").length,
40576
+ bajas: items.filter((i) => i.variacion !== null && i.variacion < 0).length,
40577
+ supera_tope: items.filter((i) => i.variacion !== null && i.variacion > tope).length,
40578
+ referencia: declarado !== null ? "declarada" : referencia !== null ? "mediana" : null,
40579
+ variacion_referencia: referencia === null ? null : redondear(referencia, 1),
40580
+ variacion_min: variaciones.length ? Math.min(...variaciones) : null,
40581
+ variacion_max: variaciones.length ? Math.max(...variaciones) : null,
40582
+ coherencia_rota: conVariacion.length > 0 && desviadas / conVariacion.length > PROPORCION_INCOHERENTE,
40583
+ tope_variacion: tope
40584
+ };
40585
+ return { ok: true, items, resumen };
40586
+ }
40587
+ function leerItems(v) {
40588
+ return Array.isArray(v) ? v.filter((x) => x && typeof x === "object" && typeof x.codigo === "string") : [];
40589
+ }
40590
+ function itemsACargar(items, excluir) {
40591
+ const fuera = new Set(Array.isArray(excluir) ? excluir.map(normalizarCodigo) : []);
40592
+ return items.filter((i) => (i.estado === "propuesta" || i.estado === "excepcion") && i.componentes_nuevos && !fuera.has(i.codigo));
40593
+ }
40594
+ var csvCampo = (v) => /[;"\n\r]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
40595
+ function archivoActualizacion(items, actual) {
40596
+ const porHis = new Map(actual.map((p) => [p.codigo, p]));
40597
+ const aranceles = [];
40598
+ for (const i of items) for (const c of i.componentes_nuevos ?? []) if (!aranceles.includes(c.arancel)) aranceles.push(c.arancel);
40599
+ const lineas = [["Prestacion", "Codigo", "Copago", "Tipo Copago", ...aranceles].map(csvCampo).join(";")];
40600
+ for (const i of items) {
40601
+ const his = porHis.get(i.codigo);
40602
+ const montos = aranceles.map((a) => {
40603
+ const c = (i.componentes_nuevos ?? []).find((x) => x.arancel === a);
40604
+ return c ? c.monto.toFixed(2) : "";
40605
+ });
40606
+ lineas.push([his?.nombre ?? i.nombre, ` ${his ? his.codigo_his : i.codigo}`, his?.copago ?? "", his?.tipo_copago ?? "", ...montos].map(csvCampo).join(";"));
40607
+ }
40608
+ const contenido = `\uFEFF${lineas.join("\r\n")}\r
40609
+ `;
40610
+ return { contenido, hash: hashArchivo(contenido), filas: items.length, aranceles };
40611
+ }
40612
+ function hashArchivo(contenido) {
40613
+ return createHash("sha256").update(contenido, "utf8").digest("hex");
40614
+ }
40615
+ function compararRelectura(aCargar, leidos) {
40616
+ const lectura = leerActual(leidos);
40617
+ if ("error" in lectura) return { error: `leidos: ${lectura.error}` };
40618
+ const porHis = new Map(lectura.practicas.map((p) => [p.codigo, p]));
40619
+ const diferencias = [];
40620
+ for (const i of aCargar) {
40621
+ const his = porHis.get(i.codigo);
40622
+ for (const c of i.componentes_nuevos ?? []) {
40623
+ const leido = his?.componentes.find((x) => x.arancel === c.arancel)?.monto ?? null;
40624
+ if (leido === null || Math.abs(leido - c.monto) > TOLERANCIA_RELECTURA) diferencias.push({ codigo: i.codigo, nombre: i.nombre, arancel: c.arancel, aprobado: c.monto, leido });
40625
+ }
40626
+ }
40627
+ return { diferencias, comprobadas: aCargar.length };
40628
+ }
40629
+
40378
40630
  // src/workflows.ts
40379
40631
  var MODOS = ["corte", "observa", "propone", "ejecuta", "autonomo"];
40380
40632
  var MODO_POR_DEFECTO = "propone";
@@ -40988,7 +41240,7 @@ var cuentaCompleta = {
40988
41240
  ]
40989
41241
  };
40990
41242
  var PLAZO_PRESENTACION_POR_DEFECTO = 60;
40991
- function sumarDias(fecha, dias) {
41243
+ function sumarDias2(fecha, dias) {
40992
41244
  const d = /* @__PURE__ */ new Date(`${fecha}T00:00:00Z`);
40993
41245
  d.setUTCDate(d.getUTCDate() + dias);
40994
41246
  return d.toISOString().slice(0, 10);
@@ -40997,7 +41249,7 @@ function calcularVencimiento(fechaPrestacion, obraSocial, contexto) {
40997
41249
  const nombre = normalizarNombre(obraSocial);
40998
41250
  const fijado = nombre ? Object.entries(contexto.plazosPresentacion ?? {}).find(([k]) => normalizarNombre(k) === nombre)?.[1] : void 0;
40999
41251
  const plazo = typeof fijado === "number" && Number.isInteger(fijado) && fijado > 0 && fijado <= 730 ? fijado : PLAZO_PRESENTACION_POR_DEFECTO;
41000
- return { plazo_dias: plazo, vence_el: sumarDias(fechaPrestacion, plazo), plazo_confirmado: typeof fijado === "number" };
41252
+ return { plazo_dias: plazo, vence_el: sumarDias2(fechaPrestacion, plazo), plazo_confirmado: typeof fijado === "number" };
41001
41253
  }
41002
41254
  var facturacionPendiente = {
41003
41255
  clave: "facturacion_pendiente",
@@ -41317,6 +41569,156 @@ var demandaPendiente = {
41317
41569
  }
41318
41570
  ]
41319
41571
  };
41572
+ function resumenDe(caso) {
41573
+ const r = caso.datos.resumen;
41574
+ return r && typeof r === "object" ? r : null;
41575
+ }
41576
+ var circularAranceles = {
41577
+ clave: "circular_aranceles",
41578
+ nombre: "Circulares de aranceles",
41579
+ puesto: "Convenios y facturaci\xF3n",
41580
+ descripcion: "Lee la circular de aranceles de una obra social dos veces por caminos distintos, la cruza con el convenio del HIS y arma la actualizaci\xF3n completa; la direcci\xF3n la aprueba en un solo pedido, se carga con el archivo que genera el plugin y el puesto comprueba releyendo cada valor.",
41581
+ inicial: "recibida",
41582
+ estados: ["recibida", "extraida", "pendiente_aprobacion", "aprobada", "aplicada", "verificada", "con_diferencias", "rechazada", "cancelada"],
41583
+ finalesOk: ["verificada"],
41584
+ finalesNoOk: ["rechazada", "cancelada"],
41585
+ minutosAbsorbidos: 180,
41586
+ transiciones: [
41587
+ {
41588
+ via: "extraer",
41589
+ de: ["recibida", "extraida"],
41590
+ a: "extraida",
41591
+ actor: ["agente", "humano"],
41592
+ nivelRequerido: "observa",
41593
+ descripcion: "Informar las dos lecturas de la circular (la del agente y la de leer-circular.mjs) y lo que tiene hoy el convenio en el HIS. El plugin las cruza y decide pr\xE1ctica por pr\xE1ctica.",
41594
+ validar: (caso, datos, contexto) => {
41595
+ const l = { ...caso.datos, ...datos };
41596
+ const cruce = cruzarCircular({
41597
+ extraccionA: datos.extraccion_a,
41598
+ extraccionB: datos.extraccion_b,
41599
+ actual: datos.actual,
41600
+ filasDeclaradas: num(l.filas_declaradas),
41601
+ porcentajeDeclarado: num(l.porcentaje_declarado),
41602
+ vigenciaDesde: l.vigencia_desde,
41603
+ hoy: contexto.hoy ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
41604
+ topeVariacion: contexto.topeVariacion ?? null
41605
+ });
41606
+ return [
41607
+ texto(datos.lectura_id) ? ok("lectura_registrada") : falla("lectura_registrada", "extraer requiere lectura_id: la lectura del convenio en el HIS registrada con ops_registrar_accion"),
41608
+ texto(datos.lectura_circular_id) ? ok("lectura_circular_registrada") : falla("lectura_circular_registrada", "extraer requiere lectura_circular_id: la lectura de la circular (conector archivo) registrada con ops_registrar_accion"),
41609
+ texto(datos.fuente) ? ok("fuente_presente") : falla("fuente_presente", "fuente (la circular: nombre del archivo y de d\xF3nde vino) es obligatoria"),
41610
+ cruce.ok ? ok("cruce_circular", `${cruce.resumen.propuestas} propuestas, ${cruce.resumen.excepciones} excepciones, ${cruce.resumen.revisar} a revisar, ${cruce.resumen.sin_convenio} sin convenio`) : falla("cruce_circular", cruce.errores.join(" \xB7 "))
41611
+ ];
41612
+ }
41613
+ },
41614
+ {
41615
+ via: "proponer",
41616
+ de: ["extraida"],
41617
+ a: "pendiente_aprobacion",
41618
+ actor: ["agente", "humano"],
41619
+ nivelRequerido: "propone",
41620
+ descripcion: "Pedir la aprobaci\xF3n de la circular con lo que calcul\xF3 el plugin.",
41621
+ validar: (caso) => {
41622
+ const r = resumenDe(caso);
41623
+ return [r && r.propuestas + r.excepciones > 0 ? ok("hay_que_cargar", `${r.propuestas + r.excepciones} pr\xE1cticas`) : falla("hay_que_cargar", "no hay ninguna pr\xE1ctica para cargar: revisar la lectura o cancelar")];
41624
+ },
41625
+ autonomia: {
41626
+ aSinExcepcion: "aprobada",
41627
+ excepciones: (caso, _datos, contexto) => {
41628
+ const r = resumenDe(caso);
41629
+ const vigencia = texto(caso.datos.vigencia_desde);
41630
+ return [
41631
+ excepcion("practicas_con_excepcion", (r?.excepciones ?? 0) > 0, `${r?.excepciones ?? 0} pr\xE1cticas con baja, suba sobre el tope o desv\xEDo`),
41632
+ excepcion("lecturas_que_no_coinciden", (r?.revisar ?? 0) > 0, `${r?.revisar ?? 0} pr\xE1cticas a revisar`),
41633
+ excepcion("sin_convenio", (r?.sin_convenio ?? 0) > 0, `${r?.sin_convenio ?? 0} pr\xE1cticas que el convenio no tiene`),
41634
+ excepcion("coherencia_rota", r?.coherencia_rota === true, r?.coherencia_rota ? "m\xE1s del 10 % de las pr\xE1cticas se aparta de la variaci\xF3n de la circular" : "coherente"),
41635
+ excepcion("vigencia_pasada", vigencia !== null && contexto.hoy !== void 0 && vigencia < contexto.hoy, vigencia ? `rige desde ${vigencia}` : "sin vigencia")
41636
+ ];
41637
+ }
41638
+ }
41639
+ },
41640
+ {
41641
+ via: "aprobar",
41642
+ de: ["pendiente_aprobacion"],
41643
+ a: "aprobada",
41644
+ actor: ["humano"],
41645
+ descripcion: "Aprobaci\xF3n de la circular por la direcci\xF3n; puede excluir pr\xE1cticas (excluir: [c\xF3digos]).",
41646
+ validar: (caso, datos) => {
41647
+ const items = leerItems(caso.datos.items);
41648
+ const excluir = datos.excluir === void 0 ? [] : datos.excluir;
41649
+ if (!Array.isArray(excluir)) return [falla("excluir_valido", "excluir tiene que ser una lista de c\xF3digos")];
41650
+ const codigos = new Set(items.map((i) => i.codigo));
41651
+ const ajenos = excluir.map(normalizarCodigo).filter((c) => !codigos.has(c));
41652
+ const quedan = itemsACargar(items, excluir).length;
41653
+ return [
41654
+ ajenos.length === 0 ? ok("excluir_valido") : falla("excluir_valido", `c\xF3digos que no est\xE1n en la circular: ${ajenos.join(", ")}`),
41655
+ quedan > 0 ? ok("hay_que_cargar", `${quedan} pr\xE1cticas`) : falla("hay_que_cargar", "excluyendo esas no queda nada para cargar: rechazar la circular")
41656
+ ];
41657
+ }
41658
+ },
41659
+ {
41660
+ via: "rechazar",
41661
+ de: ["pendiente_aprobacion"],
41662
+ a: "rechazada",
41663
+ actor: ["humano"],
41664
+ descripcion: "Rechazo con motivo.",
41665
+ validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "el rechazo requiere motivo")]
41666
+ },
41667
+ {
41668
+ via: "aplicar",
41669
+ de: ["aprobada"],
41670
+ a: "aplicada",
41671
+ actor: ["agente", "humano"],
41672
+ nivelRequerido: "ejecuta",
41673
+ cargaManual: true,
41674
+ descripcion: "Cargar en el HIS el archivo que gener\xF3 el plugin (Actualizaci\xF3n con archivo) y registrarlo, con el hash del archivo cargado.",
41675
+ validar: (caso, datos) => {
41676
+ const esperado = texto(caso.datos.archivo_csv?.hash);
41677
+ const informado = texto(datos.archivo_hash);
41678
+ return [
41679
+ texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "aplicar requiere accion_id de la carga registrada"),
41680
+ esperado && informado === esperado ? ok("archivo_aprobado") : falla("archivo_aprobado", esperado ? "el archivo cargado no es el que gener\xF3 el plugin para esta aprobaci\xF3n (archivo_hash distinto): descargarlo de nuevo y cargar ese" : "la circular no tiene archivo generado")
41681
+ ];
41682
+ }
41683
+ },
41684
+ {
41685
+ via: "verificar",
41686
+ de: ["aplicada"],
41687
+ a: (caso, datos) => {
41688
+ const r = compararRelectura(itemsACargar(leerItems(caso.datos.items), caso.datos.excluir), datos.leidos);
41689
+ return "error" in r || r.diferencias.length > 0 ? "con_diferencias" : "verificada";
41690
+ },
41691
+ actor: ["agente", "humano", "sistema"],
41692
+ nivelRequerido: "observa",
41693
+ descripcion: "Releer el convenio en el HIS despu\xE9s de la carga e informar cada pr\xE1ctica con sus componentes (leidos). El plugin compara contra lo aprobado.",
41694
+ validar: (_caso, datos) => {
41695
+ const r = compararRelectura([], datos.leidos);
41696
+ return [
41697
+ texto(datos.lectura_id) ? ok("lectura_registrada") : falla("lectura_registrada", "verificar requiere lectura_id: la relectura del convenio registrada"),
41698
+ "error" in r ? falla("leidos_validos", r.error) : ok("leidos_validos")
41699
+ ];
41700
+ }
41701
+ },
41702
+ {
41703
+ via: "corregir",
41704
+ de: ["con_diferencias"],
41705
+ a: "aplicada",
41706
+ actor: ["humano"],
41707
+ cargaManual: true,
41708
+ descripcion: "Corregir en el HIS las pr\xE1cticas que no coincidieron y volver a comprobar.",
41709
+ validar: (_caso, datos) => [texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "corregir requiere accion_id de la correcci\xF3n registrada")]
41710
+ },
41711
+ {
41712
+ via: "cancelar",
41713
+ de: ["recibida", "extraida", "pendiente_aprobacion", "aprobada", "con_diferencias"],
41714
+ a: "cancelada",
41715
+ actor: ["humano", "agente"],
41716
+ nivelRequerido: "observa",
41717
+ descripcion: "Cancelar la circular (repetida, de otra regi\xF3n, ilegible).",
41718
+ validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "la cancelaci\xF3n requiere motivo")]
41719
+ }
41720
+ ]
41721
+ };
41320
41722
  var WORKFLOWS_SOLO_LECTURA = /* @__PURE__ */ new Set([cuentaCompleta.clave, facturacionPendiente.clave, credencialProfesional.clave]);
41321
41723
  var WORKFLOWS = {
41322
41724
  [actualizacionArancel.clave]: actualizacionArancel,
@@ -41325,7 +41727,8 @@ var WORKFLOWS = {
41325
41727
  [cuentaCompleta.clave]: cuentaCompleta,
41326
41728
  [facturacionPendiente.clave]: facturacionPendiente,
41327
41729
  [credencialProfesional.clave]: credencialProfesional,
41328
- [demandaPendiente.clave]: demandaPendiente
41730
+ [demandaPendiente.clave]: demandaPendiente,
41731
+ [circularAranceles.clave]: circularAranceles
41329
41732
  };
41330
41733
  function getWorkflow(clave) {
41331
41734
  return WORKFLOWS[clave] ?? null;
@@ -41951,6 +42354,7 @@ async function listarPoliticas(ctx, companyId) {
41951
42354
  }
41952
42355
  function describirOpciones(antes, despues) {
41953
42356
  const partes = [];
42357
+ if (antes.tope_variacion !== despues.tope_variacion && despues.tope_variacion !== void 0) partes.push(`tope de variaci\xF3n de las circulares: ${String(despues.tope_variacion)} %`);
41954
42358
  if (Boolean(antes.ofrecer_primero) !== Boolean(despues.ofrecer_primero)) partes.push(`consultar al paciente antes de asignar: ${despues.ofrecer_primero ? "s\xED" : "no"}`);
41955
42359
  const plazosAntes = JSON.stringify(leerPlazos(antes.plazos));
41956
42360
  const plazosDespues = leerPlazos(despues.plazos);
@@ -41960,6 +42364,10 @@ function describirOpciones(antes, despues) {
41960
42364
  }
41961
42365
  return partes.length ? partes.join(" \xB7 ") : null;
41962
42366
  }
42367
+ function leerTope(v) {
42368
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
42369
+ return Number.isFinite(n) && n >= LIMITES_TOPE[0] && n <= LIMITES_TOPE[1] ? Math.round(n) : null;
42370
+ }
41963
42371
  function leerPlazos(v) {
41964
42372
  if (!v || typeof v !== "object" || Array.isArray(v)) return {};
41965
42373
  const out = {};
@@ -41976,6 +42384,11 @@ async function cambiarPolitica(ctx, input2) {
41976
42384
  const opcionesAntes = await obtenerOpciones(ctx, input2.companyId, input2.workflow);
41977
42385
  const opciones = { ...opcionesAntes, ...input2.opciones ?? {} };
41978
42386
  if (opciones.plazos !== void 0) opciones.plazos = leerPlazos(opciones.plazos);
42387
+ if (opciones.tope_variacion !== void 0) {
42388
+ const tope = leerTope(opciones.tope_variacion);
42389
+ if (tope === null) throw new OpsError("validacion", `tope_variacion: tiene que ser un n\xFAmero entre ${LIMITES_TOPE[0]} y ${LIMITES_TOPE[1]}`);
42390
+ opciones.tope_variacion = tope;
42391
+ }
41979
42392
  const cambioOpciones = describirOpciones(opcionesAntes, opciones);
41980
42393
  const motivo = [input2.motivo ?? null, cambioOpciones].filter(Boolean).join(" \xB7 ") || null;
41981
42394
  const existe = await ctx.db.query(
@@ -42085,6 +42498,13 @@ async function abrirCaso(ctx, input2) {
42085
42498
  if (!referencia || !/^profesional:\S+$/.test(referencia)) throw new OpsError("referencia_requerida", "en credencial_profesional la referencia es obligatoria: profesional:<id del profesional>");
42086
42499
  if (prof && referencia !== `profesional:${prof}`) throw new OpsError("referencia_requerida", `la referencia ${referencia} no coincide con el profesional_id ${prof}`);
42087
42500
  }
42501
+ if (wf.clave === "circular_aranceles") {
42502
+ const d = input2.datos ?? {};
42503
+ if (!referencia || !/^circular:\S+$/.test(referencia)) throw new OpsError("referencia_requerida", "en circular_aranceles la referencia es obligatoria: circular:<obra social>:<hash corto del archivo>");
42504
+ const faltan = ["obra_social", "convenio", "vigencia_desde"].filter((k) => !str2(d[k]));
42505
+ if (faltan.length) throw new OpsError("validacion", `circular_aranceles necesita ${faltan.join(", ")}`);
42506
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(d.vigencia_desde))) throw new OpsError("validacion", "vigencia_desde: AAAA-MM-DD");
42507
+ }
42088
42508
  const extra = {};
42089
42509
  if (wf.clave === "demanda_pendiente") {
42090
42510
  if (!referencia || !/^(orden|solicitud|control):\S+$/.test(referencia)) {
@@ -42101,7 +42521,7 @@ async function abrirCaso(ctx, input2) {
42101
42521
  extra.tipo = regla.tipo;
42102
42522
  extra.canal = regla.contacto.canal;
42103
42523
  extra.intentos = 0;
42104
- extra.vence_el = sumarDias(hoy, regla.cierre.dias_para_cerrar);
42524
+ extra.vence_el = sumarDias2(hoy, regla.cierre.dias_para_cerrar);
42105
42525
  extra.detectado_el = hoy;
42106
42526
  }
42107
42527
  if (referencia) {
@@ -42153,6 +42573,10 @@ async function contextoDe(ctx, companyId, caso, via, datos) {
42153
42573
  contexto.plazosPresentacion = leerPlazos((await obtenerOpciones(ctx, companyId, caso.workflow)).plazos);
42154
42574
  return contexto;
42155
42575
  }
42576
+ if (caso.workflow === "circular_aranceles") {
42577
+ contexto.topeVariacion = leerTope((await obtenerOpciones(ctx, companyId, caso.workflow)).tope_variacion);
42578
+ return contexto;
42579
+ }
42156
42580
  if (caso.workflow === "demanda_pendiente") {
42157
42581
  const reglaId = str2(caso.datos.regla_id);
42158
42582
  const regla = reglaId ? await leerRegla(ctx, companyId, reglaId) : null;
@@ -42223,7 +42647,7 @@ async function transicionar(ctx, input2) {
42223
42647
  );
42224
42648
  const lectura = rows[0];
42225
42649
  let vieja = false;
42226
- if (lectura && resultado.via === "comprobar") {
42650
+ if (lectura && (resultado.via === "comprobar" || caso.workflow === "circular_aranceles" && resultado.via === "verificar")) {
42227
42651
  const ultima = await ctx.db.query(
42228
42652
  `SELECT max(created_at) AS ultima FROM ${t2(ctx, "transicion")} WHERE company_id = $1 AND caso_id = $2`,
42229
42653
  [input2.companyId, caso.id]
@@ -42236,6 +42660,48 @@ async function transicionar(ctx, input2) {
42236
42660
  throw new OpsError("validacion", `lectura_registrada: ${problema}`, { validaciones: [{ regla: "lectura_registrada", ok: false, detalle: problema }], estado: caso.estado });
42237
42661
  }
42238
42662
  }
42663
+ const lecturaCircularId = str2(datos.lectura_circular_id);
42664
+ if (caso.workflow === "circular_aranceles" && lecturaCircularId) {
42665
+ const fila = (await ctx.db.query(`SELECT id, caso_id, tipo, conector, resultado FROM ${t2(ctx, "accion")} WHERE company_id = $1 AND id = $2 LIMIT 1`, [input2.companyId, lecturaCircularId]))[0];
42666
+ const problema = !fila ? `lectura_circular_id ${lecturaCircularId} no existe en esta instituci\xF3n` : String(fila.caso_id) !== caso.id ? `la lectura ${lecturaCircularId} pertenece a otro caso` : String(fila.tipo) !== "lectura" || String(fila.conector) !== "archivo" ? "la lectura de la circular tiene que ser una lectura con conector archivo" : String(fila.resultado) !== "ok" ? `la lectura ${lecturaCircularId} termin\xF3 con resultado ${String(fila.resultado)}` : null;
42667
+ if (problema) throw new OpsError("validacion", `lectura_circular_registrada: ${problema}`, { validaciones: [{ regla: "lectura_circular_registrada", ok: false, detalle: problema }], estado: caso.estado });
42668
+ }
42669
+ if (caso.workflow === "circular_aranceles") {
42670
+ delete datos.items;
42671
+ delete datos.resumen;
42672
+ delete datos.archivo_csv;
42673
+ delete datos.diferencias;
42674
+ if (resultado.via === "extraer") {
42675
+ const cruce = cruzarCircular({
42676
+ extraccionA: datos.extraccion_a,
42677
+ extraccionB: datos.extraccion_b,
42678
+ actual: datos.actual,
42679
+ filasDeclaradas: datos.filas_declaradas ?? caso.datos.filas_declaradas,
42680
+ porcentajeDeclarado: datos.porcentaje_declarado ?? caso.datos.porcentaje_declarado,
42681
+ vigenciaDesde: datos.vigencia_desde ?? caso.datos.vigencia_desde,
42682
+ hoy: contexto.hoy ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
42683
+ topeVariacion: contexto.topeVariacion ?? null
42684
+ });
42685
+ if (cruce.ok) {
42686
+ datos.items = cruce.items;
42687
+ datos.resumen = cruce.resumen;
42688
+ const act = leerActual(datos.actual);
42689
+ datos.actual = "practicas" in act ? act.practicas : [];
42690
+ }
42691
+ }
42692
+ if (resultado.via === "aprobar") datos.excluir = Array.isArray(datos.excluir) ? datos.excluir.map(normalizarCodigo) : [];
42693
+ if (resultado.a === "aprobada") {
42694
+ const aCargar = itemsACargar(leerItems(caso.datos.items), datos.excluir ?? caso.datos.excluir);
42695
+ const actual = leerActual(caso.datos.actual);
42696
+ const archivo = archivoActualizacion(aCargar, "practicas" in actual ? actual.practicas : []);
42697
+ datos.archivo_csv = { contenido: archivo.contenido, hash: archivo.hash, filas: archivo.filas, aranceles: archivo.aranceles, nombre: `actualizacion-${String(caso.datos.convenio ?? "convenio").toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${String(caso.datos.vigencia_desde ?? "")}.csv` };
42698
+ }
42699
+ if (resultado.via === "verificar") {
42700
+ const r = compararRelectura(itemsACargar(leerItems(caso.datos.items), caso.datos.excluir), datos.leidos);
42701
+ datos.diferencias = "error" in r ? [] : r.diferencias;
42702
+ datos.verificada_el = contexto.hoy ?? null;
42703
+ }
42704
+ }
42239
42705
  if (caso.workflow === "cuenta_completa" && resultado.via === "observar") {
42240
42706
  datos.faltantes = leerFaltantes(datos.faltantes) ?? [];
42241
42707
  datos.faltantes_iniciales = datos.faltantes;
@@ -42265,7 +42731,7 @@ async function transicionar(ctx, input2) {
42265
42731
  datos.intentos = hechos;
42266
42732
  datos.ultimo_contacto = contexto.hoy ?? null;
42267
42733
  datos.canal = canalContacto(datos.canal);
42268
- datos.proximo_contacto = regla && hechos < regla.contacto.max_intentos && contexto.hoy ? sumarDias(contexto.hoy, regla.contacto.dias_entre_intentos) : null;
42734
+ datos.proximo_contacto = regla && hechos < regla.contacto.max_intentos && contexto.hoy ? sumarDias2(contexto.hoy, regla.contacto.dias_entre_intentos) : null;
42269
42735
  }
42270
42736
  if (resultado.via === "responder") {
42271
42737
  datos.respuesta = respuestaContacto(datos.respuesta);
@@ -42411,7 +42877,8 @@ async function registrarCargaManual(ctx, input2) {
42411
42877
  throw new OpsError("validacion", "valor_cargado: debe ser un n\xFAmero", { validaciones: [{ regla: "valor_cargado_numerico", ok: false, detalle: "debe ser un n\xFAmero" }], estado: caso.estado });
42412
42878
  }
42413
42879
  const contextoPrevio = await contextoDe(ctx, input2.companyId, caso, def2.via, {});
42414
- const previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)" }, modo: "ejecuta", contexto: contextoPrevio });
42880
+ const archivoHash = str2(input2.archivoHash);
42881
+ const previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)", ...archivoHash ? { archivo_hash: archivoHash } : {} }, modo: "ejecuta", contexto: contextoPrevio });
42415
42882
  if (!previa.ok) throw new OpsError(previa.codigo, previa.motivo, { validaciones: previa.validaciones, estado: caso.estado });
42416
42883
  const persona = str2(input2.persona);
42417
42884
  const comentario = str2(input2.comentario);
@@ -42429,6 +42896,7 @@ async function registrarCargaManual(ctx, input2) {
42429
42896
  ...caso.datos.vigencia_desde !== void 0 ? { vigencia_desde: caso.datos.vigencia_desde } : {},
42430
42897
  ...caso.workflow === "cuenta_completa" ? { faltantes_que_completo: caso.datos.faltantes ?? [] } : {},
42431
42898
  ...caso.workflow === "facturacion_pendiente" ? { bono: caso.datos.bono_numero ?? caso.datos.bono_id, vence_el: caso.datos.vence_el } : {},
42899
+ ...caso.workflow === "circular_aranceles" ? { archivo_hash: archivoHash, archivo: caso.datos.archivo_csv?.nombre ?? null, practicas: caso.datos.archivo_csv?.filas ?? null, convenio: caso.datos.convenio ?? null } : {},
42432
42900
  ...caso.workflow === "credencial_profesional" ? { profesional: caso.datos.profesional ?? caso.datos.profesional_id, observacion_que_corrigio: caso.datos.observacion ?? null } : {},
42433
42901
  ...comentario ? { nota: comentario } : {}
42434
42902
  },
@@ -42440,7 +42908,8 @@ async function registrarCargaManual(ctx, input2) {
42440
42908
  ...persona ? { cargado_por: persona } : {},
42441
42909
  ...valorCargado !== null ? { valor_cargado: valorCargado } : {},
42442
42910
  // Una lectura anterior (la que dio diferencias) ya no describe lo que hay en el sistema.
42443
- valor_leido: null
42911
+ valor_leido: null,
42912
+ ...archivoHash ? { archivo_hash: archivoHash } : {}
42444
42913
  };
42445
42914
  try {
42446
42915
  const r = await transicionar(ctx, { companyId: input2.companyId, casoId: caso.id, via: def2.via, datos, motivo: comentario, idempotencyKey: idem, actor: input2.actor });
@@ -42863,20 +43332,23 @@ function mapAviso(r) {
42863
43332
  };
42864
43333
  }
42865
43334
  async function listarAvisos(ctx, companyId, opciones = {}) {
42866
- const desde = opciones.desde ?? sumarDias(ahoraEnHospital().hoy, -DIAS_VENTANA);
43335
+ const desde = opciones.desde ?? sumarDias2(ahoraEnHospital().hoy, -DIAS_VENTANA);
42867
43336
  const rows = await ctx.db.query(`SELECT ${COLS_AVISO} FROM ${t3(ctx, "aviso")} WHERE company_id = $1 AND dia >= $2 ORDER BY created_at ASC`, [companyId, desde]);
42868
43337
  return rows.map(mapAviso);
42869
43338
  }
42870
- async function casosQueEsperan(ctx, companyId) {
43339
+ async function casosQueEsperan(ctx, companyId, workflows) {
42871
43340
  const out = [];
42872
- for (const [workflow, estados] of Object.entries(ESPERAN_PERSONA)) out.push(...await listarCasos(ctx, { companyId, workflow, estados, limit: 100 }));
43341
+ for (const [workflow, estados] of Object.entries(ESPERAN_PERSONA)) {
43342
+ if (workflows && workflows.length > 0 && !workflows.includes(workflow)) continue;
43343
+ out.push(...await listarCasos(ctx, { companyId, workflow, estados, limit: 100 }));
43344
+ }
42873
43345
  return out;
42874
43346
  }
42875
- async function responsablesPendientes(ctx, companyId) {
43347
+ async function responsablesPendientes(ctx, companyId, workflows) {
42876
43348
  const contactos = await listarContactos(ctx, companyId);
42877
43349
  const con = new Map(contactos.filter((c) => c.activo && (c.mail || c.whatsapp)).map((c) => [c.clave, c]));
42878
43350
  const acumulado = /* @__PURE__ */ new Map();
42879
- for (const caso of await casosQueEsperan(ctx, companyId)) {
43351
+ for (const caso of await casosQueEsperan(ctx, companyId, workflows)) {
42880
43352
  for (const p of pendientesDe(caso)) {
42881
43353
  const r = acumulado.get(p.responsable.clave) ?? { clave: p.responsable.clave, nombre: p.responsable.nombre, pendientes: 0, tieneContacto: con.has(p.responsable.clave), reserva: p.responsable.reserva };
42882
43354
  r.pendientes += 1;
@@ -42942,7 +43414,7 @@ async function generarAvisos(ctx, input2) {
42942
43414
  }
42943
43415
  const h = historial.get(`${caso.id}|${contacto.clave}`) ?? { veces: 0, ultimo: "" };
42944
43416
  if (h.veces >= MAX_AVISOS) continue;
42945
- if (h.veces > 0 && sumarDias(h.ultimo, DIAS_ENTRE_AVISOS) > hoy) continue;
43417
+ if (h.veces > 0 && sumarDias2(h.ultimo, DIAS_ENTRE_AVISOS) > hoy) continue;
42946
43418
  if (yaHoy.has(contacto.clave)) continue;
42947
43419
  const g = porDestinatario.get(contacto.clave) ?? { contacto, pendientes: [] };
42948
43420
  g.pendientes.push({ casoId: caso.id, workflow: caso.workflow, que: p.que, intento: h.veces + 1, titulo: caso.titulo });
@@ -43573,7 +44045,7 @@ async function manejarApi(ctx, input2) {
43573
44045
  case "rechazar":
43574
44046
  return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "rechazar", motivo: str3(body.motivo), datos: conPersona(obj(body.datos)), actor }));
43575
44047
  case "marcar-cargado":
43576
- return respuesta(200, await registrarCargaManual(ctx, { companyId, casoId: input2.params.casoId ?? "", valorCargado: body.valorCargado, comentario: str3(body.comentario), persona: decididoPor, idempotencyKey: str3(body.idempotencyKey), actor }));
44048
+ return respuesta(200, await registrarCargaManual(ctx, { companyId, casoId: input2.params.casoId ?? "", valorCargado: body.valorCargado, archivoHash: str3(body.archivoHash), comentario: str3(body.comentario), persona: decididoPor, idempotencyKey: str3(body.idempotencyKey), actor }));
43577
44049
  case "respuesta-paciente":
43578
44050
  return respuesta(200, await registrarRespuestaPaciente(ctx, { companyId, casoId: input2.params.casoId ?? "", acepto: body.acepto === true, comentario: str3(body.comentario), persona: decididoPor, idempotencyKey: str3(body.idempotencyKey), actor }));
43579
44051
  case "no-corresponde": {
@@ -43582,7 +44054,7 @@ async function manejarApi(ctx, input2) {
43582
44054
  return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "no_corresponde", motivo: motivoConPersona(str3(body.motivo)), datos: conPersona(obj(body.datos)), idempotencyKey: str3(body.idempotencyKey), actor }));
43583
44055
  }
43584
44056
  case "version":
43585
- return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos", "ofrecer-primero", "cuenta-completa", "metricas-certificadas", "facturacion-pendiente", "credenciales", "demanda-pendiente", "reglas", "avisos"], workflows: listarWorkflows().map((w) => w.clave) });
44057
+ return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos", "ofrecer-primero", "cuenta-completa", "metricas-certificadas", "facturacion-pendiente", "credenciales", "demanda-pendiente", "reglas", "avisos", "circular-aranceles"], workflows: listarWorkflows().map((w) => w.clave) });
43586
44058
  case "politicas":
43587
44059
  return respuesta(200, { politicas: await listarPoliticas(ctx, companyId) });
43588
44060
  case "serie":
@@ -43619,7 +44091,7 @@ async function manejarApi(ctx, input2) {
43619
44091
  case "contacto":
43620
44092
  return respuesta(200, await registrarContacto(ctx, { companyId, casoId: input2.params.casoId ?? "", canal: body.canal, respuesta: body.respuesta, comentario: str3(body.comentario), persona: decididoPor, idempotencyKey: str3(body.idempotencyKey), actor }));
43621
44093
  case "contactos":
43622
- return respuesta(200, await responsablesPendientes(ctx, companyId));
44094
+ return respuesta(200, await responsablesPendientes(ctx, companyId, (qs("workflows") ?? "").split(",").map((w) => w.trim()).filter(Boolean)));
43623
44095
  case "guardar-contacto": {
43624
44096
  if (actor.tipo !== "humano") return respuesta(403, { error: "actor_no_permitido", motivo: "los contactos los carga una persona de la instituci\xF3n" });
43625
44097
  return respuesta(200, { contacto: await guardarContacto(ctx, { companyId, clave: str3(body.clave) ?? "", nombre: str3(body.nombre), mail: str3(body.mail), whatsapp: str3(body.whatsapp), activo: body.activo !== false, actor: { tipo: "humano", id: decididoPor ?? actor.id } }) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-autopilot-ops",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "description": "Autopilot Care · operación autónoma gobernada para instituciones de salud: workflows determinísticos, validaciones, registro de negocio, políticas (observa/propone/ejecuta/corte) y tablero del director, como plugin de Paperclip.",
6
6
  "files": [