paperclip-plugin-autopilot-ops 0.1.20 → 0.1.22

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.20";
3
+ var PLUGIN_VERSION = "0.1.22";
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.20";
11
+ var PLUGIN_VERSION = "0.1.22";
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.20";
39935
+ var PLUGIN_VERSION = "0.1.22";
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,276 @@ 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 porBase = /* @__PURE__ */ new Map();
40516
+ for (const p of act.practicas) {
40517
+ const raiz = p.codigo.replace(/[A-Z]$/, "");
40518
+ if (raiz !== p.codigo) porBase.set(raiz, [...porBase.get(raiz) ?? [], p]);
40519
+ }
40520
+ const usadas = /* @__PURE__ */ new Set();
40521
+ const codigos = [.../* @__PURE__ */ new Set([...filasA.map((f) => f.codigo), ...filasB.map((f) => f.codigo)])];
40522
+ const items = [];
40523
+ for (const codigoCircular of codigos) {
40524
+ const fa = porA.get(codigoCircular);
40525
+ const fb = porB.get(codigoCircular);
40526
+ let his = porHis.get(codigoCircular);
40527
+ const variantes = his ? [] : porBase.get(codigoCircular) ?? [];
40528
+ if (!his && variantes.length === 1) his = variantes[0];
40529
+ const codigo = his?.codigo ?? codigoCircular;
40530
+ const nombre = his?.nombre ?? fb?.nombre ?? fa?.nombre ?? codigo;
40531
+ const valorActual = his ? redondear(his.componentes.reduce((s, c) => s + c.monto, 0)) : null;
40532
+ 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 };
40533
+ if (variantes.length > 1) {
40534
+ items.push({ ...base, motivo: `el convenio del HIS tiene ${variantes.length} variantes de este c\xF3digo (${variantes.map((v) => v.codigo_his).join(", ")}): no se carga hasta decidir a cu\xE1l corresponde` });
40535
+ continue;
40536
+ }
40537
+ if (his && usadas.has(his.codigo)) {
40538
+ items.push({ ...base, motivo: `otra fila de la circular ya corresponde a la pr\xE1ctica ${his.codigo_his} del HIS` });
40539
+ continue;
40540
+ }
40541
+ if (his) usadas.add(his.codigo);
40542
+ if (!fa || !fb) {
40543
+ 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" });
40544
+ continue;
40545
+ }
40546
+ if (Math.abs(fa.valor_nuevo - fb.valor_nuevo) / Math.max(fa.valor_nuevo, fb.valor_nuevo) > TOLERANCIA_EXTRACCIONES) {
40547
+ items.push({ ...base, motivo: `las dos lecturas no coinciden: el agente ley\xF3 ${pesos(fa.valor_nuevo)} y el programa ${pesos(fb.valor_nuevo)}` });
40548
+ continue;
40549
+ }
40550
+ const nuevo = fb.valor_nuevo;
40551
+ if (!his) {
40552
+ 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" });
40553
+ continue;
40554
+ }
40555
+ const componentes = repartir(his.componentes, nuevo);
40556
+ if (!componentes) {
40557
+ items.push({ ...base, valor_nuevo: nuevo, motivo: "tiene varios componentes de arancel en $0: no se puede repartir el valor nuevo" });
40558
+ continue;
40559
+ }
40560
+ const variacion = valorActual && valorActual > 0 ? redondear((nuevo - valorActual) / valorActual * 100, 1) : null;
40561
+ items.push({ ...base, valor_nuevo: nuevo, variacion, estado: "propuesta", componentes_nuevos: componentes });
40562
+ }
40563
+ const conVariacion = items.filter((i) => i.estado === "propuesta" && i.variacion !== null);
40564
+ const declarado = typeof input2.porcentajeDeclarado === "number" && Number.isFinite(input2.porcentajeDeclarado) ? input2.porcentajeDeclarado : null;
40565
+ const referencia = declarado ?? mediana(conVariacion.map((i) => i.variacion));
40566
+ let desviadas = 0;
40567
+ for (const i of items) {
40568
+ if (i.estado !== "propuesta") continue;
40569
+ const motivos = [];
40570
+ if (i.valor_actual === 0 || i.variacion === null) motivos.push("el HIS tiene hoy $0: no hay contra qu\xE9 comparar");
40571
+ else {
40572
+ if (i.variacion < 0) motivos.push(`baja el valor (${porc(i.variacion)})`);
40573
+ if (i.variacion > tope) motivos.push(`sube ${porc(i.variacion)}, m\xE1s que el tope de ${porc(tope)}`);
40574
+ if (referencia !== null && Math.abs(i.variacion - referencia) > DESVIO_COHERENCIA) {
40575
+ desviadas += 1;
40576
+ 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))})`);
40577
+ }
40578
+ }
40579
+ if (motivos.length) {
40580
+ i.estado = "excepcion";
40581
+ i.motivo = motivos.join("; ");
40582
+ }
40583
+ }
40584
+ const variaciones = items.filter((i) => i.variacion !== null && (i.estado === "propuesta" || i.estado === "excepcion")).map((i) => i.variacion);
40585
+ const resumen = {
40586
+ filas_a: nA,
40587
+ filas_b: nB,
40588
+ filas_declaradas: declaradas,
40589
+ coinciden: items.filter((i) => i.valor_a !== null && i.valor_b !== null && (i.estado !== "revisar" || i.valor_nuevo !== null)).length,
40590
+ propuestas: items.filter((i) => i.estado === "propuesta").length,
40591
+ revisar: items.filter((i) => i.estado === "revisar").length,
40592
+ sin_convenio: items.filter((i) => i.estado === "sin_convenio").length,
40593
+ excepciones: items.filter((i) => i.estado === "excepcion").length,
40594
+ bajas: items.filter((i) => i.variacion !== null && i.variacion < 0).length,
40595
+ supera_tope: items.filter((i) => i.variacion !== null && i.variacion > tope).length,
40596
+ referencia: declarado !== null ? "declarada" : referencia !== null ? "mediana" : null,
40597
+ variacion_referencia: referencia === null ? null : redondear(referencia, 1),
40598
+ variacion_min: variaciones.length ? Math.min(...variaciones) : null,
40599
+ variacion_max: variaciones.length ? Math.max(...variaciones) : null,
40600
+ coherencia_rota: conVariacion.length > 0 && desviadas / conVariacion.length > PROPORCION_INCOHERENTE,
40601
+ tope_variacion: tope
40602
+ };
40603
+ return { ok: true, items, resumen };
40604
+ }
40605
+ function leerItems(v) {
40606
+ return Array.isArray(v) ? v.filter((x) => x && typeof x === "object" && typeof x.codigo === "string") : [];
40607
+ }
40608
+ function itemsACargar(items, excluir) {
40609
+ const fuera = new Set(Array.isArray(excluir) ? excluir.map(normalizarCodigo) : []);
40610
+ return items.filter((i) => (i.estado === "propuesta" || i.estado === "excepcion") && i.componentes_nuevos && !fuera.has(i.codigo));
40611
+ }
40612
+ var csvCampo = (v) => /[;"\n\r]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
40613
+ function archivoActualizacion(items, actual) {
40614
+ const porHis = new Map(actual.map((p) => [p.codigo, p]));
40615
+ const aranceles = [];
40616
+ for (const i of items) for (const c of i.componentes_nuevos ?? []) if (!aranceles.includes(c.arancel)) aranceles.push(c.arancel);
40617
+ const lineas = [["Prestacion", "Codigo", "Copago", "Tipo Copago", ...aranceles].map(csvCampo).join(";")];
40618
+ for (const i of items) {
40619
+ const his = porHis.get(i.codigo);
40620
+ const montos = aranceles.map((a) => {
40621
+ const c = (i.componentes_nuevos ?? []).find((x) => x.arancel === a);
40622
+ return c ? c.monto.toFixed(2) : "";
40623
+ });
40624
+ lineas.push([his?.nombre ?? i.nombre, ` ${his ? his.codigo_his : i.codigo}`, his?.copago ?? "", his?.tipo_copago ?? "", ...montos].map(csvCampo).join(";"));
40625
+ }
40626
+ const contenido = `\uFEFF${lineas.join("\r\n")}\r
40627
+ `;
40628
+ return { contenido, hash: hashArchivo(contenido), filas: items.length, aranceles };
40629
+ }
40630
+ function hashArchivo(contenido) {
40631
+ return createHash("sha256").update(contenido, "utf8").digest("hex");
40632
+ }
40633
+ function compararRelectura(aCargar, leidos) {
40634
+ const lectura = leerActual(leidos);
40635
+ if ("error" in lectura) return { error: `leidos: ${lectura.error}` };
40636
+ const porHis = new Map(lectura.practicas.map((p) => [p.codigo, p]));
40637
+ const diferencias = [];
40638
+ for (const i of aCargar) {
40639
+ const his = porHis.get(i.codigo);
40640
+ for (const c of i.componentes_nuevos ?? []) {
40641
+ const leido = his?.componentes.find((x) => x.arancel === c.arancel)?.monto ?? null;
40642
+ 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 });
40643
+ }
40644
+ }
40645
+ return { diferencias, comprobadas: aCargar.length };
40646
+ }
40647
+
40378
40648
  // src/workflows.ts
40379
40649
  var MODOS = ["corte", "observa", "propone", "ejecuta", "autonomo"];
40380
40650
  var MODO_POR_DEFECTO = "propone";
@@ -40988,7 +41258,7 @@ var cuentaCompleta = {
40988
41258
  ]
40989
41259
  };
40990
41260
  var PLAZO_PRESENTACION_POR_DEFECTO = 60;
40991
- function sumarDias(fecha, dias) {
41261
+ function sumarDias2(fecha, dias) {
40992
41262
  const d = /* @__PURE__ */ new Date(`${fecha}T00:00:00Z`);
40993
41263
  d.setUTCDate(d.getUTCDate() + dias);
40994
41264
  return d.toISOString().slice(0, 10);
@@ -40997,7 +41267,7 @@ function calcularVencimiento(fechaPrestacion, obraSocial, contexto) {
40997
41267
  const nombre = normalizarNombre(obraSocial);
40998
41268
  const fijado = nombre ? Object.entries(contexto.plazosPresentacion ?? {}).find(([k]) => normalizarNombre(k) === nombre)?.[1] : void 0;
40999
41269
  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" };
41270
+ return { plazo_dias: plazo, vence_el: sumarDias2(fechaPrestacion, plazo), plazo_confirmado: typeof fijado === "number" };
41001
41271
  }
41002
41272
  var facturacionPendiente = {
41003
41273
  clave: "facturacion_pendiente",
@@ -41317,6 +41587,156 @@ var demandaPendiente = {
41317
41587
  }
41318
41588
  ]
41319
41589
  };
41590
+ function resumenDe(caso) {
41591
+ const r = caso.datos.resumen;
41592
+ return r && typeof r === "object" ? r : null;
41593
+ }
41594
+ var circularAranceles = {
41595
+ clave: "circular_aranceles",
41596
+ nombre: "Circulares de aranceles",
41597
+ puesto: "Convenios y facturaci\xF3n",
41598
+ 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.",
41599
+ inicial: "recibida",
41600
+ estados: ["recibida", "extraida", "pendiente_aprobacion", "aprobada", "aplicada", "verificada", "con_diferencias", "rechazada", "cancelada"],
41601
+ finalesOk: ["verificada"],
41602
+ finalesNoOk: ["rechazada", "cancelada"],
41603
+ minutosAbsorbidos: 180,
41604
+ transiciones: [
41605
+ {
41606
+ via: "extraer",
41607
+ de: ["recibida", "extraida"],
41608
+ a: "extraida",
41609
+ actor: ["agente", "humano"],
41610
+ nivelRequerido: "observa",
41611
+ 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.",
41612
+ validar: (caso, datos, contexto) => {
41613
+ const l = { ...caso.datos, ...datos };
41614
+ const cruce = cruzarCircular({
41615
+ extraccionA: datos.extraccion_a,
41616
+ extraccionB: datos.extraccion_b,
41617
+ actual: datos.actual,
41618
+ filasDeclaradas: num(l.filas_declaradas),
41619
+ porcentajeDeclarado: num(l.porcentaje_declarado),
41620
+ vigenciaDesde: l.vigencia_desde,
41621
+ hoy: contexto.hoy ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
41622
+ topeVariacion: contexto.topeVariacion ?? null
41623
+ });
41624
+ return [
41625
+ 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"),
41626
+ 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"),
41627
+ texto(datos.fuente) ? ok("fuente_presente") : falla("fuente_presente", "fuente (la circular: nombre del archivo y de d\xF3nde vino) es obligatoria"),
41628
+ 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 "))
41629
+ ];
41630
+ }
41631
+ },
41632
+ {
41633
+ via: "proponer",
41634
+ de: ["extraida"],
41635
+ a: "pendiente_aprobacion",
41636
+ actor: ["agente", "humano"],
41637
+ nivelRequerido: "propone",
41638
+ descripcion: "Pedir la aprobaci\xF3n de la circular con lo que calcul\xF3 el plugin.",
41639
+ validar: (caso) => {
41640
+ const r = resumenDe(caso);
41641
+ 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")];
41642
+ },
41643
+ autonomia: {
41644
+ aSinExcepcion: "aprobada",
41645
+ excepciones: (caso, _datos, contexto) => {
41646
+ const r = resumenDe(caso);
41647
+ const vigencia = texto(caso.datos.vigencia_desde);
41648
+ return [
41649
+ excepcion("practicas_con_excepcion", (r?.excepciones ?? 0) > 0, `${r?.excepciones ?? 0} pr\xE1cticas con baja, suba sobre el tope o desv\xEDo`),
41650
+ excepcion("lecturas_que_no_coinciden", (r?.revisar ?? 0) > 0, `${r?.revisar ?? 0} pr\xE1cticas a revisar`),
41651
+ excepcion("sin_convenio", (r?.sin_convenio ?? 0) > 0, `${r?.sin_convenio ?? 0} pr\xE1cticas que el convenio no tiene`),
41652
+ 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"),
41653
+ excepcion("vigencia_pasada", vigencia !== null && contexto.hoy !== void 0 && vigencia < contexto.hoy, vigencia ? `rige desde ${vigencia}` : "sin vigencia")
41654
+ ];
41655
+ }
41656
+ }
41657
+ },
41658
+ {
41659
+ via: "aprobar",
41660
+ de: ["pendiente_aprobacion"],
41661
+ a: "aprobada",
41662
+ actor: ["humano"],
41663
+ descripcion: "Aprobaci\xF3n de la circular por la direcci\xF3n; puede excluir pr\xE1cticas (excluir: [c\xF3digos]).",
41664
+ validar: (caso, datos) => {
41665
+ const items = leerItems(caso.datos.items);
41666
+ const excluir = datos.excluir === void 0 ? [] : datos.excluir;
41667
+ if (!Array.isArray(excluir)) return [falla("excluir_valido", "excluir tiene que ser una lista de c\xF3digos")];
41668
+ const codigos = new Set(items.map((i) => i.codigo));
41669
+ const ajenos = excluir.map(normalizarCodigo).filter((c) => !codigos.has(c));
41670
+ const quedan = itemsACargar(items, excluir).length;
41671
+ return [
41672
+ ajenos.length === 0 ? ok("excluir_valido") : falla("excluir_valido", `c\xF3digos que no est\xE1n en la circular: ${ajenos.join(", ")}`),
41673
+ quedan > 0 ? ok("hay_que_cargar", `${quedan} pr\xE1cticas`) : falla("hay_que_cargar", "excluyendo esas no queda nada para cargar: rechazar la circular")
41674
+ ];
41675
+ }
41676
+ },
41677
+ {
41678
+ via: "rechazar",
41679
+ de: ["pendiente_aprobacion"],
41680
+ a: "rechazada",
41681
+ actor: ["humano"],
41682
+ descripcion: "Rechazo con motivo.",
41683
+ validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "el rechazo requiere motivo")]
41684
+ },
41685
+ {
41686
+ via: "aplicar",
41687
+ de: ["aprobada"],
41688
+ a: "aplicada",
41689
+ actor: ["agente", "humano"],
41690
+ nivelRequerido: "ejecuta",
41691
+ cargaManual: true,
41692
+ descripcion: "Cargar en el HIS el archivo que gener\xF3 el plugin (Actualizaci\xF3n con archivo) y registrarlo, con el hash del archivo cargado.",
41693
+ validar: (caso, datos) => {
41694
+ const esperado = texto(caso.datos.archivo_csv?.hash);
41695
+ const informado = texto(datos.archivo_hash);
41696
+ return [
41697
+ texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "aplicar requiere accion_id de la carga registrada"),
41698
+ 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")
41699
+ ];
41700
+ }
41701
+ },
41702
+ {
41703
+ via: "verificar",
41704
+ de: ["aplicada"],
41705
+ a: (caso, datos) => {
41706
+ const r = compararRelectura(itemsACargar(leerItems(caso.datos.items), caso.datos.excluir), datos.leidos);
41707
+ return "error" in r || r.diferencias.length > 0 ? "con_diferencias" : "verificada";
41708
+ },
41709
+ actor: ["agente", "humano", "sistema"],
41710
+ nivelRequerido: "observa",
41711
+ 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.",
41712
+ validar: (_caso, datos) => {
41713
+ const r = compararRelectura([], datos.leidos);
41714
+ return [
41715
+ texto(datos.lectura_id) ? ok("lectura_registrada") : falla("lectura_registrada", "verificar requiere lectura_id: la relectura del convenio registrada"),
41716
+ "error" in r ? falla("leidos_validos", r.error) : ok("leidos_validos")
41717
+ ];
41718
+ }
41719
+ },
41720
+ {
41721
+ via: "corregir",
41722
+ de: ["con_diferencias"],
41723
+ a: "aplicada",
41724
+ actor: ["humano"],
41725
+ cargaManual: true,
41726
+ descripcion: "Corregir en el HIS las pr\xE1cticas que no coincidieron y volver a comprobar.",
41727
+ validar: (_caso, datos) => [texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "corregir requiere accion_id de la correcci\xF3n registrada")]
41728
+ },
41729
+ {
41730
+ via: "cancelar",
41731
+ de: ["recibida", "extraida", "pendiente_aprobacion", "aprobada", "con_diferencias"],
41732
+ a: "cancelada",
41733
+ actor: ["humano", "agente"],
41734
+ nivelRequerido: "observa",
41735
+ descripcion: "Cancelar la circular (repetida, de otra regi\xF3n, ilegible).",
41736
+ validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "la cancelaci\xF3n requiere motivo")]
41737
+ }
41738
+ ]
41739
+ };
41320
41740
  var WORKFLOWS_SOLO_LECTURA = /* @__PURE__ */ new Set([cuentaCompleta.clave, facturacionPendiente.clave, credencialProfesional.clave]);
41321
41741
  var WORKFLOWS = {
41322
41742
  [actualizacionArancel.clave]: actualizacionArancel,
@@ -41325,7 +41745,8 @@ var WORKFLOWS = {
41325
41745
  [cuentaCompleta.clave]: cuentaCompleta,
41326
41746
  [facturacionPendiente.clave]: facturacionPendiente,
41327
41747
  [credencialProfesional.clave]: credencialProfesional,
41328
- [demandaPendiente.clave]: demandaPendiente
41748
+ [demandaPendiente.clave]: demandaPendiente,
41749
+ [circularAranceles.clave]: circularAranceles
41329
41750
  };
41330
41751
  function getWorkflow(clave) {
41331
41752
  return WORKFLOWS[clave] ?? null;
@@ -41951,6 +42372,7 @@ async function listarPoliticas(ctx, companyId) {
41951
42372
  }
41952
42373
  function describirOpciones(antes, despues) {
41953
42374
  const partes = [];
42375
+ 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
42376
  if (Boolean(antes.ofrecer_primero) !== Boolean(despues.ofrecer_primero)) partes.push(`consultar al paciente antes de asignar: ${despues.ofrecer_primero ? "s\xED" : "no"}`);
41955
42377
  const plazosAntes = JSON.stringify(leerPlazos(antes.plazos));
41956
42378
  const plazosDespues = leerPlazos(despues.plazos);
@@ -41960,6 +42382,10 @@ function describirOpciones(antes, despues) {
41960
42382
  }
41961
42383
  return partes.length ? partes.join(" \xB7 ") : null;
41962
42384
  }
42385
+ function leerTope(v) {
42386
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
42387
+ return Number.isFinite(n) && n >= LIMITES_TOPE[0] && n <= LIMITES_TOPE[1] ? Math.round(n) : null;
42388
+ }
41963
42389
  function leerPlazos(v) {
41964
42390
  if (!v || typeof v !== "object" || Array.isArray(v)) return {};
41965
42391
  const out = {};
@@ -41976,6 +42402,11 @@ async function cambiarPolitica(ctx, input2) {
41976
42402
  const opcionesAntes = await obtenerOpciones(ctx, input2.companyId, input2.workflow);
41977
42403
  const opciones = { ...opcionesAntes, ...input2.opciones ?? {} };
41978
42404
  if (opciones.plazos !== void 0) opciones.plazos = leerPlazos(opciones.plazos);
42405
+ if (opciones.tope_variacion !== void 0) {
42406
+ const tope = leerTope(opciones.tope_variacion);
42407
+ if (tope === null) throw new OpsError("validacion", `tope_variacion: tiene que ser un n\xFAmero entre ${LIMITES_TOPE[0]} y ${LIMITES_TOPE[1]}`);
42408
+ opciones.tope_variacion = tope;
42409
+ }
41979
42410
  const cambioOpciones = describirOpciones(opcionesAntes, opciones);
41980
42411
  const motivo = [input2.motivo ?? null, cambioOpciones].filter(Boolean).join(" \xB7 ") || null;
41981
42412
  const existe = await ctx.db.query(
@@ -42085,6 +42516,13 @@ async function abrirCaso(ctx, input2) {
42085
42516
  if (!referencia || !/^profesional:\S+$/.test(referencia)) throw new OpsError("referencia_requerida", "en credencial_profesional la referencia es obligatoria: profesional:<id del profesional>");
42086
42517
  if (prof && referencia !== `profesional:${prof}`) throw new OpsError("referencia_requerida", `la referencia ${referencia} no coincide con el profesional_id ${prof}`);
42087
42518
  }
42519
+ if (wf.clave === "circular_aranceles") {
42520
+ const d = input2.datos ?? {};
42521
+ 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>");
42522
+ const faltan = ["obra_social", "convenio", "vigencia_desde"].filter((k) => !str2(d[k]));
42523
+ if (faltan.length) throw new OpsError("validacion", `circular_aranceles necesita ${faltan.join(", ")}`);
42524
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(d.vigencia_desde))) throw new OpsError("validacion", "vigencia_desde: AAAA-MM-DD");
42525
+ }
42088
42526
  const extra = {};
42089
42527
  if (wf.clave === "demanda_pendiente") {
42090
42528
  if (!referencia || !/^(orden|solicitud|control):\S+$/.test(referencia)) {
@@ -42101,7 +42539,7 @@ async function abrirCaso(ctx, input2) {
42101
42539
  extra.tipo = regla.tipo;
42102
42540
  extra.canal = regla.contacto.canal;
42103
42541
  extra.intentos = 0;
42104
- extra.vence_el = sumarDias(hoy, regla.cierre.dias_para_cerrar);
42542
+ extra.vence_el = sumarDias2(hoy, regla.cierre.dias_para_cerrar);
42105
42543
  extra.detectado_el = hoy;
42106
42544
  }
42107
42545
  if (referencia) {
@@ -42153,6 +42591,10 @@ async function contextoDe(ctx, companyId, caso, via, datos) {
42153
42591
  contexto.plazosPresentacion = leerPlazos((await obtenerOpciones(ctx, companyId, caso.workflow)).plazos);
42154
42592
  return contexto;
42155
42593
  }
42594
+ if (caso.workflow === "circular_aranceles") {
42595
+ contexto.topeVariacion = leerTope((await obtenerOpciones(ctx, companyId, caso.workflow)).tope_variacion);
42596
+ return contexto;
42597
+ }
42156
42598
  if (caso.workflow === "demanda_pendiente") {
42157
42599
  const reglaId = str2(caso.datos.regla_id);
42158
42600
  const regla = reglaId ? await leerRegla(ctx, companyId, reglaId) : null;
@@ -42223,7 +42665,7 @@ async function transicionar(ctx, input2) {
42223
42665
  );
42224
42666
  const lectura = rows[0];
42225
42667
  let vieja = false;
42226
- if (lectura && resultado.via === "comprobar") {
42668
+ if (lectura && (resultado.via === "comprobar" || caso.workflow === "circular_aranceles" && resultado.via === "verificar")) {
42227
42669
  const ultima = await ctx.db.query(
42228
42670
  `SELECT max(created_at) AS ultima FROM ${t2(ctx, "transicion")} WHERE company_id = $1 AND caso_id = $2`,
42229
42671
  [input2.companyId, caso.id]
@@ -42236,6 +42678,48 @@ async function transicionar(ctx, input2) {
42236
42678
  throw new OpsError("validacion", `lectura_registrada: ${problema}`, { validaciones: [{ regla: "lectura_registrada", ok: false, detalle: problema }], estado: caso.estado });
42237
42679
  }
42238
42680
  }
42681
+ const lecturaCircularId = str2(datos.lectura_circular_id);
42682
+ if (caso.workflow === "circular_aranceles" && lecturaCircularId) {
42683
+ 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];
42684
+ 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;
42685
+ if (problema) throw new OpsError("validacion", `lectura_circular_registrada: ${problema}`, { validaciones: [{ regla: "lectura_circular_registrada", ok: false, detalle: problema }], estado: caso.estado });
42686
+ }
42687
+ if (caso.workflow === "circular_aranceles") {
42688
+ delete datos.items;
42689
+ delete datos.resumen;
42690
+ delete datos.archivo_csv;
42691
+ delete datos.diferencias;
42692
+ if (resultado.via === "extraer") {
42693
+ const cruce = cruzarCircular({
42694
+ extraccionA: datos.extraccion_a,
42695
+ extraccionB: datos.extraccion_b,
42696
+ actual: datos.actual,
42697
+ filasDeclaradas: datos.filas_declaradas ?? caso.datos.filas_declaradas,
42698
+ porcentajeDeclarado: datos.porcentaje_declarado ?? caso.datos.porcentaje_declarado,
42699
+ vigenciaDesde: datos.vigencia_desde ?? caso.datos.vigencia_desde,
42700
+ hoy: contexto.hoy ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
42701
+ topeVariacion: contexto.topeVariacion ?? null
42702
+ });
42703
+ if (cruce.ok) {
42704
+ datos.items = cruce.items;
42705
+ datos.resumen = cruce.resumen;
42706
+ const act = leerActual(datos.actual);
42707
+ datos.actual = "practicas" in act ? act.practicas : [];
42708
+ }
42709
+ }
42710
+ if (resultado.via === "aprobar") datos.excluir = Array.isArray(datos.excluir) ? datos.excluir.map(normalizarCodigo) : [];
42711
+ if (resultado.a === "aprobada") {
42712
+ const aCargar = itemsACargar(leerItems(caso.datos.items), datos.excluir ?? caso.datos.excluir);
42713
+ const actual = leerActual(caso.datos.actual);
42714
+ const archivo = archivoActualizacion(aCargar, "practicas" in actual ? actual.practicas : []);
42715
+ 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` };
42716
+ }
42717
+ if (resultado.via === "verificar") {
42718
+ const r = compararRelectura(itemsACargar(leerItems(caso.datos.items), caso.datos.excluir), datos.leidos);
42719
+ datos.diferencias = "error" in r ? [] : r.diferencias;
42720
+ datos.verificada_el = contexto.hoy ?? null;
42721
+ }
42722
+ }
42239
42723
  if (caso.workflow === "cuenta_completa" && resultado.via === "observar") {
42240
42724
  datos.faltantes = leerFaltantes(datos.faltantes) ?? [];
42241
42725
  datos.faltantes_iniciales = datos.faltantes;
@@ -42265,7 +42749,7 @@ async function transicionar(ctx, input2) {
42265
42749
  datos.intentos = hechos;
42266
42750
  datos.ultimo_contacto = contexto.hoy ?? null;
42267
42751
  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;
42752
+ datos.proximo_contacto = regla && hechos < regla.contacto.max_intentos && contexto.hoy ? sumarDias2(contexto.hoy, regla.contacto.dias_entre_intentos) : null;
42269
42753
  }
42270
42754
  if (resultado.via === "responder") {
42271
42755
  datos.respuesta = respuestaContacto(datos.respuesta);
@@ -42411,7 +42895,8 @@ async function registrarCargaManual(ctx, input2) {
42411
42895
  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
42896
  }
42413
42897
  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 });
