paperclip-plugin-autopilot-ops 0.1.6 → 0.1.8

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.6";
3
+ var PLUGIN_VERSION = "0.1.8";
4
4
  var DB_NAMESPACE_SLUG = "autopilot_ops";
5
5
  var PAGE_ROUTE = "autopilot";
6
6
  var TOOL_NAMES = {
@@ -34,7 +34,7 @@ var manifest = {
34
34
  apiVersion: 1,
35
35
  version: PLUGIN_VERSION,
36
36
  displayName: "Autopilot Care",
37
- description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, recupero de turnos liberados desde la lista de espera, tableros de gesti\xF3n publicados por agentes y panel del director.",
37
+ description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, recupero de turnos liberados desde la lista de espera, control de cuenta completa antes de facturar, tableros de gesti\xF3n publicados por agentes y panel del director.",
38
38
  author: "Alephoo",
39
39
  categories: ["automation", "ui"],
40
40
  capabilities: [
@@ -84,7 +84,7 @@ var manifest = {
84
84
  {
85
85
  name: TOOL_NAMES.abrirCaso,
86
86
  displayName: "Abrir caso",
87
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `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"). `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.',
87
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `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). `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.',
88
88
  parametersSchema: {
89
89
  type: "object",
90
90
  properties: {
@@ -101,7 +101,7 @@ var manifest = {
101
101
  {
102
102
  name: TOOL_NAMES.transicionar,
103
103
  displayName: "Transicionar caso",
104
- 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 del hospital 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; `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo.",
104
+ 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 del hospital 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.',
105
105
  parametersSchema: {
106
106
  type: "object",
107
107
  properties: {
@@ -123,7 +123,7 @@ var manifest = {
123
123
  properties: {
124
124
  casoId: { type: "string" },
125
125
  tipo: { type: "string", enum: ["lectura", "escritura", "verificacion"] },
126
- conector: { type: "string", enum: ["api", "archivo", "pantalla", "manual"] },
126
+ conector: { type: "string", enum: ["api", "base", "archivo", "pantalla", "manual"], description: "Por d\xF3nde se ley\xF3 o escribi\xF3: api, base (lectura de la base de datos del hospital), archivo, pantalla o manual." },
127
127
  objetivo: { type: "string" },
128
128
  resultado: { type: "string", enum: ["ok", "error", "pendiente"] },
129
129
  evidencia: { type: "object" },
@@ -221,6 +221,22 @@ var manifest = {
221
221
  capability: "api.routes.register",
222
222
  companyResolution: { from: "body", key: "companyId" }
223
223
  },
224
+ {
225
+ routeKey: "respuesta-paciente",
226
+ method: "POST",
227
+ path: "/casos/:casoId/respuesta",
228
+ auth: "board",
229
+ capability: "api.routes.register",
230
+ companyResolution: { from: "body", key: "companyId" }
231
+ },
232
+ {
233
+ routeKey: "no-corresponde",
234
+ method: "POST",
235
+ path: "/casos/:casoId/no-corresponde",
236
+ auth: "board",
237
+ capability: "api.routes.register",
238
+ companyResolution: { from: "body", key: "companyId" }
239
+ },
224
240
  {
225
241
  routeKey: "version",
226
242
  method: "GET",
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.6";
11
+ var PLUGIN_VERSION = "0.1.8";
12
12
  var DB_NAMESPACE_SLUG = "autopilot_ops";
13
13
  var PAGE_ROUTE = "autopilot";
14
14
  var TOOL_NAMES = {
@@ -42,7 +42,7 @@ var manifest = {
42
42
  apiVersion: 1,
43
43
  version: PLUGIN_VERSION,
44
44
  displayName: "Autopilot Care",
45
- description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, recupero de turnos liberados desde la lista de espera, tableros de gesti\xF3n publicados por agentes y panel del director.",
45
+ description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, recupero de turnos liberados desde la lista de espera, control de cuenta completa antes de facturar, tableros de gesti\xF3n publicados por agentes y panel del director.",
46
46
  author: "Alephoo",
47
47
  categories: ["automation", "ui"],
48
48
  capabilities: [
@@ -92,7 +92,7 @@ var manifest = {
92
92
  {
93
93
  name: TOOL_NAMES.abrirCaso,
94
94
  displayName: "Abrir caso",
95
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `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"). `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.',
95
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `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). `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.',
96
96
  parametersSchema: {
97
97
  type: "object",
98
98
  properties: {
@@ -109,7 +109,7 @@ var manifest = {
109
109
  {
110
110
  name: TOOL_NAMES.transicionar,
111
111
  displayName: "Transicionar caso",
112
- 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 del hospital 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; `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo.",
112
+ 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 del hospital 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.',
113
113
  parametersSchema: {
114
114
  type: "object",
115
115
  properties: {
@@ -131,7 +131,7 @@ var manifest = {
131
131
  properties: {
132
132
  casoId: { type: "string" },
133
133
  tipo: { type: "string", enum: ["lectura", "escritura", "verificacion"] },
134
- conector: { type: "string", enum: ["api", "archivo", "pantalla", "manual"] },
134
+ conector: { type: "string", enum: ["api", "base", "archivo", "pantalla", "manual"], description: "Por d\xF3nde se ley\xF3 o escribi\xF3: api, base (lectura de la base de datos del hospital), archivo, pantalla o manual." },
135
135
  objetivo: { type: "string" },
136
136
  resultado: { type: "string", enum: ["ok", "error", "pendiente"] },
137
137
  evidencia: { type: "object" },
@@ -229,6 +229,22 @@ var manifest = {
229
229
  capability: "api.routes.register",
230
230
  companyResolution: { from: "body", key: "companyId" }
231
231
  },
232
+ {
233
+ routeKey: "respuesta-paciente",
234
+ method: "POST",
235
+ path: "/casos/:casoId/respuesta",
236
+ auth: "board",
237
+ capability: "api.routes.register",
238
+ companyResolution: { from: "body", key: "companyId" }
239
+ },
240
+ {
241
+ routeKey: "no-corresponde",
242
+ method: "POST",
243
+ path: "/casos/:casoId/no-corresponde",
244
+ auth: "board",
245
+ capability: "api.routes.register",
246
+ companyResolution: { from: "body", key: "companyId" }
247
+ },
232
248
  {
233
249
  routeKey: "version",
234
250
  method: "GET",
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.6";
39935
+ var PLUGIN_VERSION = "0.1.8";
39936
39936
  var DB_NAMESPACE_SLUG = "autopilot_ops";
39937
39937
  var PAGE_ROUTE = "autopilot";
39938
39938
  var TOOL_NAMES = {
@@ -39966,7 +39966,7 @@ var manifest = {
39966
39966
  apiVersion: 1,
39967
39967
  version: PLUGIN_VERSION,
39968
39968
  displayName: "Autopilot Care",
39969
- description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, recupero de turnos liberados desde la lista de espera, tableros de gesti\xF3n publicados por agentes y panel del director.",
39969
+ description: "Operaci\xF3n aut\xF3noma gobernada para instituciones de salud: workflows determin\xEDsticos por puesto, validaciones, registro de negocio con evidencia, pol\xEDticas (observa / propone / ejecuta / aut\xF3nomo / corte), carga manual del equipo del hospital con comprobaci\xF3n del agente, recupero de turnos liberados desde la lista de espera, control de cuenta completa antes de facturar, tableros de gesti\xF3n publicados por agentes y panel del director.",
39970
39970
  author: "Alephoo",
39971
39971
  categories: ["automation", "ui"],
39972
39972
  capabilities: [
@@ -40016,7 +40016,7 @@ var manifest = {
40016
40016
  {
40017
40017
  name: TOOL_NAMES.abrirCaso,
40018
40018
  displayName: "Abrir caso",
40019
- description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `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"). `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.',
40019
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `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). `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.',
40020
40020
  parametersSchema: {
40021
40021
  type: "object",
40022
40022
  properties: {
@@ -40033,7 +40033,7 @@ var manifest = {
40033
40033
  {
40034
40034
  name: TOOL_NAMES.transicionar,
40035
40035
  displayName: "Transicionar caso",
40036
- 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 del hospital 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; `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo.",
40036
+ 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 del hospital 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.',
40037
40037
  parametersSchema: {
40038
40038
  type: "object",
40039
40039
  properties: {
@@ -40055,7 +40055,7 @@ var manifest = {
40055
40055
  properties: {
40056
40056
  casoId: { type: "string" },
40057
40057
  tipo: { type: "string", enum: ["lectura", "escritura", "verificacion"] },
40058
- conector: { type: "string", enum: ["api", "archivo", "pantalla", "manual"] },
40058
+ conector: { type: "string", enum: ["api", "base", "archivo", "pantalla", "manual"], description: "Por d\xF3nde se ley\xF3 o escribi\xF3: api, base (lectura de la base de datos del hospital), archivo, pantalla o manual." },
40059
40059
  objetivo: { type: "string" },
40060
40060
  resultado: { type: "string", enum: ["ok", "error", "pendiente"] },
40061
40061
  evidencia: { type: "object" },
@@ -40153,6 +40153,22 @@ var manifest = {
40153
40153
  capability: "api.routes.register",
40154
40154
  companyResolution: { from: "body", key: "companyId" }
40155
40155
  },
40156
+ {
40157
+ routeKey: "respuesta-paciente",
40158
+ method: "POST",
40159
+ path: "/casos/:casoId/respuesta",
40160
+ auth: "board",
40161
+ capability: "api.routes.register",
40162
+ companyResolution: { from: "body", key: "companyId" }
40163
+ },
40164
+ {
40165
+ routeKey: "no-corresponde",
40166
+ method: "POST",
40167
+ path: "/casos/:casoId/no-corresponde",
40168
+ auth: "board",
40169
+ capability: "api.routes.register",
40170
+ companyResolution: { from: "body", key: "companyId" }
40171
+ },
40156
40172
  {
40157
40173
  routeKey: "version",
40158
40174
  method: "GET",
@@ -40470,6 +40486,26 @@ function fueraDePreferencia(preferencia, hora) {
40470
40486
  if (p.includes("tarde")) return hora < "13:00";
40471
40487
  return false;
40472
40488
  }
40489
+ var MARGEN_MINUTOS_ASIGNAR = 60;
40490
+ function minutosHastaLaCita(l, contexto) {
40491
+ const hora = horaHM(l.hora);
40492
+ if (!fechaISO(l.fecha) || !hora || !contexto.hoy || !contexto.horaActual) return null;
40493
+ const cita = Date.parse(`${String(l.fecha)}T${hora}:00Z`);
40494
+ const ahora = Date.parse(`${contexto.hoy}T${contexto.horaActual}:00Z`);
40495
+ return Number.isNaN(cita) || Number.isNaN(ahora) ? null : Math.round((cita - ahora) / 6e4);
40496
+ }
40497
+ function validarMargen(caso, contexto) {
40498
+ const minutos = minutosHastaLaCita(caso.datos, contexto);
40499
+ if (minutos === null) return ok("margen_para_avisar", "sin fecha y hora para medir");
40500
+ return minutos >= MARGEN_MINUTOS_ASIGNAR ? ok("margen_para_avisar", `faltan ${minutos} min`) : falla("margen_para_avisar", minutos <= 0 ? "la cita ya pas\xF3" : `la cita es en ${minutos} min: hacen falta al menos ${MARGEN_MINUTOS_ASIGNAR} para que el paciente se entere`);
40501
+ }
40502
+ function validarPacienteAcepto(caso, contexto) {
40503
+ if (!contexto.ofrecerPrimero) return ok("paciente_acepto", "la instituci\xF3n no pide consulta previa");
40504
+ return caso.estado === "aceptado" ? ok("paciente_acepto", "el paciente acept\xF3") : falla("paciente_acepto", "en esta instituci\xF3n el paciente tiene que aceptar la cita antes de asignarla: primero ofrecer y esperar que acepte");
40505
+ }
40506
+ function declinadas(caso) {
40507
+ return Array.isArray(caso.datos.declinaron) ? caso.datos.declinaron.map((v) => refExterna(v)).filter((v) => v !== null) : [];
40508
+ }
40473
40509
  function validarLugar(caso, datos, contexto) {
40474
40510
  const l = { ...caso.datos, ...datos };
40475
40511
  const completo = fechaISO(l.fecha) && horaHM(l.hora) !== null && texto(l.profesional) !== null && texto(l.especialidad) !== null;
@@ -40484,7 +40520,7 @@ function validarLugar(caso, datos, contexto) {
40484
40520
  }
40485
40521
  var recuperoTurno = {
40486
40522
  clave: "recupero_turno",
40487
- nombre: "Recupero de turnos liberados",
40523
+ nombre: "Recupero de citas",
40488
40524
  puesto: "Pacientes y agenda",
40489
40525
  descripcion: "Detecta un turno que se liber\xF3 en la agenda, elige de la lista de espera (solicitudes de turno) a quien corresponde por especialidad, prioridad y antig\xFCedad, le asigna el turno en el sistema de gesti\xF3n y comprueba reley\xE9ndolo que qued\xF3 asignado.",
40490
40526
  inicial: "detectado",
@@ -40494,6 +40530,7 @@ var recuperoTurno = {
40494
40530
  "aprobado",
40495
40531
  "ofrecido",
40496
40532
  "aceptado",
40533
+ "asignando",
40497
40534
  "asignado",
40498
40535
  "verificado",
40499
40536
  "con_diferencias",
@@ -40522,7 +40559,10 @@ var recuperoTurno = {
40522
40559
  const espSolicitud = normalizarNombre(datos.solicitud_especialidad);
40523
40560
  out.push(esp && espSolicitud && esp === espSolicitud ? ok("especialidad_coincide") : falla("especialidad_coincide", `la solicitud es de ${texto(datos.solicitud_especialidad) ?? "(sin especialidad)"} y el turno es de ${texto(l.especialidad) ?? "(sin especialidad)"}`));
40524
40561
  out.push(contexto.solicitudEnOtroCaso ? falla("solicitud_libre", `esa solicitud ya tiene otro recupero en curso o asignado (${contexto.solicitudEnOtroCaso})`) : ok("solicitud_libre"));
40525
- const { posicion, total } = posicionEnCola(datos, contexto.solicitudesEnCurso);
40562
+ const yaNo = declinadas(caso);
40563
+ const elegida = refExterna(datos.solicitud_id);
40564
+ out.push(elegida && yaNo.includes(elegida) ? falla("no_la_rechazo", "el paciente de esa solicitud ya no acept\xF3 esta cita: proponer a la siguiente") : ok("no_la_rechazo"));
40565
+ const { posicion, total } = posicionEnCola(datos, [...contexto.solicitudesEnCurso ?? [], ...yaNo]);
40526
40566
  if (!leerCola(datos.cola)) out.push(falla("orden_de_la_cola", "cola es obligatoria: la lista de espera pendiente de esa especialidad tal como se vio, [{solicitud_id, prioridad, alta}]"));
40527
40567
  else if (posicion === null) out.push(falla("orden_de_la_cola", "la solicitud elegida no est\xE1 en la cola informada"));
40528
40568
  else if (posicion > 1 && !texto(datos.justificacion)) out.push(falla("orden_de_la_cola", `es la ${posicion}.\xAA de ${total} en la cola: pasarla adelante de las anteriores requiere justificacion`));
@@ -40534,7 +40574,7 @@ var recuperoTurno = {
40534
40574
  aSinExcepcion: "aprobado",
40535
40575
  excepciones: (caso, datos, contexto) => {
40536
40576
  const l = { ...caso.datos, ...datos };
40537
- const { posicion, total } = posicionEnCola(datos, contexto.solicitudesEnCurso);
40577
+ const { posicion, total } = posicionEnCola(datos, [...contexto.solicitudesEnCurso ?? [], ...declinadas(caso)]);
40538
40578
  const pedido = normalizarNombre(datos.solicitud_profesional);
40539
40579
  const profesional = normalizarNombre(l.profesional);
40540
40580
  const paraHoy = Boolean(contexto.hoy && l.fecha === contexto.hoy);
@@ -40574,8 +40614,11 @@ var recuperoTurno = {
40574
40614
  a: "ofrecido",
40575
40615
  actor: ["agente", "humano"],
40576
40616
  nivelRequerido: "ejecuta",
40577
- descripcion: "Ofrecerle el turno al paciente antes de asignarlo (paso opcional seg\xFAn el hospital).",
40578
- validar: (_caso, datos) => [texto(datos.canal) ? ok("canal_informado") : falla("canal_informado", "ofrecer requiere canal (tel\xE9fono, WhatsApp, correo)")]
40617
+ descripcion: "Ofrecerle la cita al paciente antes de asignarla (obligatorio donde la instituci\xF3n pide consulta previa).",
40618
+ validar: (caso, datos, contexto) => [
40619
+ texto(datos.canal) ? ok("canal_informado") : falla("canal_informado", "ofrecer requiere canal (tel\xE9fono, WhatsApp, correo)"),
40620
+ validarMargen(caso, contexto)
40621
+ ]
40579
40622
  },
40580
40623
  {
40581
40624
  via: "aceptar",
@@ -40596,16 +40639,34 @@ var recuperoTurno = {
40596
40639
  validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "no_acepta requiere motivo (no contest\xF3, no puede ese d\xEDa\u2026)")]
40597
40640
  },
40598
40641
  {
40599
- via: "asignar",
40642
+ // El plugin autoriza ANTES de que se escriba en el sistema del hospital: la cita está a tiempo
40643
+ // y, donde la institución lo pide, el paciente aceptó. Sin esta autorización el agente no escribe.
40644
+ via: "autorizar",
40600
40645
  de: ["aprobado", "aceptado"],
40646
+ a: "asignando",
40647
+ actor: ["agente", "humano"],
40648
+ nivelRequerido: "ejecuta",
40649
+ descripcion: "Pedir autorizaci\xF3n para asignar la cita en el sistema de gesti\xF3n. Va siempre antes de escribir.",
40650
+ validar: (caso, _datos, contexto) => [
40651
+ ...validarLugar(caso, {}, contexto),
40652
+ validarMargen(caso, contexto),
40653
+ validarPacienteAcepto(caso, contexto)
40654
+ ]
40655
+ },
40656
+ {
40657
+ // Registra lo que ya se escribió: no se rechaza por la hora (la escritura ocurrió). Un agente solo
40658
+ // llega acá con la autorización; una persona que lo hizo a mano, desde aprobado o aceptado.
40659
+ via: "asignar",
40660
+ de: ["asignando", "aprobado", "aceptado"],
40601
40661
  a: "asignado",
40602
40662
  actor: ["agente", "humano"],
40603
40663
  nivelRequerido: "ejecuta",
40604
40664
  cargaManual: true,
40605
- descripcion: "Dar el turno a la solicitud aprobada en el sistema de gesti\xF3n y registrar la acci\xF3n.",
40665
+ descripcion: "Registrar la cita asignada en el sistema de gesti\xF3n, con la acci\xF3n de escritura.",
40606
40666
  validar: (caso, datos, contexto) => [
40607
- ...validarLugar(caso, {}, contexto),
40608
- texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "asignar requiere accion_id de la escritura registrada con ops_registrar_accion")
40667
+ texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "asignar requiere accion_id de la escritura registrada con ops_registrar_accion"),
40668
+ contexto.actor === "agente" && caso.estado !== "asignando" ? falla("escritura_autorizada", "un agente tiene que pedir autorizar antes de escribir en el sistema") : ok("escritura_autorizada"),
40669
+ ...caso.estado === "asignando" ? [] : [validarPacienteAcepto(caso, contexto)]
40609
40670
  ]
40610
40671
  },
40611
40672
  {
@@ -40645,7 +40706,7 @@ var recuperoTurno = {
40645
40706
  },
40646
40707
  {
40647
40708
  via: "descartar",
40648
- de: ["detectado", "pendiente_aprobacion", "aprobado", "ofrecido", "aceptado"],
40709
+ de: ["detectado", "pendiente_aprobacion", "aprobado", "ofrecido", "aceptado", "asignando"],
40649
40710
  a: "cancelado",
40650
40711
  actor: ["agente", "humano"],
40651
40712
  nivelRequerido: "observa",
@@ -40654,10 +40715,146 @@ var recuperoTurno = {
40654
40715
  }
40655
40716
  ]
40656
40717
  };
40718
+ var REGLAS_CUENTA = {
40719
+ autorizacion_faltante: {
40720
+ nombre: "Falta el n\xFAmero de autorizaci\xF3n",
40721
+ dondeSeCorrige: "Alephoo \u203A Facturaci\xF3n \u203A Preparar Informaci\xF3n \u203A Editar bono: n\xFAmero de autorizaci\xF3n de la prestaci\xF3n (o \xABValidar con broker\xBB)"
40722
+ },
40723
+ validacion_faltante: {
40724
+ nombre: "Falta la validaci\xF3n online del financiador",
40725
+ dondeSeCorrige: "Alephoo \u203A Facturaci\xF3n \u203A Preparar Informaci\xF3n \u203A Editar bono: \xABValidar con broker\xBB en la prestaci\xF3n"
40726
+ },
40727
+ afiliado_faltante: {
40728
+ nombre: "Falta el n\xFAmero de afiliado",
40729
+ dondeSeCorrige: "Alephoo \u203A Facturaci\xF3n \u203A Preparar Informaci\xF3n \u203A Editar bono: n\xFAmero de afiliado (tambi\xE9n actualiza el padr\xF3n del paciente)"
40730
+ },
40731
+ orden_faltante: {
40732
+ nombre: "Faltan los datos de la orden m\xE9dica",
40733
+ dondeSeCorrige: "Alephoo \u203A Facturaci\xF3n \u203A Preparar Informaci\xF3n \u203A Editar bono: m\xE9dico solicitante y fecha de la receta de la prestaci\xF3n"
40734
+ },
40735
+ prestacion_sin_valor: {
40736
+ nombre: "Prestaci\xF3n valorizada en $0",
40737
+ dondeSeCorrige: "Alephoo \u203A Facturaci\xF3n \u203A Configuraci\xF3n de convenios: cargar el valor de la pr\xE1ctica; despu\xE9s Facturaci\xF3n \u203A Actualizar Bonos"
40738
+ },
40739
+ epicrisis_faltante: {
40740
+ nombre: "Internaci\xF3n con egreso y sin epicrisis",
40741
+ dondeSeCorrige: "Alephoo \u203A historia cl\xEDnica del paciente internado \u203A Epicrisis"
40742
+ }
40743
+ };
40744
+ var BONO_POR_FACTURAR = ["no_confirmado", "confirmado", "prefacturado"];
40745
+ var BONO_FACTURADO = ["facturado", "refacturado"];
40746
+ function leerFaltantes(v) {
40747
+ if (!Array.isArray(v)) return null;
40748
+ const out = [];
40749
+ for (const e of v) {
40750
+ if (!e || typeof e !== "object") return null;
40751
+ const r = e;
40752
+ const regla = texto(r.regla);
40753
+ const detalle = texto(r.detalle);
40754
+ if (!regla || !REGLAS_CUENTA[regla] || !detalle) return null;
40755
+ out.push({ regla, detalle, item: refExterna(r.item) });
40756
+ }
40757
+ return out;
40758
+ }
40759
+ function validarFaltantes(campo, v, alMenosUno) {
40760
+ const lista = leerFaltantes(v);
40761
+ if (!lista) {
40762
+ return falla("faltantes_validos", `${campo} tiene que ser una lista [{regla, detalle, item?}] con reglas del cat\xE1logo: ${Object.keys(REGLAS_CUENTA).join(", ")}`);
40763
+ }
40764
+ if (alMenosUno && lista.length === 0) return falla("faltantes_validos", `${campo} est\xE1 vac\xEDo: una cuenta sin faltantes no se abre`);
40765
+ return ok("faltantes_validos", lista.length === 0 ? "sin faltantes" : lista.length === 1 ? "1 faltante" : `${lista.length} faltantes`);
40766
+ }
40767
+ function estadoBono(v) {
40768
+ const s = texto(v);
40769
+ return s ? s.toLowerCase().replace(/[\s-]+/g, "_") : null;
40770
+ }
40771
+ var cuentaCompleta = {
40772
+ clave: "cuenta_completa",
40773
+ nombre: "Cuenta completa antes de facturar",
40774
+ puesto: "Admisi\xF3n y cuenta",
40775
+ descripcion: "Lee en el sistema del hospital cada prestaci\xF3n pendiente de facturar y marca lo que le falta para facturarse sin d\xE9bito (autorizaci\xF3n, validaci\xF3n, afiliado, orden, valor, epicrisis). El equipo lo completa y el puesto lo comprueba volviendo a leer. El puesto no escribe en el sistema.",
40776
+ inicial: "detectado",
40777
+ estados: ["detectado", "para_completar", "completado", "con_faltantes", "completa", "facturado_incompleto", "no_corresponde", "descartado"],
40778
+ finalesOk: ["completa"],
40779
+ finalesNoOk: ["facturado_incompleto", "no_corresponde", "descartado"],
40780
+ minutosAbsorbidos: 10,
40781
+ transiciones: [
40782
+ {
40783
+ via: "observar",
40784
+ de: ["detectado"],
40785
+ a: "para_completar",
40786
+ actor: ["agente", "humano"],
40787
+ nivelRequerido: "observa",
40788
+ descripcion: "Informar lo que le falta a la prestaci\xF3n (bono) para facturarse, con la lectura registrada que lo muestra. Solo faltantes del cat\xE1logo; el bono tiene que estar todav\xEDa sin facturar.",
40789
+ validar: (caso, datos) => {
40790
+ const l = { ...caso.datos, ...datos };
40791
+ const estado = estadoBono(datos.bono_estado);
40792
+ return [
40793
+ refExterna(l.bono_id) ? ok("bono_presente") : falla("bono_presente", "observar requiere bono_id: el n\xFAmero de bono en el sistema del hospital"),
40794
+ estado && BONO_POR_FACTURAR.includes(estado) ? ok("bono_sin_facturar", estado) : falla("bono_sin_facturar", `el bono tiene que estar sin facturar (${BONO_POR_FACTURAR.join(", ")}); est\xE1: ${texto(datos.bono_estado) ?? "sin informar"}`),
40795
+ validarFaltantes("faltantes", datos.faltantes, true),
40796
+ texto(datos.lectura_id) ? ok("lectura_registrada") : falla("lectura_registrada", "observar requiere lectura_id: la lectura de la base registrada con ops_registrar_accion"),
40797
+ texto(datos.fuente) ? ok("fuente_presente") : falla("fuente_presente", "fuente (base y consulta de donde salen los faltantes) es obligatoria")
40798
+ ];
40799
+ }
40800
+ },
40801
+ {
40802
+ via: "completar",
40803
+ de: ["para_completar", "con_faltantes"],
40804
+ a: "completado",
40805
+ actor: ["humano"],
40806
+ cargaManual: true,
40807
+ descripcion: "Una persona del hospital complet\xF3 en el sistema lo que faltaba. El puesto lo comprueba volviendo a leer.",
40808
+ validar: (_caso, datos) => [
40809
+ texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "completar requiere accion_id de la carga registrada")
40810
+ ]
40811
+ },
40812
+ {
40813
+ // Lo decide el plugin con lo que se volvió a leer: sin faltantes, completa; facturada con faltantes, sale con riesgo de débito.
40814
+ via: "comprobar",
40815
+ de: ["para_completar", "completado", "con_faltantes"],
40816
+ a: (caso, datos) => {
40817
+ const faltan = leerFaltantes(datos.faltantes_leidos) ?? [];
40818
+ if (faltan.length === 0) return "completa";
40819
+ if (BONO_FACTURADO.includes(estadoBono(datos.bono_estado_leido) ?? "")) return "facturado_incompleto";
40820
+ return caso.estado === "completado" ? "con_faltantes" : caso.estado;
40821
+ },
40822
+ actor: ["agente", "humano", "sistema"],
40823
+ nivelRequerido: "observa",
40824
+ descripcion: "Volver a leer el bono en el sistema e informar lo que todav\xEDa falta (lista vac\xEDa si est\xE1 completo) y su estado. El plugin decide: completa, sigue para completar, con faltantes o facturada incompleta.",
40825
+ validar: (_caso, datos) => {
40826
+ const estado = estadoBono(datos.bono_estado_leido);
40827
+ return [
40828
+ validarFaltantes("faltantes_leidos", datos.faltantes_leidos, false),
40829
+ estado && [...BONO_POR_FACTURAR, ...BONO_FACTURADO].includes(estado) ? ok("estado_leido", estado) : falla("estado_leido", `bono_estado_leido tiene que ser uno de: ${[...BONO_POR_FACTURAR, ...BONO_FACTURADO].join(", ")} (si el bono se elimin\xF3 o se anul\xF3, descartar)`),
40830
+ texto(datos.lectura_id) ? ok("lectura_registrada") : falla("lectura_registrada", "comprobar requiere lectura_id: la lectura de la base registrada con ops_registrar_accion")
40831
+ ];
40832
+ }
40833
+ },
40834
+ {
40835
+ via: "no_corresponde",
40836
+ de: ["para_completar", "completado", "con_faltantes"],
40837
+ a: "no_corresponde",
40838
+ actor: ["humano"],
40839
+ descripcion: "El equipo indica que lo marcado no hace falta para esta prestaci\xF3n (por ejemplo, el financiador no pide autorizaci\xF3n). Queda registrado con el motivo.",
40840
+ validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "no_corresponde requiere motivo")]
40841
+ },
40842
+ {
40843
+ via: "descartar",
40844
+ de: ["detectado", "para_completar", "completado", "con_faltantes"],
40845
+ a: "descartado",
40846
+ actor: ["agente", "humano"],
40847
+ nivelRequerido: "observa",
40848
+ descripcion: "El bono se elimin\xF3, se anul\xF3 o qued\xF3 como no cobrable: ya no hay nada que completar.",
40849
+ validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "descartar requiere motivo")]
40850
+ }
40851
+ ]
40852
+ };
40657
40853
  var WORKFLOWS = {
40658
40854
  [actualizacionArancel.clave]: actualizacionArancel,
40659
40855
  [tareaOperativa.clave]: tareaOperativa,
40660
- [recuperoTurno.clave]: recuperoTurno
40856
+ [recuperoTurno.clave]: recuperoTurno,
40857
+ [cuentaCompleta.clave]: cuentaCompleta
40661
40858
  };
40662
40859
  function getWorkflow(clave) {
40663
40860
  return WORKFLOWS[clave] ?? null;
@@ -40674,7 +40871,7 @@ function modoHabilita(modo, nivelRequerido) {
40674
40871
  function evaluarTransicion(input2) {
40675
40872
  const { caso, via, actor } = input2;
40676
40873
  const datos = input2.datos ?? {};
40677
- const contexto = input2.contexto ?? {};
40874
+ const contexto = { ...input2.contexto ?? {}, actor };
40678
40875
  const wf = getWorkflow(caso.workflow);
40679
40876
  if (!wf) {
40680
40877
  return { ok: false, codigo: "workflow_desconocido", motivo: `workflow desconocido: ${caso.workflow}`, validaciones: [] };
@@ -40840,9 +41037,16 @@ async function obtenerModo(ctx, companyId, workflow) {
40840
41037
  const modo = rows[0]?.modo;
40841
41038
  return typeof modo === "string" ? modo : MODO_POR_DEFECTO;
40842
41039
  }
41040
+ async function obtenerOpciones(ctx, companyId, workflow) {
41041
+ const rows = await ctx.db.query(
41042
+ `SELECT opciones FROM ${t(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
41043
+ [companyId, workflow]
41044
+ );
41045
+ return jsonb(rows[0]?.opciones);
41046
+ }
40843
41047
  async function listarPoliticas(ctx, companyId) {
40844
41048
  const rows = await ctx.db.query(
40845
- `SELECT workflow, modo, motivo, updated_at FROM ${t(ctx, "politica")} WHERE company_id = $1`,
41049
+ `SELECT workflow, modo, motivo, opciones, updated_at FROM ${t(ctx, "politica")} WHERE company_id = $1`,
40846
41050
  [companyId]
40847
41051
  );
40848
41052
  const porWorkflow = new Map(rows.map((r) => [String(r.workflow), r]));
@@ -40855,40 +41059,50 @@ async function listarPoliticas(ctx, companyId) {
40855
41059
  modo: r ? String(r.modo) : MODO_POR_DEFECTO,
40856
41060
  motivo: r ? r.motivo ?? null : null,
40857
41061
  actualizadaEn: r ? iso(r.updated_at) : null,
40858
- porDefecto: !r
41062
+ porDefecto: !r,
41063
+ opciones: r ? jsonb(r.opciones) : {}
40859
41064
  };
40860
41065
  });
40861
41066
  }
41067
+ function describirOpciones(antes, despues) {
41068
+ const partes = [];
41069
+ if (Boolean(antes.ofrecer_primero) !== Boolean(despues.ofrecer_primero)) partes.push(`consultar al paciente antes de asignar: ${despues.ofrecer_primero ? "s\xED" : "no"}`);
41070
+ return partes.length ? partes.join(" \xB7 ") : null;
41071
+ }
40862
41072
  async function cambiarPolitica(ctx, input2) {
40863
41073
  if (!getWorkflow(input2.workflow)) throw new OpsError("workflow_desconocido", `workflow desconocido: ${input2.workflow}`);
40864
41074
  if (input2.actor.tipo !== "humano") throw new OpsError("actor_no_permitido", "solo un humano puede cambiar la pol\xEDtica de un workflow");
40865
41075
  const anterior = await obtenerModo(ctx, input2.companyId, input2.workflow);
41076
+ const opcionesAntes = await obtenerOpciones(ctx, input2.companyId, input2.workflow);
41077
+ const opciones = { ...opcionesAntes, ...input2.opciones ?? {} };
41078
+ const cambioOpciones = describirOpciones(opcionesAntes, opciones);
41079
+ const motivo = [input2.motivo ?? null, cambioOpciones].filter(Boolean).join(" \xB7 ") || null;
40866
41080
  const existe = await ctx.db.query(
40867
41081
  `SELECT id FROM ${t(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
40868
41082
  [input2.companyId, input2.workflow]
40869
41083
  );
40870
41084
  if (existe.length > 0) {
40871
41085
  await ctx.db.execute(
40872
- `UPDATE ${t(ctx, "politica")} SET modo = $3, motivo = $4, actor_tipo = $5, actor_id = $6, updated_at = now() WHERE company_id = $1 AND workflow = $2`,
40873
- [input2.companyId, input2.workflow, input2.modo, input2.motivo ?? null, input2.actor.tipo, input2.actor.id]
41086
+ `UPDATE ${t(ctx, "politica")} SET modo = $3, motivo = $4, actor_tipo = $5, actor_id = $6, opciones = $7::jsonb, updated_at = now() WHERE company_id = $1 AND workflow = $2`,
41087
+ [input2.companyId, input2.workflow, input2.modo, motivo, input2.actor.tipo, input2.actor.id, JSON.stringify(opciones)]
40874
41088
  );
40875
41089
  } else {
40876
41090
  await ctx.db.execute(
40877
- `INSERT INTO ${t(ctx, "politica")} (id, company_id, workflow, modo, motivo, actor_tipo, actor_id) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
40878
- [randomUUID(), input2.companyId, input2.workflow, input2.modo, input2.motivo ?? null, input2.actor.tipo, input2.actor.id]
41091
+ `INSERT INTO ${t(ctx, "politica")} (id, company_id, workflow, modo, motivo, actor_tipo, actor_id, opciones) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)`,
41092
+ [randomUUID(), input2.companyId, input2.workflow, input2.modo, motivo, input2.actor.tipo, input2.actor.id, JSON.stringify(opciones)]
40879
41093
  );
40880
41094
  }
40881
41095
  await ctx.db.execute(
40882
41096
  `INSERT INTO ${t(ctx, "politica_historial")} (id, company_id, workflow, modo_anterior, modo_nuevo, motivo, actor_tipo, actor_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
40883
- [randomUUID(), input2.companyId, input2.workflow, anterior, input2.modo, input2.motivo ?? null, input2.actor.tipo, input2.actor.id]
41097
+ [randomUUID(), input2.companyId, input2.workflow, anterior, input2.modo, motivo, input2.actor.tipo, input2.actor.id]
40884
41098
  );
40885
41099
  await ctx.activity.log({
40886
41100
  companyId: input2.companyId,
40887
- message: `Autopilot Care: pol\xEDtica de ${input2.workflow} cambiada de ${anterior} a ${input2.modo}${input2.motivo ? ` (${input2.motivo})` : ""}`,
41101
+ message: `Autopilot Care: pol\xEDtica de ${input2.workflow} cambiada de ${anterior} a ${input2.modo}${motivo ? ` (${motivo})` : ""}`,
40888
41102
  metadata: { workflow: input2.workflow, modoAnterior: anterior, modoNuevo: input2.modo, actorId: input2.actor.id }
40889
41103
  });
40890
41104
  const wf = getWorkflow(input2.workflow);
40891
- return { workflow: wf.clave, nombre: wf.nombre, puesto: wf.puesto, modo: input2.modo, motivo: input2.motivo ?? null, actualizadaEn: (/* @__PURE__ */ new Date()).toISOString(), porDefecto: false };
41105
+ return { workflow: wf.clave, nombre: wf.nombre, puesto: wf.puesto, modo: input2.modo, motivo, actualizadaEn: (/* @__PURE__ */ new Date()).toISOString(), porDefecto: false, opciones };
40892
41106
  }
40893
41107
  async function leerCaso(ctx, companyId, casoId) {
40894
41108
  const rows = await ctx.db.query(
@@ -40960,6 +41174,11 @@ async function abrirCaso(ctx, input2) {
40960
41174
  if (prev[0]) return { caso: mapCaso(prev[0]), creado: false };
40961
41175
  }
40962
41176
  const referencia = str(input2.referencia);
41177
+ if (wf.clave === "cuenta_completa") {
41178
+ const bono = refExterna(input2.datos?.bono_id);
41179
+ if (!referencia || !/^bono:\S+$/.test(referencia)) throw new OpsError("referencia_requerida", "en cuenta_completa la referencia es obligatoria: bono:<n\xFAmero de bono>");
41180
+ if (bono && referencia !== `bono:${bono}`) throw new OpsError("referencia_requerida", `la referencia ${referencia} no coincide con el bono_id ${bono}`);
41181
+ }
40963
41182
  if (referencia) {
40964
41183
  const abiertos = await ctx.db.query(
40965
41184
  `SELECT ${CASO_COLS} FROM ${t(ctx, "caso")} WHERE company_id = $1 AND workflow = $2 AND referencia = $3 AND cerrado_at IS NULL ORDER BY created_at DESC LIMIT 1`,
@@ -41005,7 +41224,9 @@ function ahoraEnHospital(ahora = /* @__PURE__ */ new Date()) {
41005
41224
  async function contextoDe(ctx, companyId, caso, via, datos) {
41006
41225
  const { hoy, hora, inicioDelDia } = ahoraEnHospital();
41007
41226
  const contexto = { hoy, horaActual: hora };
41008
- if (caso.workflow !== "recupero_turno" || via !== "proponer") return contexto;
41227
+ if (caso.workflow !== "recupero_turno") return contexto;
41228
+ contexto.ofrecerPrimero = (await obtenerOpciones(ctx, companyId, caso.workflow)).ofrecer_primero === true;
41229
+ if (via !== "proponer") return contexto;
41009
41230
  const otros = await ctx.db.query(
41010
41231
  `SELECT id, datos->>'solicitud_id' AS solicitud_id FROM ${t(ctx, "caso")} WHERE company_id = $1 AND workflow = $2 AND id <> $3 AND datos->>'solicitud_id' IS NOT NULL AND estado NOT IN ($4, $5, $6) LIMIT 1000`,
41011
41232
  [companyId, "recupero_turno", caso.id, "sin_candidato", "rechazado", "cancelado"]
@@ -41052,6 +41273,40 @@ async function transicionar(ctx, input2) {
41052
41273
  throw new OpsError("validacion", `accion_registrada: ${problema}`, { validaciones: [{ regla: "accion_registrada", ok: false, detalle: problema }], estado: caso.estado });
41053
41274
  }
41054
41275
  }
41276
+ const lecturaId = str(datos.lectura_id);
41277
+ if (lecturaId) {
41278
+ const rows = await ctx.db.query(
41279
+ `SELECT id, caso_id, tipo, resultado, created_at FROM ${t(ctx, "accion")} WHERE company_id = $1 AND id = $2 LIMIT 1`,
41280
+ [input2.companyId, lecturaId]
41281
+ );
41282
+ const lectura = rows[0];
41283
+ let vieja = false;
41284
+ if (lectura && resultado.via === "comprobar") {
41285
+ const ultima = await ctx.db.query(
41286
+ `SELECT max(created_at) AS ultima FROM ${t(ctx, "transicion")} WHERE company_id = $1 AND caso_id = $2`,
41287
+ [input2.companyId, caso.id]
41288
+ );
41289
+ const desde = ultima[0]?.ultima ? Date.parse(iso(ultima[0].ultima)) : NaN;
41290
+ vieja = Number.isFinite(desde) && Date.parse(iso(lectura.created_at)) <= desde;
41291
+ }
41292
+ const problema = !lectura ? `lectura_id ${lecturaId} no existe en esta instituci\xF3n` : String(lectura.caso_id) !== caso.id ? `la lectura ${lecturaId} pertenece a otro caso` : !["lectura", "verificacion"].includes(String(lectura.tipo)) ? `la acci\xF3n ${lecturaId} es de tipo ${String(lectura.tipo)}; hace falta una lectura o verificaci\xF3n` : String(lectura.resultado) !== "ok" ? `la lectura ${lecturaId} termin\xF3 con resultado ${String(lectura.resultado)}` : vieja ? `la lectura ${lecturaId} es anterior al \xFAltimo cambio del caso: volver a leer el bono y registrar esa lectura` : null;
41293
+ if (problema) {
41294
+ throw new OpsError("validacion", `lectura_registrada: ${problema}`, { validaciones: [{ regla: "lectura_registrada", ok: false, detalle: problema }], estado: caso.estado });
41295
+ }
41296
+ }
41297
+ if (caso.workflow === "cuenta_completa" && resultado.via === "observar") {
41298
+ datos.faltantes = leerFaltantes(datos.faltantes) ?? [];
41299
+ datos.faltantes_iniciales = datos.faltantes;
41300
+ }
41301
+ if (caso.workflow === "cuenta_completa" && resultado.via === "comprobar") {
41302
+ datos.faltantes = leerFaltantes(datos.faltantes_leidos) ?? [];
41303
+ delete datos.faltantes_leidos;
41304
+ }
41305
+ if (caso.workflow === "recupero_turno" && resultado.via === "no_acepta") {
41306
+ const previas = Array.isArray(caso.datos.declinaron) ? caso.datos.declinaron.map((v) => refExterna(v)).filter((v) => v !== null) : [];
41307
+ const esta = refExterna(caso.datos.solicitud_id);
41308
+ datos.declinaron = esta && !previas.includes(esta) ? [...previas, esta] : previas;
41309
+ }
41055
41310
  const excepcionesEvaluadas = resultado.validaciones.filter((v) => v.excepcion);
41056
41311
  if (excepcionesEvaluadas.length > 0) datos.excepciones = excepcionesEvaluadas.filter((v) => !v.ok).map((v) => v.regla);
41057
41312
  else if (resultado.via === "proponer") datos.excepciones = [];
@@ -41102,7 +41357,7 @@ async function transicionar(ctx, input2) {
41102
41357
  };
41103
41358
  }
41104
41359
  var TIPOS_ACCION = ["lectura", "escritura", "verificacion"];
41105
- var CONECTORES = ["api", "archivo", "pantalla", "manual"];
41360
+ var CONECTORES = ["api", "base", "archivo", "pantalla", "manual"];
41106
41361
  var RESULTADOS_ACCION = ["ok", "error", "pendiente"];
41107
41362
  async function registrarAccion(ctx, input2) {
41108
41363
  const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
@@ -41112,12 +41367,19 @@ async function registrarAccion(ctx, input2) {
41112
41367
  if (!RESULTADOS_ACCION.includes(input2.resultado)) throw new OpsError("resultado_invalido", `resultado debe ser: ${RESULTADOS_ACCION.join(", ")}`);
41113
41368
  const objetivo = str(input2.objetivo);
41114
41369
  if (!objetivo) throw new OpsError("objetivo_requerido", "objetivo es obligatorio (qu\xE9 pantalla, endpoint o archivo se toc\xF3)");
41370
+ if (caso.workflow === "cuenta_completa" && input2.tipo === "escritura" && input2.actor.tipo === "agente") {
41371
+ throw new OpsError("politica", "en cuenta_completa el puesto solo lee: no registra escrituras sobre el sistema del hospital; lo que falta lo completa el equipo");
41372
+ }
41115
41373
  if (input2.tipo === "escritura" && input2.actor.tipo === "agente") {
41116
41374
  const modo = await obtenerModo(ctx, input2.companyId, caso.workflow);
41117
41375
  if (!modoHabilita(modo, "ejecuta")) throw new OpsError("politica", `la pol\xEDtica vigente de ${caso.workflow} es "${modo}": un agente no puede registrar escrituras sobre el sistema del hospital`);
41118
41376
  }
41119
41377
  const id = randomUUID();
41120
- const evidencia = input2.evidencia ?? {};
41378
+ const evidencia = { ...input2.evidencia ?? {} };
41379
+ delete evidencia.fuera_de_flujo;
41380
+ if (caso.workflow === "recupero_turno" && input2.tipo === "escritura" && input2.actor.tipo === "agente" && caso.estado !== "asignando") {
41381
+ evidencia.fuera_de_flujo = true;
41382
+ }
41121
41383
  await ctx.db.execute(
41122
41384
  `INSERT INTO ${t(ctx, "accion")} (id, company_id, caso_id, tipo, conector, objetivo, resultado, evidencia, detalle, actor_tipo, actor_id, run_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11, $12)`,
41123
41385
  [id, input2.companyId, caso.id, input2.tipo, input2.conector, objetivo, input2.resultado, JSON.stringify(evidencia), input2.detalle ?? null, input2.actor.tipo, input2.actor.id, input2.actor.runId ?? null]
@@ -41177,8 +41439,8 @@ async function registrarCargaManual(ctx, input2) {
41177
41439
  if (!sinValor && valorCargado === null) {
41178
41440
  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 });
41179
41441
  }
41180
- const { hoy, hora } = ahoraEnHospital();
41181
- const previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)" }, modo: "ejecuta", contexto: { hoy, horaActual: hora } });
41442
+ const contextoPrevio = await contextoDe(ctx, input2.companyId, caso, def2.via, {});
41443
+ const previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)" }, modo: "ejecuta", contexto: contextoPrevio });
41182
41444
  if (!previa.ok) throw new OpsError(previa.codigo, previa.motivo, { validaciones: previa.validaciones, estado: caso.estado });
41183
41445
  const persona = str(input2.persona);
41184
41446
  const comentario = str(input2.comentario);
@@ -41194,6 +41456,7 @@ async function registrarCargaManual(ctx, input2) {
41194
41456
  ...valorCargado !== null ? { valor_cargado: valorCargado } : {},
41195
41457
  ...caso.datos.valor_nuevo !== void 0 ? { valor_aprobado: caso.datos.valor_nuevo } : {},
41196
41458
  ...caso.datos.vigencia_desde !== void 0 ? { vigencia_desde: caso.datos.vigencia_desde } : {},
41459
+ ...caso.workflow === "cuenta_completa" ? { faltantes_que_completo: caso.datos.faltantes ?? [] } : {},
41197
41460
  ...comentario ? { nota: comentario } : {}
41198
41461
  },
41199
41462
  detalle: comentario,
@@ -41223,6 +41486,16 @@ async function registrarCargaManual(ctx, input2) {
41223
41486
  throw err;
41224
41487
  }
41225
41488
  }
41489
+ async function registrarRespuestaPaciente(ctx, input2) {
41490
+ if (input2.actor.tipo !== "humano") throw new OpsError("actor_no_permitido", "la respuesta del paciente la registra una persona del hospital; un agente usa ops_transicionar");
41491
+ const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
41492
+ if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
41493
+ if (caso.workflow !== "recupero_turno") throw new OpsError("estado_invalido", "solo las citas ofrecidas al paciente llevan respuesta");
41494
+ const persona = str(input2.persona);
41495
+ const comentario = str(input2.comentario);
41496
+ const datos = input2.acepto ? { respuesta: comentario ?? "Acept\xF3 la cita", ...persona ? { registrado_por: persona } : {} } : { motivo: comentario ?? "No acept\xF3 la cita", ...persona ? { registrado_por: persona } : {} };
41497
+ return transicionar(ctx, { companyId: input2.companyId, casoId: caso.id, via: input2.acepto ? "aceptar" : "no_acepta", datos, motivo: comentario, idempotencyKey: input2.idempotencyKey, actor: input2.actor });
41498
+ }
41226
41499
  function finalesOk() {
41227
41500
  return [...new Set(listarWorkflows().flatMap((w) => w.finalesOk))];
41228
41501
  }
@@ -41647,11 +41920,14 @@ function registrarHerramientas(ctx) {
41647
41920
  const detalle = await leerCasoCompleto(ctx, runCtx.companyId, casoId);
41648
41921
  if (!detalle) throw new OpsError("caso_inexistente", `no existe el caso ${casoId} en esta instituci\xF3n`);
41649
41922
  const disponibles = detalle.disponibles.filter((d) => d.actor.includes("agente")).map((d) => d.via);
41923
+ const opciones = await obtenerOpciones(ctx, runCtx.companyId, detalle.caso.workflow);
41924
+ const consulta = detalle.caso.workflow === "recupero_turno" ? `
41925
+ Consulta previa al paciente: ${opciones.ofrecer_primero === true ? "S\xCD (ofrecer y esperar que acepte antes de autorizar)" : "no"}` : "";
41650
41926
  return {
41651
41927
  content: `${resumenCaso(detalle.caso)}
41652
41928
  Transiciones disponibles para agente: ${disponibles.join(", ") || "ninguna"}
41653
- Transiciones registradas: ${detalle.transiciones.length}; acciones registradas: ${detalle.acciones.length}`,
41654
- data: detalle
41929
+ Transiciones registradas: ${detalle.transiciones.length}; acciones registradas: ${detalle.acciones.length}${consulta}`,
41930
+ data: { ...detalle, opciones }
41655
41931
  };
41656
41932
  }));
41657
41933
  ctx.tools.register(TOOL_NAMES.abrirCaso, toolSchema(TOOL_NAMES.abrirCaso), async (params, runCtx) => conErrores(async () => {
@@ -41836,8 +42112,15 @@ async function manejarApi(ctx, input2) {
41836
42112
  return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "rechazar", motivo: str2(body.motivo), datos: conPersona(obj(body.datos)), actor }));
41837
42113
  case "marcar-cargado":
41838
42114
  return respuesta(200, await registrarCargaManual(ctx, { companyId, casoId: input2.params.casoId ?? "", valorCargado: body.valorCargado, comentario: str2(body.comentario), persona: decididoPor, idempotencyKey: str2(body.idempotencyKey), actor }));
42115
+ case "respuesta-paciente":
42116
+ return respuesta(200, await registrarRespuestaPaciente(ctx, { companyId, casoId: input2.params.casoId ?? "", acepto: body.acepto === true, comentario: str2(body.comentario), persona: decididoPor, idempotencyKey: str2(body.idempotencyKey), actor }));
42117
+ case "no-corresponde": {
42118
+ if (actor.tipo !== "humano") return respuesta(403, { error: "actor_no_permitido", motivo: "no_corresponde lo decide una persona del hospital" });
42119
+ if (!str2(body.motivo)) return respuesta(422, { error: "validacion", motivo: "motivo_presente: no_corresponde requiere motivo" });
42120
+ return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "no_corresponde", motivo: motivoConPersona(str2(body.motivo)), datos: conPersona(obj(body.datos)), idempotencyKey: str2(body.idempotencyKey), actor }));
42121
+ }
41839
42122
  case "version":
41840
- return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos"], workflows: listarWorkflows().map((w) => w.clave) });
42123
+ return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos", "ofrecer-primero", "cuenta-completa"], workflows: listarWorkflows().map((w) => w.clave) });
41841
42124
  case "politicas":
41842
42125
  return respuesta(200, { politicas: await listarPoliticas(ctx, companyId) });
41843
42126
  case "serie":
@@ -41854,7 +42137,8 @@ async function manejarApi(ctx, input2) {
41854
42137
  const workflow = str2(body.workflow);
41855
42138
  if (!workflow) return respuesta(400, { error: "workflow_requerido" });
41856
42139
  if (!esModo(body.modo)) return respuesta(400, { error: "modo_invalido", detalle: "modo debe ser corte, observa, propone, ejecuta o autonomo" });
41857
- return respuesta(200, await cambiarPolitica(ctx, { companyId, workflow, modo: body.modo, motivo: motivoConPersona(str2(body.motivo)), actor }));
42140
+ const opciones = body.opciones && typeof body.opciones === "object" && !Array.isArray(body.opciones) ? body.opciones : null;
42141
+ return respuesta(200, await cambiarPolitica(ctx, { companyId, workflow, modo: body.modo, motivo: motivoConPersona(str2(body.motivo)), opciones, actor }));
41858
42142
  }
41859
42143
  default:
41860
42144
  return respuesta(404, { error: "ruta_desconocida", routeKey: input2.routeKey });
@@ -0,0 +1,6 @@
1
+ -- Opciones de cada función por institución, además del nivel de autonomía.
2
+ -- recupero_turno: {"ofrecer_primero": true} → el paciente tiene que aceptar la cita
3
+ -- antes de que se asigne (el plugin no autoriza la escritura hasta que acepta).
4
+
5
+ ALTER TABLE plugin_autopilot_ops_1a68482ede.politica
6
+ ADD COLUMN opciones jsonb NOT NULL DEFAULT '{}'::jsonb;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-autopilot-ops",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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": [