paperclip-plugin-autopilot-ops 0.1.33 → 0.1.34

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.33";
3
+ var PLUGIN_VERSION = "0.1.34";
4
4
  var DB_NAMESPACE_SLUG = "autopilot_ops";
5
5
  var PAGE_ROUTE = "autopilot";
6
6
  var TOOL_NAMES = {
@@ -68,13 +68,18 @@ var manifest = {
68
68
  {
69
69
  name: TOOL_NAMES.listarCasos,
70
70
  displayName: "Listar casos",
71
- description: "Lista casos del registro de negocio de la instituci\xF3n. Filtros opcionales: workflow, estado, limit (m\xE1x. 100). Solo lectura.",
71
+ description: 'Lista casos del registro de negocio de la instituci\xF3n, abiertos y cerrados, del m\xE1s recientemente actualizado al m\xE1s viejo. Filtros opcionales: workflow (si el plugin no tiene esa funci\xF3n responde el error `workflow_desconocido`); estado (uno) o estados (varios, en un arreglo); referencia (todos los casos de ese objeto real, por ejemplo `rentabilidad:<prestacion_id>:<financiador_id>`, para saber si ya tiene caso sin recorrer el listado); limit (por defecto 50, m\xE1x. 500). Para recorrer todos sin perder ninguno aunque cambien mientras tanto: `orden: "id"` y, en cada p\xE1gina siguiente, `despues_de` con el id que indica la respuesta (\xABPuede haber m\xE1s casos\u2026\xBB); si vuelven menos casos que el limit, no hay m\xE1s. `offset` pagina el orden por actualizaci\xF3n (se corre si los casos cambian entre una p\xE1gina y otra). Solo lectura.',
72
72
  parametersSchema: {
73
73
  type: "object",
74
74
  properties: {
75
75
  workflow: { type: "string" },
76
76
  estado: { type: "string" },
77
- limit: { type: "number" }
77
+ estados: { type: "array", items: { type: "string" }, description: "Varios estados a la vez (se ignora si viene estado)." },
78
+ referencia: { type: "string", description: "Solo los casos con esta referencia exacta." },
79
+ limit: { type: "number" },
80
+ orden: { type: "string", enum: ["actualizacion", "id"], description: '"id": orden estable para recorrer todo con despues_de. Por defecto, por \xFAltima actualizaci\xF3n.' },
81
+ despues_de: { type: "string", description: 'Id del \xFAltimo caso de la p\xE1gina anterior (con orden "id"): trae los que siguen.' },
82
+ offset: { type: "number" }
78
83
  }
79
84
  }
80
85
  },
@@ -128,7 +133,7 @@ var manifest = {
128
133
  {
129
134
  name: TOOL_NAMES.abrirCaso,
130
135
  displayName: "Abrir caso",
131
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `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 (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y 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.',
136
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `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 (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y 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), `rentabilidad_practica` (referencia obligatoria `rentabilidad:<prestacion_id>:<financiador_id>`; datos: prestacion_id, financiador_id, codigo, practica, financiador), `rentabilidad_internacion` (una internaci\xF3n egresada; referencia obligatoria `rentabilidad_internacion:<internacion_id>`; datos: internacion_id, financiador_id, financiador). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese (creado: false). En `rentabilidad_internacion` se devuelve el caso existente aunque est\xE9 cerrado: una internaci\xF3n se mide una sola vez. Pasar `idempotencyKey` para reintentos seguros.',
132
137
  parametersSchema: {
133
138
  type: "object",
134
139
  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.33";
11
+ var PLUGIN_VERSION = "0.1.34";
12
12
  var DB_NAMESPACE_SLUG = "autopilot_ops";
13
13
  var PAGE_ROUTE = "autopilot";
14
14
  var TOOL_NAMES = {
@@ -76,13 +76,18 @@ var manifest = {
76
76
  {
77
77
  name: TOOL_NAMES.listarCasos,
78
78
  displayName: "Listar casos",
79
- description: "Lista casos del registro de negocio de la instituci\xF3n. Filtros opcionales: workflow, estado, limit (m\xE1x. 100). Solo lectura.",
79
+ description: 'Lista casos del registro de negocio de la instituci\xF3n, abiertos y cerrados, del m\xE1s recientemente actualizado al m\xE1s viejo. Filtros opcionales: workflow (si el plugin no tiene esa funci\xF3n responde el error `workflow_desconocido`); estado (uno) o estados (varios, en un arreglo); referencia (todos los casos de ese objeto real, por ejemplo `rentabilidad:<prestacion_id>:<financiador_id>`, para saber si ya tiene caso sin recorrer el listado); limit (por defecto 50, m\xE1x. 500). Para recorrer todos sin perder ninguno aunque cambien mientras tanto: `orden: "id"` y, en cada p\xE1gina siguiente, `despues_de` con el id que indica la respuesta (\xABPuede haber m\xE1s casos\u2026\xBB); si vuelven menos casos que el limit, no hay m\xE1s. `offset` pagina el orden por actualizaci\xF3n (se corre si los casos cambian entre una p\xE1gina y otra). Solo lectura.',
80
80
  parametersSchema: {
81
81
  type: "object",
82
82
  properties: {
83
83
  workflow: { type: "string" },
84
84
  estado: { type: "string" },
85
- limit: { type: "number" }
85
+ estados: { type: "array", items: { type: "string" }, description: "Varios estados a la vez (se ignora si viene estado)." },
86
+ referencia: { type: "string", description: "Solo los casos con esta referencia exacta." },
87
+ limit: { type: "number" },
88
+ orden: { type: "string", enum: ["actualizacion", "id"], description: '"id": orden estable para recorrer todo con despues_de. Por defecto, por \xFAltima actualizaci\xF3n.' },
89
+ despues_de: { type: "string", description: 'Id del \xFAltimo caso de la p\xE1gina anterior (con orden "id"): trae los que siguen.' },
90
+ offset: { type: "number" }
86
91
  }
87
92
  }
88
93
  },
@@ -136,7 +141,7 @@ var manifest = {
136
141
  {
137
142
  name: TOOL_NAMES.abrirCaso,
138
143
  displayName: "Abrir caso",
139
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `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 (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y 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.',
144
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `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 (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y 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), `rentabilidad_practica` (referencia obligatoria `rentabilidad:<prestacion_id>:<financiador_id>`; datos: prestacion_id, financiador_id, codigo, practica, financiador), `rentabilidad_internacion` (una internaci\xF3n egresada; referencia obligatoria `rentabilidad_internacion:<internacion_id>`; datos: internacion_id, financiador_id, financiador). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese (creado: false). En `rentabilidad_internacion` se devuelve el caso existente aunque est\xE9 cerrado: una internaci\xF3n se mide una sola vez. Pasar `idempotencyKey` para reintentos seguros.',
140
145
  parametersSchema: {
141
146
  type: "object",
142
147
  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.33";
39935
+ var PLUGIN_VERSION = "0.1.34";
39936
39936
  var DB_NAMESPACE_SLUG = "autopilot_ops";
39937
39937
  var PAGE_ROUTE = "autopilot";
39938
39938
  var TOOL_NAMES = {
@@ -40000,13 +40000,18 @@ var manifest = {
40000
40000
  {
40001
40001
  name: TOOL_NAMES.listarCasos,
40002
40002
  displayName: "Listar casos",
40003
- description: "Lista casos del registro de negocio de la instituci\xF3n. Filtros opcionales: workflow, estado, limit (m\xE1x. 100). Solo lectura.",
40003
+ description: 'Lista casos del registro de negocio de la instituci\xF3n, abiertos y cerrados, del m\xE1s recientemente actualizado al m\xE1s viejo. Filtros opcionales: workflow (si el plugin no tiene esa funci\xF3n responde el error `workflow_desconocido`); estado (uno) o estados (varios, en un arreglo); referencia (todos los casos de ese objeto real, por ejemplo `rentabilidad:<prestacion_id>:<financiador_id>`, para saber si ya tiene caso sin recorrer el listado); limit (por defecto 50, m\xE1x. 500). Para recorrer todos sin perder ninguno aunque cambien mientras tanto: `orden: "id"` y, en cada p\xE1gina siguiente, `despues_de` con el id que indica la respuesta (\xABPuede haber m\xE1s casos\u2026\xBB); si vuelven menos casos que el limit, no hay m\xE1s. `offset` pagina el orden por actualizaci\xF3n (se corre si los casos cambian entre una p\xE1gina y otra). Solo lectura.',
40004
40004
  parametersSchema: {
40005
40005
  type: "object",
40006
40006
  properties: {
40007
40007
  workflow: { type: "string" },
40008
40008
  estado: { type: "string" },
40009
- limit: { type: "number" }
40009
+ estados: { type: "array", items: { type: "string" }, description: "Varios estados a la vez (se ignora si viene estado)." },
40010
+ referencia: { type: "string", description: "Solo los casos con esta referencia exacta." },
40011
+ limit: { type: "number" },
40012
+ orden: { type: "string", enum: ["actualizacion", "id"], description: '"id": orden estable para recorrer todo con despues_de. Por defecto, por \xFAltima actualizaci\xF3n.' },
40013
+ despues_de: { type: "string", description: 'Id del \xFAltimo caso de la p\xE1gina anterior (con orden "id"): trae los que siguen.' },
40014
+ offset: { type: "number" }
40010
40015
  }
40011
40016
  }
40012
40017
  },
@@ -40060,7 +40065,7 @@ var manifest = {
40060
40065
  {
40061
40066
  name: TOOL_NAMES.abrirCaso,
40062
40067
  displayName: "Abrir caso",
40063
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `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 (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y 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.',
40068
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `internacion_al_dia` (una internaci\xF3n abierta; referencia obligatoria `internacion:<id>`; datos: internacion_id), `honorario_en_cero` (referencia `honorarios:<profesional_id>`; datos: profesional_id, profesional), `debito_profesional` (referencia `debito:<profesional_id>:<nota_credito_id>`; datos: profesional_id, profesional, nota_credito_id, obra_social), `liquidacion_profesional` (referencia `liquidacion:<profesional_id>:<AAAA-MM-DD>`; datos: profesional_id, profesional, liquidado_el, institucion, mail_his), `cobranza_financiador` (un caso abierto por financiador; referencia obligatoria `cobranza:<obra_social_id>`; datos: obra_social (nombre como en el HIS), obra_social_id, institucion (nombre de la instituci\xF3n para el reclamo)), `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 (regla activa) y candidato tal cual lo devuelve buscar-demanda.mjs; el plugin verifica la regla con el candidato (si no la cumple, no abre) y 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), `rentabilidad_practica` (referencia obligatoria `rentabilidad:<prestacion_id>:<financiador_id>`; datos: prestacion_id, financiador_id, codigo, practica, financiador), `rentabilidad_internacion` (una internaci\xF3n egresada; referencia obligatoria `rentabilidad_internacion:<internacion_id>`; datos: internacion_id, financiador_id, financiador). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese (creado: false). En `rentabilidad_internacion` se devuelve el caso existente aunque est\xE9 cerrado: una internaci\xF3n se mide una sola vez. Pasar `idempotencyKey` para reintentos seguros.',
40064
40069
  parametersSchema: {
40065
40070
  type: "object",
40066
40071
  properties: {
@@ -43114,8 +43119,8 @@ var rentabilidadPractica = {
43114
43119
  descripcion: "Calcula, para cada pr\xE1ctica y cada financiador, lo que deja: lo que paga el convenio menos el honorario del profesional (con la regla del HIS) y los insumos. Marca las que dan p\xE9rdida o quedan debajo del umbral de la instituci\xF3n, y las que no se pueden calcular porque faltan datos en el HIS. Decide la direcci\xF3n: renegociar con el financiador o aceptarla. El puesto no escribe en el sistema.",
43115
43120
  inicial: "detectado",
43116
43121
  estados: ["detectado", "pendiente_aprobacion", "datos_incompletos", "completado", "en_renegociacion", "resuelta", "aceptada", "rentable", "descartado"],
43117
- finalesOk: ["resuelta"],
43118
- finalesNoOk: ["aceptada", "rentable", "descartado"],
43122
+ finalesOk: ["resuelta", "rentable", "aceptada"],
43123
+ finalesNoOk: ["descartado"],
43119
43124
  minutosAbsorbidos: 15,
43120
43125
  transiciones: [
43121
43126
  {
@@ -43174,13 +43179,54 @@ function validarLecturaCostoInternacion(caso, datos, contexto) {
43174
43179
  texto4(datos.lectura_id) ? ok("lectura_registrada") : falla("lectura_registrada", "hace falta lectura_id: la lectura de la base registrada con ops_registrar_accion")
43175
43180
  ];
43176
43181
  }
43182
+ var DIAS_REVISION_CUENTA = 14;
43183
+ var centavos = (v) => typeof v === "number" && Number.isFinite(v) ? Math.round(v * 100) : null;
43184
+ var fechaCortaISO = (v) => {
43185
+ const iso5 = fechaAISO(v);
43186
+ return iso5 ? `${iso5.slice(8, 10)}/${iso5.slice(5, 7)}/${iso5.slice(0, 4)}` : null;
43187
+ };
43188
+ function baseRevisionCuenta(d) {
43189
+ const e = d.evaluacion && typeof d.evaluacion === "object" ? d.evaluacion : {};
43190
+ return {
43191
+ facturado: typeof d.revision_facturado === "number" ? d.revision_facturado : typeof e.facturado === "number" ? e.facturado : null,
43192
+ costo: typeof d.revision_costo === "number" ? d.revision_costo : typeof e.costo === "number" ? e.costo : null
43193
+ };
43194
+ }
43195
+ function resultadoRevisionCuenta(datosCaso, e, hoy) {
43196
+ const base = baseRevisionCuenta(datosCaso);
43197
+ const cambio2 = (antes, ahora) => antes !== null && centavos(antes) !== centavos(ahora);
43198
+ if (cambio2(base.facturado, e.facturado) || cambio2(base.costo, e.costo)) return "con_cambios";
43199
+ const vence = fechaAISO(datosCaso.revision_vence_el);
43200
+ return !vence || !hoy || hoy >= vence ? "sin_cambios" : null;
43201
+ }
43202
+ function notaRevisionCuenta(d, estado, e, hoy) {
43203
+ const pedida = fechaCortaISO(d.revision_pedida_el);
43204
+ const desde = pedida ? `Se pidi\xF3 revisar la cuenta el ${pedida}` : "Se pidi\xF3 revisar la cuenta";
43205
+ if (estado === "en_revision") {
43206
+ const vence = fechaCortaISO(d.revision_vence_el);
43207
+ return `${desde}: si no cambia, vuelve a la direcci\xF3n${vence ? ` el ${vence}` : ""}.`;
43208
+ }
43209
+ if (estado !== "pendiente_aprobacion" || typeof d.revisada_sin_cambios !== "boolean") return "";
43210
+ if (d.revisada_sin_cambios) {
43211
+ const dias = hoy ? diasEntre3(d.revision_pedida_el, hoy) : null;
43212
+ return `${desde} y${dias !== null && dias > 0 ? ` en ${dias} ${dias === 1 ? "d\xEDa" : "d\xEDas"}` : ""} no cambi\xF3.`;
43213
+ }
43214
+ const base = baseRevisionCuenta(d);
43215
+ const cambios = [
43216
+ base.facturado !== null && centavos(base.facturado) !== centavos(e.facturado) ? `lo facturado pas\xF3 de ${pesos2(base.facturado)} a ${pesos2(e.facturado)}` : null,
43217
+ base.costo !== null && centavos(base.costo) !== centavos(e.costo) ? `el costo pas\xF3 de ${pesos2(base.costo)} a ${pesos2(e.costo)}` : null
43218
+ ].filter((x) => x !== null);
43219
+ const sigue = e.clase === "perdida" ? "sigue con p\xE9rdida" : "sigue debajo del margen m\xEDnimo";
43220
+ return `${desde}: ${cambios.length ? cambios.join(" y ") : "la cuenta cambi\xF3 desde entonces"}, y ${sigue}.`;
43221
+ }
43177
43222
  var destinoCostoInternacion = (caso, _datos, contexto) => {
43178
43223
  const r = contexto.costoInternacion;
43179
43224
  if (!r || "error" in r) return caso.estado;
43180
43225
  const clase2 = r.evaluacion.clase;
43181
43226
  if (clase2 === "datos_incompletos") return "datos_incompletos";
43182
43227
  if (clase2 === "rentable") return caso.estado === "detectado" ? "rentable" : "resuelta";
43183
- return caso.estado === "en_revision" ? "en_revision" : "pendiente_aprobacion";
43228
+ if (caso.estado === "en_revision") return resultadoRevisionCuenta(caso.datos, r.evaluacion, contexto.hoy) ? "pendiente_aprobacion" : "en_revision";
43229
+ return "pendiente_aprobacion";
43184
43230
  };
43185
43231
  var rentabilidadInternacion = {
43186
43232
  clave: "rentabilidad_internacion",
@@ -43189,8 +43235,8 @@ var rentabilidadInternacion = {
43189
43235
  descripcion: "Calcula, para cada internaci\xF3n egresada, lo que dej\xF3: lo facturado contra lo que cost\xF3 (honorarios con la regla del HIS, insumos que farmacia le entreg\xF3 al paciente a costo de compra, d\xEDas cama y quir\xF3fano con los costos que fija la instituci\xF3n). Las que dan p\xE9rdida o quedan debajo del umbral van a la direcci\xF3n: revisar la cuenta (consumos sin facturar, renegociar el m\xF3dulo con el financiador) o aceptarla as\xED. El puesto no escribe en el sistema.",
43190
43236
  inicial: "detectado",
43191
43237
  estados: ["detectado", "pendiente_aprobacion", "datos_incompletos", "completado", "en_revision", "resuelta", "aceptada", "rentable", "descartado"],
43192
- finalesOk: ["resuelta"],
43193
- finalesNoOk: ["aceptada", "rentable", "descartado"],
43238
+ finalesOk: ["resuelta", "rentable", "aceptada"],
43239
+ finalesNoOk: ["descartado"],
43194
43240
  minutosAbsorbidos: 20,
43195
43241
  transiciones: [
43196
43242
  {
@@ -43202,7 +43248,7 @@ var rentabilidadInternacion = {
43202
43248
  descripcion: "Primera lectura de la internaci\xF3n. El plugin calcula el resultado y decide: pendiente de aprobaci\xF3n (p\xE9rdida o margen bajo), datos incompletos o rentable.",
43203
43249
  validar: validarLecturaCostoInternacion
43204
43250
  },
43205
- { via: "aprobar", de: ["pendiente_aprobacion"], a: "en_revision", actor: ["humano"], descripcion: "La direcci\xF3n pide revisar la cuenta: consumos que no se facturaron o el m\xF3dulo con el financiador. El puesto vuelve a medir." },
43251
+ { via: "aprobar", de: ["pendiente_aprobacion"], a: "en_revision", actor: ["humano"], descripcion: `La direcci\xF3n pide revisar la cuenta: consumos que no se facturaron o el m\xF3dulo con el financiador. El puesto vuelve a medir: si la cuenta cambia y sigue con p\xE9rdida o margen bajo, o si pasan ${DIAS_REVISION_CUENTA} d\xEDas sin cambios, vuelve a la direcci\xF3n.` },
43206
43252
  {
43207
43253
  via: "rechazar",
43208
43254
  de: ["pendiente_aprobacion", "en_revision"],
@@ -43226,7 +43272,7 @@ var rentabilidadInternacion = {
43226
43272
  a: destinoCostoInternacion,
43227
43273
  actor: ["agente", "humano", "sistema"],
43228
43274
  nivelRequerido: "observa",
43229
- descripcion: "Volver a leer la internaci\xF3n. El plugin decide: resuelta (ya deja margen, por ejemplo porque se factur\xF3 lo que faltaba), sigue pendiente o en revisi\xF3n, o datos incompletos.",
43275
+ descripcion: `Volver a leer la internaci\xF3n. El plugin decide: resuelta (ya deja margen, por ejemplo porque se factur\xF3 lo que faltaba), pendiente de aprobaci\xF3n (sigue con p\xE9rdida o margen bajo; desde la revisi\xF3n, solo si la cuenta cambi\xF3 o pasaron ${DIAS_REVISION_CUENTA} d\xEDas sin cambios), sigue en revisi\xF3n o datos incompletos.`,
43230
43276
  validar: validarLecturaCostoInternacion
43231
43277
  },
43232
43278
  {
@@ -43843,7 +43889,7 @@ function evaluarRentabilidad(input2) {
43843
43889
  if (l.insumos.length === 0) avisos.push("sin_insumos");
43844
43890
  const debajo = [];
43845
43891
  const honorarios = [];
43846
- if (l.honorarios.length === 0) faltas.push("sin_regla_honorario");
43892
+ if (l.honorarios.length === 0) faltas.push("sin_profesional");
43847
43893
  for (const h of l.honorarios) {
43848
43894
  const nombre = h.profesional ?? h.profesional_id;
43849
43895
  if (!h.regla) {
@@ -43893,6 +43939,7 @@ var NOMBRE_FALTA = {
43893
43939
  sin_valor_convenio: "el convenio no tiene valor para esta pr\xE1ctica",
43894
43940
  insumo_sin_precio: "hay insumos sin precio",
43895
43941
  sin_regla_honorario: "hay profesionales sin regla de honorarios",
43942
+ sin_profesional: "se factur\xF3 sin un profesional (si es un medicamento o un descartable, no corresponde medirla como pr\xE1ctica)",
43896
43943
  regla_sin_valor: "hay reglas de honorarios sin valor"
43897
43944
  };
43898
43945
  var porcentaje = (n) => `${n.toLocaleString("es-AR", { maximumFractionDigits: 1 })} %`;
@@ -44033,7 +44080,8 @@ function leerLecturaCostoInternacion(v) {
44033
44080
  const monto = num7(f.monto);
44034
44081
  if (cantidad2 === null || cantidad2 < 0 || monto === null || monto < 0) return { error: `lectura.facturado[${i}]: cantidad y monto tienen que ser n\xFAmeros \u2265 0` };
44035
44082
  if (f.regla !== null && f.regla !== void 0 && !leerRegla(f.regla)) return { error: `lectura.facturado[${i}].regla: tiene que ser {id, prof, inst} o null` };
44036
- facturado.push({ prestacion_id: texto7(f.prestacion_id) ?? String(i), nombre: texto7(f.nombre) ?? "", cantidad: cantidad2, monto: redondear5(monto), profesional_id: texto7(f.profesional_id), regla: leerRegla(f.regla) });
44083
+ const noLiquidable = f.liquidable === false || f.liquidable === 0 || f.liquidable === "0" || f.liquidable === "false";
44084
+ facturado.push({ prestacion_id: texto7(f.prestacion_id) ?? String(i), nombre: texto7(f.nombre) ?? "", cantidad: cantidad2, monto: redondear5(monto), profesional_id: texto7(f.profesional_id), regla: leerRegla(f.regla), ...noLiquidable ? { liquidable: false } : {} });
44037
44085
  }
44038
44086
  const consumos = [];
44039
44087
  for (const [i, x] of r.consumos.entries()) {
@@ -44054,28 +44102,65 @@ function leerLecturaCostoInternacion(v) {
44054
44102
  var DIA_MS = 864e5;
44055
44103
  var ABREN = /* @__PURE__ */ new Set(["ingreso", "pase_cama", "reingreso"]);
44056
44104
  var CIERRAN = /* @__PURE__ */ new Set(["egreso"]);
44105
+ var ORDEN_EVENTO = { egreso: 0, ingreso: 1, reingreso: 1, pase_cama: 2 };
44106
+ var claveServicio = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/\s+/g, " ").trim();
44107
+ function repartir2(pesos4, unidades) {
44108
+ const suma = pesos4.reduce((a, b) => a + b, 0);
44109
+ const exactas = pesos4.map((p) => suma > 0 ? p / suma * unidades : 0);
44110
+ const enteras = exactas.map((x) => Math.floor(x));
44111
+ let resto = unidades - enteras.reduce((a, b) => a + b, 0);
44112
+ const orden = pesos4.map((_, i) => i).sort((a, b) => exactas[b] - enteras[b] - (exactas[a] - enteras[a]) || pesos4[b] - pesos4[a] || a - b);
44113
+ for (const i of orden) {
44114
+ if (resto <= 0) break;
44115
+ enteras[i] += 1;
44116
+ resto -= 1;
44117
+ }
44118
+ return enteras;
44119
+ }
44120
+ function repartirDecimas(pesos4, total) {
44121
+ const decimas = Math.round(total * 10);
44122
+ if (pesos4.length > decimas) return repartir2(pesos4, decimas);
44123
+ const suma = pesos4.reduce((a, b) => a + b, 0);
44124
+ const excedentes = pesos4.map((p) => Math.max(0, p / suma * decimas - 1));
44125
+ return repartir2(excedentes, decimas - pesos4.length).map((d) => d + 1);
44126
+ }
44057
44127
  function diasPorServicio(l, hoy) {
44058
- const evs = l.eventos.filter((e) => ABREN.has(e.tipo) || CIERRAN.has(e.tipo)).sort((a, b) => a.fecha.localeCompare(b.fecha));
44059
- const fin = Date.parse(l.egreso ?? `${hoy}T23:59:59`);
44128
+ const inicio = Date.parse(l.ingreso);
44129
+ const fin = Math.max(inicio, Date.parse(l.egreso ?? `${hoy}T23:59:59`));
44130
+ const recortar = (t6) => Math.min(fin, Math.max(inicio, t6));
44131
+ const evs = l.eventos.filter((e) => ABREN.has(e.tipo) || CIERRAN.has(e.tipo)).map((e) => ({ ...e, t: recortar(Date.parse(e.fecha)) })).sort((a, b) => a.t - b.t || (ORDEN_EVENTO[a.tipo] ?? 1) - (ORDEN_EVENTO[b.tipo] ?? 1) || a.fecha.localeCompare(b.fecha));
44060
44132
  const tramos = /* @__PURE__ */ new Map();
44133
+ const sumar = (servicio, ms) => {
44134
+ const k = claveServicio(servicio);
44135
+ const t6 = tramos.get(k);
44136
+ if (t6) t6.ms += Math.max(0, ms);
44137
+ else tramos.set(k, { nombre: servicio, ms: Math.max(0, ms) });
44138
+ };
44061
44139
  let abierto = null;
44062
44140
  let ultimoServicio = "Sin servicio";
44063
44141
  for (const e of evs) {
44064
- const t6 = Date.parse(e.fecha);
44065
- if (abierto) tramos.set(abierto.servicio, (tramos.get(abierto.servicio) ?? 0) + Math.max(0, t6 - abierto.desde));
44142
+ if (abierto) sumar(abierto.servicio, e.t - abierto.desde);
44066
44143
  abierto = null;
44067
44144
  if (ABREN.has(e.tipo)) {
44068
44145
  ultimoServicio = e.servicio ?? ultimoServicio;
44069
- abierto = { desde: t6, servicio: ultimoServicio };
44146
+ abierto = { desde: e.t, servicio: ultimoServicio };
44070
44147
  }
44071
44148
  }
44072
- if (abierto) tramos.set(abierto.servicio, (tramos.get(abierto.servicio) ?? 0) + Math.max(0, fin - abierto.desde));
44073
- if (tramos.size === 0) tramos.set(ultimoServicio, Math.max(0, fin - Date.parse(l.ingreso)));
44074
- const totalMs = [...tramos.values()].reduce((a, b) => a + b, 0);
44149
+ if (abierto) sumar(abierto.servicio, fin - abierto.desde);
44150
+ if (tramos.size === 0) sumar(ultimoServicio, fin - inicio);
44151
+ const totalMs = [...tramos.values()].reduce((a, t6) => a + t6.ms, 0);
44075
44152
  const total = Math.max(1, Math.ceil(totalMs / DIA_MS - 0.01));
44153
+ let activos = [...tramos.values()].filter((t6) => t6.ms > 0);
44154
+ if (activos.length === 0) activos = [{ ...tramos.get(claveServicio(ultimoServicio)) ?? { nombre: ultimoServicio }, ms: 1 }];
44155
+ const decimas = repartirDecimas(activos.map((t6) => t6.ms), total);
44156
+ const msActivos = activos.reduce((a, t6) => a + t6.ms, 0);
44076
44157
  const out = /* @__PURE__ */ new Map();
44077
- for (const [s, ms] of tramos) out.set(s, totalMs > 0 ? Math.round(ms / totalMs * total * 10) / 10 : total / tramos.size);
44078
- return { tramos: out, total };
44158
+ const exactos = /* @__PURE__ */ new Map();
44159
+ activos.forEach((t6, i) => {
44160
+ out.set(t6.nombre, decimas[i] / 10);
44161
+ exactos.set(t6.nombre, t6.ms / msActivos * total);
44162
+ });
44163
+ return { tramos: out, total, exactos };
44079
44164
  }
44080
44165
  function evaluarCostoInternacion(input2) {
44081
44166
  const { lectura: l, hoy } = input2;
@@ -44088,7 +44173,7 @@ function evaluarCostoInternacion(input2) {
44088
44173
  let honorarios = 0;
44089
44174
  let sinRegla = 0;
44090
44175
  for (const f of l.facturado) {
44091
- if (!f.profesional_id) continue;
44176
+ if (!f.profesional_id || f.liquidable === false) continue;
44092
44177
  if (!f.regla || f.regla.prof === null && f.regla.inst === null) sinRegla += 1;
44093
44178
  honorarios += honorarioDe({ regla: f.regla, monto: f.monto, cantidad: f.cantidad });
44094
44179
  }
@@ -44107,32 +44192,34 @@ function evaluarCostoInternacion(input2) {
44107
44192
  }
44108
44193
  insumos = redondear5(insumos);
44109
44194
  if (sinCosto.length) faltas.push("insumo_sin_costo");
44110
- const { tramos, total } = diasPorServicio(l, hoy);
44195
+ const { tramos, total, exactos } = diasPorServicio(l, hoy);
44111
44196
  const porServicio = input2.costoDiaPorServicio ?? {};
44197
+ const porClave = /* @__PURE__ */ new Map();
44198
+ for (const [s, c] of Object.entries(porServicio)) if (!porClave.has(claveServicio(s))) porClave.set(claveServicio(s), c);
44112
44199
  const general = input2.costoDiaCama ?? null;
44113
44200
  let cama = 0;
44114
44201
  let faltaCama = false;
44115
44202
  const dias_por_servicio = [...tramos.entries()].map(([servicio, dias]) => {
44116
- const costo_dia = porServicio[servicio] ?? general;
44203
+ const costo_dia = porServicio[servicio] ?? porClave.get(claveServicio(servicio)) ?? general;
44117
44204
  if (costo_dia === null || costo_dia === void 0) faltaCama = true;
44118
- else cama += dias * costo_dia;
44205
+ else cama += (exactos.get(servicio) ?? dias) * costo_dia;
44119
44206
  return { servicio, dias, costo_dia: costo_dia ?? null };
44120
44207
  });
44121
44208
  cama = redondear5(cama);
44122
44209
  if (faltaCama) faltas.push("sin_costo_cama");
44123
44210
  const minutos = l.cirugias.reduce((a, c) => a + (c.minutos ?? 0), 0);
44124
- if (l.cirugias.some((c) => c.minutos === null)) avisos.push("cirugia_sin_duracion");
44125
44211
  let quirofano = 0;
44126
- if (minutos > 0) {
44212
+ if (l.cirugias.length > 0) {
44127
44213
  if (input2.costoHoraQuirofano === null || input2.costoHoraQuirofano === void 0) faltas.push("sin_costo_quirofano");
44128
44214
  else quirofano = redondear5(minutos / 60 * input2.costoHoraQuirofano);
44129
44215
  }
44216
+ if (l.cirugias.some((c) => c.minutos === null)) faltas.push("cirugia_sin_duracion");
44130
44217
  const costo = redondear5(honorarios + insumos + cama + quirofano);
44131
- const calculable = faltas.length === 0;
44132
- const margen = facturado > 0 ? redondear5(facturado - costo) : null;
44133
- const margen_pct = margen !== null && facturado > 0 ? Math.round(margen / facturado * 1e3) / 10 : null;
44218
+ const calculable = faltas.length === 0 && facturado > 0;
44219
+ const margen = calculable ? redondear5(facturado - costo) : null;
44220
+ const margen_pct = margen !== null ? Math.round(margen / facturado * 1e3) / 10 : null;
44134
44221
  let clase2;
44135
- if (!calculable || margen_pct === null) clase2 = "datos_incompletos";
44222
+ if (margen === null || margen_pct === null) clase2 = "datos_incompletos";
44136
44223
  else if (margen < 0) clase2 = "perdida";
44137
44224
  else if (margen_pct < umbral) clase2 = "margen_bajo";
44138
44225
  else clase2 = "rentable";
@@ -44142,12 +44229,14 @@ var NOMBRE_FALTA2 = {
44142
44229
  sin_facturar: "no tiene nada facturado",
44143
44230
  insumo_sin_costo: "hay insumos sin costo de compra en el HIS",
44144
44231
  sin_costo_cama: "falta el costo del d\xEDa cama",
44145
- sin_costo_quirofano: "falta el costo de la hora de quir\xF3fano"
44232
+ sin_costo_quirofano: "falta el costo de la hora de quir\xF3fano",
44233
+ cirugia_sin_duracion: "hay cirug\xEDas sin la duraci\xF3n en el parte quir\xFArgico"
44146
44234
  };
44147
44235
  var porcentaje2 = (n) => `${n.toLocaleString("es-AR", { maximumFractionDigits: 1 })} %`;
44236
+ var enumerar = (xs) => xs.length > 1 ? `${xs.slice(0, -1).join(", ")} y ${xs[xs.length - 1]}` : xs[0] ?? "";
44148
44237
  function oracionCostoInternacion(l, e) {
44149
44238
  const quien = `La internaci\xF3n ${l.internacion_id} con ${l.financiador} (${e.dias} ${e.dias === 1 ? "d\xEDa" : "d\xEDas"})`;
44150
- if (e.clase === "datos_incompletos") return `${quien}: no se puede calcular el resultado porque ${e.faltas.map((f) => NOMBRE_FALTA2[f]).join(" y ")}.`;
44239
+ if (e.clase === "datos_incompletos") return `${quien}: no se puede calcular el resultado porque ${enumerar(e.faltas.map((f) => NOMBRE_FALTA2[f] ?? f.replace(/_/g, " ")))}.`;
44151
44240
  if (e.clase === "perdida") return `${quien} dio p\xE9rdida: se facturaron ${pesos2(e.facturado)} y cost\xF3 ${pesos2(e.costo)} (${pesos2(Math.abs(e.margen))} de p\xE9rdida).`;
44152
44241
  if (e.clase === "margen_bajo") return `${quien} dej\xF3 un margen de ${porcentaje2(e.margen_pct)}, debajo del ${porcentaje2(e.umbral)} de la instituci\xF3n: se facturaron ${pesos2(e.facturado)} y cost\xF3 ${pesos2(e.costo)}.`;
44153
44242
  return `${quien} dej\xF3 un margen de ${porcentaje2(e.margen_pct)}.`;
@@ -44472,10 +44561,22 @@ async function leerCasoCompleto(ctx, companyId, casoId) {
44472
44561
  async function listarCasos(ctx, input2) {
44473
44562
  const params = [input2.companyId];
44474
44563
  const where = ["company_id = $1"];
44564
+ const despuesDe = str2(input2.despuesDe);
44565
+ if (despuesDe && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(despuesDe)) throw new OpsError("validacion", `despues_de tiene que ser el id de un caso (el \xFAltimo de la p\xE1gina anterior); vino \xAB${despuesDe}\xBB`);
44566
+ const porId = input2.orden === "id" || despuesDe !== null;
44567
+ if (despuesDe) {
44568
+ params.push(despuesDe);
44569
+ where.push(`id > $${params.length}`);
44570
+ }
44475
44571
  if (input2.workflow) {
44476
44572
  params.push(input2.workflow);
44477
44573
  where.push(`workflow = $${params.length}`);
44478
44574
  }
44575
+ const referencia = str2(input2.referencia);
44576
+ if (referencia) {
44577
+ params.push(referencia);
44578
+ where.push(`referencia = $${params.length}`);
44579
+ }
44479
44580
  if (input2.estado) {
44480
44581
  params.push(input2.estado);
44481
44582
  where.push(`estado = $${params.length}`);
@@ -44486,14 +44587,18 @@ async function listarCasos(ctx, input2) {
44486
44587
  });
44487
44588
  where.push(`estado IN (${marks.join(", ")})`);
44488
44589
  }
44489
- const limit = Math.min(Math.max(Number(input2.limit ?? 50) || 50, 1), 100);
44590
+ const limit = Math.min(Math.max(Number(input2.limit ?? 50) || 50, 1), 500);
44591
+ const offset = Math.min(Math.max(Math.floor(Number(input2.offset ?? 0)) || 0, 0), 1e6);
44490
44592
  params.push(limit);
44593
+ const pLimit = params.length;
44594
+ params.push(offset);
44491
44595
  const rows = await ctx.db.query(
44492
- `SELECT ${CASO_COLS} FROM ${t3(ctx, "caso")} WHERE ${where.join(" AND ")} ORDER BY updated_at DESC LIMIT $${params.length}`,
44596
+ `SELECT ${CASO_COLS} FROM ${t3(ctx, "caso")} WHERE ${where.join(" AND ")} ORDER BY ${porId ? "id" : "updated_at DESC, id"} LIMIT $${pLimit} OFFSET $${params.length}`,
44493
44597
  params
44494
44598
  );
44495
44599
  return rows.map(mapCaso);
44496
44600
  }
44601
+ var CLAVE_UNA_POR_REFERENCIA = "una-por-referencia:";
44497
44602
  async function abrirCaso(ctx, input2) {
44498
44603
  const wf = getWorkflow(input2.workflow);
44499
44604
  if (!wf) throw new OpsError("workflow_desconocido", `workflow desconocido: ${input2.workflow}. Disponibles: ${listarWorkflows().map((w) => w.clave).join(", ")}`);
@@ -44597,18 +44702,26 @@ async function abrirCaso(ctx, input2) {
44597
44702
  extra.detectado_el = hoy;
44598
44703
  }
44599
44704
  if (referencia) {
44600
- const abiertos = await ctx.db.query(
44601
- `SELECT ${CASO_COLS} FROM ${t3(ctx, "caso")} WHERE company_id = $1 AND workflow = $2 AND referencia = $3 AND cerrado_at IS NULL ORDER BY created_at DESC LIMIT 1`,
44602
- [input2.companyId, input2.workflow, referencia]
44705
+ const soloAbiertos = wf.clave !== "rentabilidad_internacion";
44706
+ const previos = await ctx.db.query(
44707
+ `SELECT ${CASO_COLS} FROM ${t3(ctx, "caso")} WHERE company_id = $1 AND workflow = $2 AND referencia = $3${soloAbiertos ? " AND cerrado_at IS NULL" : ""} ORDER BY created_at DESC LIMIT 1`,
44708
+ [input2.companyId, wf.clave, referencia]
44603
44709
  );
44604
- if (abiertos[0]) return { caso: mapCaso(abiertos[0]), creado: false };
44710
+ if (previos[0]) return { caso: mapCaso(previos[0]), creado: false };
44605
44711
  }
44606
44712
  const id3 = randomUUID3();
44607
44713
  const datos = { ...input2.datos ?? {}, ...extra };
44608
- await ctx.db.execute(
44609
- `INSERT INTO ${t3(ctx, "caso")} (id, company_id, workflow, estado, referencia, titulo, datos, abierto_por_tipo, abierto_por_id, agente_id, issue_id, idempotency_key) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10, $11, $12)`,
44610
- [id3, input2.companyId, wf.clave, wf.inicial, referencia, titulo, JSON.stringify(datos), input2.actor.tipo, input2.actor.id, input2.agenteId ?? null, input2.issueId ?? null, idem]
44611
- );
44714
+ const clave = wf.clave === "rentabilidad_internacion" && referencia ? `${CLAVE_UNA_POR_REFERENCIA}${referencia}` : idem;
44715
+ try {
44716
+ await ctx.db.execute(
44717
+ `INSERT INTO ${t3(ctx, "caso")} (id, company_id, workflow, estado, referencia, titulo, datos, abierto_por_tipo, abierto_por_id, agente_id, issue_id, idempotency_key) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10, $11, $12)`,
44718
+ [id3, input2.companyId, wf.clave, wf.inicial, referencia, titulo, JSON.stringify(datos), input2.actor.tipo, input2.actor.id, input2.agenteId ?? null, input2.issueId ?? null, clave]
44719
+ );
44720
+ } catch (err) {
44721
+ const ganador = clave ? await ctx.db.query(`SELECT ${CASO_COLS} FROM ${t3(ctx, "caso")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`, [input2.companyId, clave]) : [];
44722
+ if (ganador[0]) return { caso: mapCaso(ganador[0]), creado: false };
44723
+ throw err;
44724
+ }
44612
44725
  const now = (/* @__PURE__ */ new Date()).toISOString();
44613
44726
  return {
44614
44727
  creado: true,
@@ -44924,22 +45037,43 @@ async function transicionar(ctx, input2) {
44924
45037
  delete datos.lectura;
44925
45038
  }
44926
45039
  if (caso.workflow === "rentabilidad_internacion") {
44927
- for (const k of ["evaluacion", "oracion", "financiador", "ingreso", "egreso", "facturado", "consumos", "cirugias", "comprobado_el"]) delete datos[k];
45040
+ for (const k of ["evaluacion", "oracion", "financiador", "ingreso", "egreso", "facturado", "consumos", "cirugias", "comprobado_el", "revisada_sin_cambios", "revision_pedida_el", "revision_vence_el", "revision_facturado", "revision_costo"]) delete datos[k];
45041
+ const hoy = contexto.hoy ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
44928
45042
  const r = contexto.costoInternacion;
44929
45043
  const l = leerLecturaCostoInternacion(datos.lectura);
44930
- if (r && !("error" in r) && !("error" in l)) {
45044
+ const nueva = r && !("error" in r) && !("error" in l) ? { lectura: l.lectura, evaluacion: r.evaluacion } : null;
45045
+ if (resultado.via === "aprobar") {
45046
+ const vista = caso.datos.evaluacion && typeof caso.datos.evaluacion === "object" ? caso.datos.evaluacion : {};
44931
45047
  Object.assign(datos, {
44932
- financiador: l.lectura.financiador,
44933
- ingreso: l.lectura.ingreso,
44934
- egreso: l.lectura.egreso,
44935
- facturado: l.lectura.facturado,
44936
- consumos: l.lectura.consumos,
44937
- cirugias: l.lectura.cirugias,
44938
- evaluacion: r.evaluacion,
44939
- oracion: oracionCostoInternacion(l.lectura, r.evaluacion),
45048
+ revision_pedida_el: hoy,
45049
+ revision_vence_el: sumarDias4(hoy, DIAS_REVISION_CUENTA),
45050
+ revision_facturado: typeof vista.facturado === "number" ? vista.facturado : null,
45051
+ revision_costo: typeof vista.costo === "number" ? vista.costo : null,
45052
+ revisada_sin_cambios: null
45053
+ });
45054
+ } else if (nueva && resultado.a === "pendiente_aprobacion") {
45055
+ if (caso.estado === "en_revision") datos.revisada_sin_cambios = resultadoRevisionCuenta(caso.datos, nueva.evaluacion, hoy) === "sin_cambios";
45056
+ else if (caso.datos.revisada_sin_cambios === true && resultadoRevisionCuenta(caso.datos, nueva.evaluacion, hoy) === "con_cambios") datos.revisada_sin_cambios = false;
45057
+ }
45058
+ if (nueva) {
45059
+ Object.assign(datos, {
45060
+ financiador: nueva.lectura.financiador,
45061
+ ingreso: nueva.lectura.ingreso,
45062
+ egreso: nueva.lectura.egreso,
45063
+ facturado: nueva.lectura.facturado,
45064
+ consumos: nueva.lectura.consumos,
45065
+ cirugias: nueva.lectura.cirugias,
45066
+ evaluacion: nueva.evaluacion,
44940
45067
  comprobado_el: contexto.hoy ?? null
44941
45068
  });
44942
45069
  }
45070
+ const despues = { ...caso.datos, ...datos };
45071
+ const e = nueva?.evaluacion ?? (despues.evaluacion && typeof despues.evaluacion === "object" ? despues.evaluacion : null);
45072
+ if (e && typeof e.facturado === "number" && typeof e.costo === "number") {
45073
+ const quien = { internacion_id: str2(despues.internacion_id) ?? "", financiador: str2(despues.financiador) ?? "" };
45074
+ const nota = notaRevisionCuenta(despues, resultado.a, e, hoy);
45075
+ datos.oracion = `${oracionCostoInternacion(nueva?.lectura ?? quien, e)}${nota ? ` ${nota}` : ""}`;
45076
+ }
44943
45077
  delete datos.lectura;
44944
45078
  }
44945
45079
  if (caso.workflow === "cobranza_financiador") {
@@ -45743,7 +45877,8 @@ var ESPERAN_PERSONA = {
45743
45877
  credencial_profesional: ["observada"],
45744
45878
  internacion_al_dia: ["con_pendientes"],
45745
45879
  honorario_en_cero: ["para_cargar"],
45746
- debito_profesional: ["aprobado", "con_diferencias"]
45880
+ debito_profesional: ["aprobado", "con_diferencias"],
45881
+ rentabilidad_internacion: ["en_revision"]
45747
45882
  };
45748
45883
  var CANALES_AVISO = ["mail", "whatsapp"];
45749
45884
  var HORARIO_AVISOS = { desde: "08:00", hasta: "18:00" };
@@ -45825,6 +45960,10 @@ function pendientesDe(caso, hoy) {
45825
45960
  const vence = texto8(d.vence_el);
45826
45961
  return [{ ...base, que: `${bono}: sin presentar${vence ? `, vence el ${vence.slice(8, 10)}/${vence.slice(5, 7)}` : ""}`, responsable: rol("facturacion") }];
45827
45962
  }
45963
+ if (caso.workflow === "rentabilidad_internacion") {
45964
+ const vence = texto8(d.revision_vence_el);
45965
+ return [{ ...base, que: `${caso.titulo}: revisar la cuenta (consumos sin facturar o m\xF3dulo con el financiador)${vence ? `, antes del ${vence.slice(8, 10)}/${vence.slice(5, 7)}` : ""}`, responsable: rol("facturacion") }];
45966
+ }
45828
45967
  if (caso.workflow === "credencial_profesional") {
45829
45968
  const id3 = texto8(d.profesional_id);
45830
45969
  const nombre = texto8(d.profesional) ?? (id3 ? `Profesional ${id3}` : "Profesional");
@@ -46375,6 +46514,13 @@ function numOrNull(v) {
46375
46514
  const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
46376
46515
  return Number.isFinite(n) ? n : null;
46377
46516
  }
46517
+ function listaTextos(v) {
46518
+ const crudos = (Array.isArray(v) ? v : [v]).flatMap((x) => typeof x === "string" ? x.split(",") : []);
46519
+ return [...new Set(crudos.map(str3).filter((s) => s !== null))].slice(0, 20);
46520
+ }
46521
+ function ordenCasos(orden, despuesDe) {
46522
+ return orden === "id" || despuesDe !== null ? "id" : null;
46523
+ }
46378
46524
  function companyFrom(params, fallback) {
46379
46525
  const companyId = str3(params.companyId) ?? str3(fallback);
46380
46526
  if (!companyId) throw new OpsError("company_requerida", "companyId es obligatorio");
@@ -46404,10 +46550,19 @@ async function conErrores(fn) {
46404
46550
  function registrarHerramientas(ctx) {
46405
46551
  ctx.tools.register(TOOL_NAMES.listarCasos, toolSchema(TOOL_NAMES.listarCasos), async (params, runCtx) => conErrores(async () => {
46406
46552
  const p = obj(params);
46407
- const casos = await listarCasos(ctx, { companyId: runCtx.companyId, workflow: str3(p.workflow), estado: str3(p.estado), limit: numOrNull(p.limit) });
46553
+ const workflow = str3(p.workflow);
46554
+ if (workflow && !getWorkflow(workflow)) throw new OpsError("workflow_desconocido", `workflow desconocido: ${workflow}. Disponibles: ${listarWorkflows().map((w) => w.clave).join(", ")}`);
46555
+ const despuesDe = str3(p.despues_de);
46556
+ const porId = ordenCasos(p.orden, despuesDe) === "id";
46557
+ const casos = await listarCasos(ctx, { companyId: runCtx.companyId, workflow, estado: str3(p.estado), estados: listaTextos(p.estados), referencia: str3(p.referencia), limit: numOrNull(p.limit), offset: numOrNull(p.offset), orden: ordenCasos(p.orden, despuesDe), despuesDe });
46558
+ const limite = Math.min(Math.max(numOrNull(p.limit) || 50, 1), 500);
46559
+ const desde = Math.max(Math.floor(numOrNull(p.offset) ?? 0), 0);
46560
+ const mas = casos.length < limite ? "" : porId ? `
46561
+ Puede haber m\xE1s casos: ped\xED la p\xE1gina siguiente con los mismos filtros, orden "id" y despues_de "${casos[casos.length - 1].id}" (limit ${limite}).` : `
46562
+ Puede haber m\xE1s casos: ped\xED la p\xE1gina siguiente con offset ${desde + casos.length} (limit ${limite}).`;
46408
46563
  return {
46409
- content: casos.length ? casos.map(resumenCaso).join("\n") : "No hay casos que coincidan.",
46410
- data: { casos }
46564
+ content: casos.length ? `${casos.map(resumenCaso).join("\n")}${mas}` : "No hay casos que coincidan.",
46565
+ data: { casos, ...mas && porId ? { despues_de: casos[casos.length - 1].id } : {} }
46411
46566
  };
46412
46567
  }));
46413
46568
  ctx.tools.register(TOOL_NAMES.leerCaso, toolSchema(TOOL_NAMES.leerCaso), async (params, runCtx) => conErrores(async () => {
@@ -46577,7 +46732,7 @@ function registrarDatosYAcciones(ctx) {
46577
46732
  transiciones: w.transiciones.map((t6) => ({ via: t6.via, de: t6.de, a: typeof t6.a === "string" ? t6.a : "(seg\xFAn datos)", actor: t6.actor, nivelRequerido: t6.nivelRequerido ?? null, descripcion: t6.descripcion }))
46578
46733
  })));
46579
46734
  ctx.data.register(DATA_KEYS.tablero, async (params) => tablero(ctx, companyFrom(params), numOrNull(params.dias)));
46580
- ctx.data.register(DATA_KEYS.casos, async (params) => listarCasos(ctx, { companyId: companyFrom(params), workflow: str3(params.workflow), estado: str3(params.estado), limit: numOrNull(params.limit) }));
46735
+ ctx.data.register(DATA_KEYS.casos, async (params) => listarCasos(ctx, { companyId: companyFrom(params), workflow: str3(params.workflow), estado: str3(params.estado), estados: listaTextos(params.estados), referencia: str3(params.referencia), limit: numOrNull(params.limit), offset: numOrNull(params.offset), orden: ordenCasos(params.orden, str3(params.despues_de)), despuesDe: str3(params.despues_de) }));
46581
46736
  ctx.data.register(DATA_KEYS.caso, async (params) => {
46582
46737
  const casoId = str3(params.casoId);
46583
46738
  if (!casoId) throw new OpsError("caso_requerido", "casoId es obligatorio");
@@ -46643,7 +46798,7 @@ async function manejarApi(ctx, input2) {
46643
46798
  case "tablero":
46644
46799
  return respuesta(200, await tablero(ctx, companyId, numOrNull(qs("dias"))));
46645
46800
  case "casos":
46646
- return respuesta(200, { casos: await listarCasos(ctx, { companyId, workflow: qs("workflow"), estado: qs("estado"), limit: numOrNull(qs("limit")) }) });
46801
+ return respuesta(200, { casos: await listarCasos(ctx, { companyId, workflow: qs("workflow"), estado: qs("estado"), estados: listaTextos(q.estados), referencia: qs("referencia"), limit: numOrNull(qs("limit")), offset: numOrNull(qs("offset")), orden: ordenCasos(qs("orden"), qs("despues_de")), despuesDe: qs("despues_de") }) });
46647
46802
  case "caso": {
46648
46803
  const detalle = await leerCasoCompleto(ctx, companyId, input2.params.casoId ?? "");
46649
46804
  return detalle ? respuesta(200, detalle) : respuesta(404, { error: "caso_inexistente" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-autopilot-ops",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
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": [