42898
+ const archivoHash = str2(input2.archivoHash);
42899
+ const previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)", ...archivoHash ? { archivo_hash: archivoHash } : {} }, modo: "ejecuta", contexto: contextoPrevio });
42415
42900
  if (!previa.ok) throw new OpsError(previa.codigo, previa.motivo, { validaciones: previa.validaciones, estado: caso.estado });
42416
42901
  const persona = str2(input2.persona);
42417
42902
  const comentario = str2(input2.comentario);
@@ -42429,6 +42914,7 @@ async function registrarCargaManual(ctx, input2) {
42429
42914
  ...caso.datos.vigencia_desde !== void 0 ? { vigencia_desde: caso.datos.vigencia_desde } : {},
42430
42915
  ...caso.workflow === "cuenta_completa" ? { faltantes_que_completo: caso.datos.faltantes ?? [] } : {},
42431
42916
  ...caso.workflow === "facturacion_pendiente" ? { bono: caso.datos.bono_numero ?? caso.datos.bono_id, vence_el: caso.datos.vence_el } : {},
42917
+ ...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
42918
  ...caso.workflow === "credencial_profesional" ? { profesional: caso.datos.profesional ?? caso.datos.profesional_id, observacion_que_corrigio: caso.datos.observacion ?? null } : {},
42433
42919
  ...comentario ? { nota: comentario } : {}
42434
42920
  },
@@ -42440,7 +42926,8 @@ async function registrarCargaManual(ctx, input2) {
42440
42926
  ...persona ? { cargado_por: persona } : {},
42441
42927
  ...valorCargado !== null ? { valor_cargado: valorCargado } : {},
42442
42928
  // Una lectura anterior (la que dio diferencias) ya no describe lo que hay en el sistema.
42443
- valor_leido: null
42929
+ valor_leido: null,
42930
+ ...archivoHash ? { archivo_hash: archivoHash } : {}
42444
42931
  };
42445
42932
  try {
42446
42933
  const r = await transicionar(ctx, { companyId: input2.companyId, casoId: caso.id, via: def2.via, datos, motivo: comentario, idempotencyKey: idem, actor: input2.actor });
@@ -42863,7 +43350,7 @@ function mapAviso(r) {
42863
43350
  };
42864
43351
  }
42865
43352
  async function listarAvisos(ctx, companyId, opciones = {}) {
42866
- const desde = opciones.desde ?? sumarDias(ahoraEnHospital().hoy, -DIAS_VENTANA);
43353
+ const desde = opciones.desde ?? sumarDias2(ahoraEnHospital().hoy, -DIAS_VENTANA);
42867
43354
  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
43355
  return rows.map(mapAviso);
42869
43356
  }
@@ -42945,7 +43432,7 @@ async function generarAvisos(ctx, input2) {
42945
43432
  }
42946
43433
  const h = historial.get(`${caso.id}|${contacto.clave}`) ?? { veces: 0, ultimo: "" };
42947
43434
  if (h.veces >= MAX_AVISOS) continue;
42948
- if (h.veces > 0 && sumarDias(h.ultimo, DIAS_ENTRE_AVISOS) > hoy) continue;
43435
+ if (h.veces > 0 && sumarDias2(h.ultimo, DIAS_ENTRE_AVISOS) > hoy) continue;
42949
43436
  if (yaHoy.has(contacto.clave)) continue;
42950
43437
  const g = porDestinatario.get(contacto.clave) ?? { contacto, pendientes: [] };
42951
43438
  g.pendientes.push({ casoId: caso.id, workflow: caso.workflow, que: p.que, intento: h.veces + 1, titulo: caso.titulo });
@@ -43576,7 +44063,7 @@ async function manejarApi(ctx, input2) {
43576
44063
  case "rechazar":
43577
44064
  return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "rechazar", motivo: str3(body.motivo), datos: conPersona(obj(body.datos)), actor }));
43578
44065
  case "marcar-cargado":
43579
- 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 }));
44066
+ 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 }));
43580
44067
  case "respuesta-paciente":
43581
44068
  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 }));
43582
44069
  case "no-corresponde": {
@@ -43585,7 +44072,7 @@ async function manejarApi(ctx, input2) {
43585
44072
  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 }));
43586
44073
  }
43587
44074
  case "version":
43588
- 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) });
44075
+ 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) });
43589
44076
  case "politicas":
43590
44077
  return respuesta(200, { politicas: await listarPoliticas(ctx, companyId) });
43591
44078
  case "serie":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-autopilot-ops",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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": [