paperclip-plugin-autopilot-ops 0.1.29 → 0.1.31

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.29";
3
+ var PLUGIN_VERSION = "0.1.31";
4
4
  var DB_NAMESPACE_SLUG = "autopilot_ops";
5
5
  var PAGE_ROUTE = "autopilot";
6
6
  var TOOL_NAMES = {
@@ -9,6 +9,10 @@ var TOOL_NAMES = {
9
9
  abrirCaso: "ops_abrir_caso",
10
10
  transicionar: "ops_transicionar",
11
11
  registrarAccion: "ops_registrar_accion",
12
+ reservarEscritura: "ops_reservar_escritura",
13
+ confirmarEscritura: "ops_confirmar_escritura",
14
+ cerrarEscritura: "ops_cerrar_escritura",
15
+ verificarEscritura: "ops_verificar_escritura",
12
16
  consultarIndicador: "ops_consultar_indicador",
13
17
  publicarTablero: "ops_publicar_tablero",
14
18
  listarTableros: "ops_listar_tableros",
@@ -161,6 +165,58 @@ var manifest = {
161
165
  required: ["casoId", "tipo", "conector", "objetivo", "resultado"]
162
166
  }
163
167
  },
168
+ {
169
+ name: TOOL_NAMES.reservarEscritura,
170
+ displayName: "Reservar una escritura en el HIS",
171
+ description: "Paso 1 de toda escritura en el HIS: antes de tocarlo, dec\xED qu\xE9 vas a escribir (clave de negocio \xFAnica, p. ej. turno:2026-10-20T10:00|prof:15|sol:8812), sobre qu\xE9 registro (p. ej. solicitud:8812) y qu\xE9 le\xEDste de ese registro reci\xE9n (leido). El plugin la rechaza si el freno est\xE1 puesto, fuera de horario, si se pas\xF3 el tope del d\xEDa, si esa escritura ya se hizo o qued\xF3 dudosa, o si otro trabajo escribe el mismo registro. Devuelve el id de la reserva (vence en 15 minutos).",
172
+ parametersSchema: {
173
+ type: "object",
174
+ properties: {
175
+ casoId: { type: "string" },
176
+ clave: { type: "string", description: "Identifica la escritura de negocio: la misma clave no se escribe dos veces." },
177
+ registro: { type: "string", description: "El registro del HIS que se toca (tipo:id)." },
178
+ conector: { type: "string", enum: ["api", "pantalla", "archivo"] },
179
+ objetivo: { type: "string", description: "Qu\xE9 pantalla, endpoint o archivo." },
180
+ leido: { type: "object", description: "Lo que le\xEDste del registro ahora (campos y valores); se compara justo antes de guardar." }
181
+ },
182
+ required: ["casoId", "clave", "registro", "conector", "objetivo", "leido"]
183
+ }
184
+ },
185
+ {
186
+ name: TOOL_NAMES.confirmarEscritura,
187
+ displayName: "Confirmar antes de guardar",
188
+ description: "Paso 2 (solo si la funci\xF3n lo exige, sobre sistemas que no validan por s\xED mismos): justo antes de guardar, volv\xE9 a leer el registro y mand\xE1 lo le\xEDdo con los mismos campos. Si cambi\xF3 desde la reserva (lo toc\xF3 una persona), la escritura se descarta: no guardes y volv\xE9 a proponer.",
189
+ parametersSchema: {
190
+ type: "object",
191
+ properties: { accionId: { type: "string" }, leido: { type: "object" } },
192
+ required: ["accionId", "leido"]
193
+ }
194
+ },
195
+ {
196
+ name: TOOL_NAMES.cerrarEscritura,
197
+ displayName: "Cerrar una escritura",
198
+ description: "Paso 3: c\xF3mo sali\xF3 la escritura. ok (qued\xF3 escrita; si la funci\xF3n exige confirmar, solo despu\xE9s de confirmarla), error (no se escribi\xF3 nada) o incierta (no sab\xE9s si qued\xF3 escrita: no reintentes; verificala con ops_verificar_escritura). Con 3 fallas seguidas la funci\xF3n se frena sola.",
199
+ parametersSchema: {
200
+ type: "object",
201
+ properties: {
202
+ accionId: { type: "string" },
203
+ resultado: { type: "string", enum: ["ok", "error", "incierta"] },
204
+ evidencia: { type: "object" },
205
+ detalle: { type: "string" }
206
+ },
207
+ required: ["accionId", "resultado"]
208
+ }
209
+ },
210
+ {
211
+ name: TOOL_NAMES.verificarEscritura,
212
+ displayName: "Verificar una escritura dudosa",
213
+ description: "Para una escritura que qued\xF3 incierta: rele\xE9 el HIS y dec\xED si qued\xF3 escrita (quedo true: se da por hecha y segu\xEDs), si no qued\xF3 (quedo false: se puede reintentar) o si no lo pod\xE9s saber (quedo null: despu\xE9s de 2 intentos o una hora pasa a una persona). Mand\xE1 en leido lo que rele\xEDste.",
214
+ parametersSchema: {
215
+ type: "object",
216
+ properties: { accionId: { type: "string" }, quedo: { type: ["boolean", "null"] }, leido: { type: "object" }, detalle: { type: "string" } },
217
+ required: ["accionId", "quedo", "leido"]
218
+ }
219
+ },
164
220
  {
165
221
  name: TOOL_NAMES.consultarIndicador,
166
222
  displayName: "Consultar indicador",
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.29";
11
+ var PLUGIN_VERSION = "0.1.31";
12
12
  var DB_NAMESPACE_SLUG = "autopilot_ops";
13
13
  var PAGE_ROUTE = "autopilot";
14
14
  var TOOL_NAMES = {
@@ -17,6 +17,10 @@ var TOOL_NAMES = {
17
17
  abrirCaso: "ops_abrir_caso",
18
18
  transicionar: "ops_transicionar",
19
19
  registrarAccion: "ops_registrar_accion",
20
+ reservarEscritura: "ops_reservar_escritura",
21
+ confirmarEscritura: "ops_confirmar_escritura",
22
+ cerrarEscritura: "ops_cerrar_escritura",
23
+ verificarEscritura: "ops_verificar_escritura",
20
24
  consultarIndicador: "ops_consultar_indicador",
21
25
  publicarTablero: "ops_publicar_tablero",
22
26
  listarTableros: "ops_listar_tableros",
@@ -169,6 +173,58 @@ var manifest = {
169
173
  required: ["casoId", "tipo", "conector", "objetivo", "resultado"]
170
174
  }
171
175
  },
176
+ {
177
+ name: TOOL_NAMES.reservarEscritura,
178
+ displayName: "Reservar una escritura en el HIS",
179
+ description: "Paso 1 de toda escritura en el HIS: antes de tocarlo, dec\xED qu\xE9 vas a escribir (clave de negocio \xFAnica, p. ej. turno:2026-10-20T10:00|prof:15|sol:8812), sobre qu\xE9 registro (p. ej. solicitud:8812) y qu\xE9 le\xEDste de ese registro reci\xE9n (leido). El plugin la rechaza si el freno est\xE1 puesto, fuera de horario, si se pas\xF3 el tope del d\xEDa, si esa escritura ya se hizo o qued\xF3 dudosa, o si otro trabajo escribe el mismo registro. Devuelve el id de la reserva (vence en 15 minutos).",
180
+ parametersSchema: {
181
+ type: "object",
182
+ properties: {
183
+ casoId: { type: "string" },
184
+ clave: { type: "string", description: "Identifica la escritura de negocio: la misma clave no se escribe dos veces." },
185
+ registro: { type: "string", description: "El registro del HIS que se toca (tipo:id)." },
186
+ conector: { type: "string", enum: ["api", "pantalla", "archivo"] },
187
+ objetivo: { type: "string", description: "Qu\xE9 pantalla, endpoint o archivo." },
188
+ leido: { type: "object", description: "Lo que le\xEDste del registro ahora (campos y valores); se compara justo antes de guardar." }
189
+ },
190
+ required: ["casoId", "clave", "registro", "conector", "objetivo", "leido"]
191
+ }
192
+ },
193
+ {
194
+ name: TOOL_NAMES.confirmarEscritura,
195
+ displayName: "Confirmar antes de guardar",
196
+ description: "Paso 2 (solo si la funci\xF3n lo exige, sobre sistemas que no validan por s\xED mismos): justo antes de guardar, volv\xE9 a leer el registro y mand\xE1 lo le\xEDdo con los mismos campos. Si cambi\xF3 desde la reserva (lo toc\xF3 una persona), la escritura se descarta: no guardes y volv\xE9 a proponer.",
197
+ parametersSchema: {
198
+ type: "object",
199
+ properties: { accionId: { type: "string" }, leido: { type: "object" } },
200
+ required: ["accionId", "leido"]
201
+ }
202
+ },
203
+ {
204
+ name: TOOL_NAMES.cerrarEscritura,
205
+ displayName: "Cerrar una escritura",
206
+ description: "Paso 3: c\xF3mo sali\xF3 la escritura. ok (qued\xF3 escrita; si la funci\xF3n exige confirmar, solo despu\xE9s de confirmarla), error (no se escribi\xF3 nada) o incierta (no sab\xE9s si qued\xF3 escrita: no reintentes; verificala con ops_verificar_escritura). Con 3 fallas seguidas la funci\xF3n se frena sola.",
207
+ parametersSchema: {
208
+ type: "object",
209
+ properties: {
210
+ accionId: { type: "string" },
211
+ resultado: { type: "string", enum: ["ok", "error", "incierta"] },
212
+ evidencia: { type: "object" },
213
+ detalle: { type: "string" }
214
+ },
215
+ required: ["accionId", "resultado"]
216
+ }
217
+ },
218
+ {
219
+ name: TOOL_NAMES.verificarEscritura,
220
+ displayName: "Verificar una escritura dudosa",
221
+ description: "Para una escritura que qued\xF3 incierta: rele\xE9 el HIS y dec\xED si qued\xF3 escrita (quedo true: se da por hecha y segu\xEDs), si no qued\xF3 (quedo false: se puede reintentar) o si no lo pod\xE9s saber (quedo null: despu\xE9s de 2 intentos o una hora pasa a una persona). Mand\xE1 en leido lo que rele\xEDste.",
222
+ parametersSchema: {
223
+ type: "object",
224
+ properties: { accionId: { type: "string" }, quedo: { type: ["boolean", "null"] }, leido: { type: "object" }, detalle: { type: "string" } },
225
+ required: ["accionId", "quedo", "leido"]
226
+ }
227
+ },
172
228
  {
173
229
  name: TOOL_NAMES.consultarIndicador,
174
230
  displayName: "Consultar indicador",
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.29";
39935
+ var PLUGIN_VERSION = "0.1.31";
39936
39936
  var DB_NAMESPACE_SLUG = "autopilot_ops";
39937
39937
  var PAGE_ROUTE = "autopilot";
39938
39938
  var TOOL_NAMES = {
@@ -39941,6 +39941,10 @@ var TOOL_NAMES = {
39941
39941
  abrirCaso: "ops_abrir_caso",
39942
39942
  transicionar: "ops_transicionar",
39943
39943
  registrarAccion: "ops_registrar_accion",
39944
+ reservarEscritura: "ops_reservar_escritura",
39945
+ confirmarEscritura: "ops_confirmar_escritura",
39946
+ cerrarEscritura: "ops_cerrar_escritura",
39947
+ verificarEscritura: "ops_verificar_escritura",
39944
39948
  consultarIndicador: "ops_consultar_indicador",
39945
39949
  publicarTablero: "ops_publicar_tablero",
39946
39950
  listarTableros: "ops_listar_tableros",
@@ -40093,6 +40097,58 @@ var manifest = {
40093
40097
  required: ["casoId", "tipo", "conector", "objetivo", "resultado"]
40094
40098
  }
40095
40099
  },
40100
+ {
40101
+ name: TOOL_NAMES.reservarEscritura,
40102
+ displayName: "Reservar una escritura en el HIS",
40103
+ description: "Paso 1 de toda escritura en el HIS: antes de tocarlo, dec\xED qu\xE9 vas a escribir (clave de negocio \xFAnica, p. ej. turno:2026-10-20T10:00|prof:15|sol:8812), sobre qu\xE9 registro (p. ej. solicitud:8812) y qu\xE9 le\xEDste de ese registro reci\xE9n (leido). El plugin la rechaza si el freno est\xE1 puesto, fuera de horario, si se pas\xF3 el tope del d\xEDa, si esa escritura ya se hizo o qued\xF3 dudosa, o si otro trabajo escribe el mismo registro. Devuelve el id de la reserva (vence en 15 minutos).",
40104
+ parametersSchema: {
40105
+ type: "object",
40106
+ properties: {
40107
+ casoId: { type: "string" },
40108
+ clave: { type: "string", description: "Identifica la escritura de negocio: la misma clave no se escribe dos veces." },
40109
+ registro: { type: "string", description: "El registro del HIS que se toca (tipo:id)." },
40110
+ conector: { type: "string", enum: ["api", "pantalla", "archivo"] },
40111
+ objetivo: { type: "string", description: "Qu\xE9 pantalla, endpoint o archivo." },
40112
+ leido: { type: "object", description: "Lo que le\xEDste del registro ahora (campos y valores); se compara justo antes de guardar." }
40113
+ },
40114
+ required: ["casoId", "clave", "registro", "conector", "objetivo", "leido"]
40115
+ }
40116
+ },
40117
+ {
40118
+ name: TOOL_NAMES.confirmarEscritura,
40119
+ displayName: "Confirmar antes de guardar",
40120
+ description: "Paso 2 (solo si la funci\xF3n lo exige, sobre sistemas que no validan por s\xED mismos): justo antes de guardar, volv\xE9 a leer el registro y mand\xE1 lo le\xEDdo con los mismos campos. Si cambi\xF3 desde la reserva (lo toc\xF3 una persona), la escritura se descarta: no guardes y volv\xE9 a proponer.",
40121
+ parametersSchema: {
40122
+ type: "object",
40123
+ properties: { accionId: { type: "string" }, leido: { type: "object" } },
40124
+ required: ["accionId", "leido"]
40125
+ }
40126
+ },
40127
+ {
40128
+ name: TOOL_NAMES.cerrarEscritura,
40129
+ displayName: "Cerrar una escritura",
40130
+ description: "Paso 3: c\xF3mo sali\xF3 la escritura. ok (qued\xF3 escrita; si la funci\xF3n exige confirmar, solo despu\xE9s de confirmarla), error (no se escribi\xF3 nada) o incierta (no sab\xE9s si qued\xF3 escrita: no reintentes; verificala con ops_verificar_escritura). Con 3 fallas seguidas la funci\xF3n se frena sola.",
40131
+ parametersSchema: {
40132
+ type: "object",
40133
+ properties: {
40134
+ accionId: { type: "string" },
40135
+ resultado: { type: "string", enum: ["ok", "error", "incierta"] },
40136
+ evidencia: { type: "object" },
40137
+ detalle: { type: "string" }
40138
+ },
40139
+ required: ["accionId", "resultado"]
40140
+ }
40141
+ },
40142
+ {
40143
+ name: TOOL_NAMES.verificarEscritura,
40144
+ displayName: "Verificar una escritura dudosa",
40145
+ description: "Para una escritura que qued\xF3 incierta: rele\xE9 el HIS y dec\xED si qued\xF3 escrita (quedo true: se da por hecha y segu\xEDs), si no qued\xF3 (quedo false: se puede reintentar) o si no lo pod\xE9s saber (quedo null: despu\xE9s de 2 intentos o una hora pasa a una persona). Mand\xE1 en leido lo que rele\xEDste.",
40146
+ parametersSchema: {
40147
+ type: "object",
40148
+ properties: { accionId: { type: "string" }, quedo: { type: ["boolean", "null"] }, leido: { type: "object" }, detalle: { type: "string" } },
40149
+ required: ["accionId", "quedo", "leido"]
40150
+ }
40151
+ },
40096
40152
  {
40097
40153
  name: TOOL_NAMES.consultarIndicador,
40098
40154
  displayName: "Consultar indicador",
@@ -40391,6 +40447,60 @@ var manifest_default = manifest;
40391
40447
  // src/ops.ts
40392
40448
  import { randomUUID as randomUUID2 } from "node:crypto";
40393
40449
 
40450
+ // src/escrituras.ts
40451
+ var MINUTOS_RESERVA = 15;
40452
+ var FALLAS_PARA_FRENAR = 3;
40453
+ var INTENTOS_VERIFICACION = 2;
40454
+ var MINUTOS_DUDOSA_PARA_PERSONA = 60;
40455
+ function dudosaParaPersona(intentosSinSaber, minutosDudosa) {
40456
+ return intentosSinSaber >= INTENTOS_VERIFICACION || minutosDudosa >= MINUTOS_DUDOSA_PARA_PERSONA;
40457
+ }
40458
+ var RESULTADOS_CIERRE = ["ok", "error", "incierta"];
40459
+ var HORA = /^([01]\d|2[0-3]):[0-5]\d$/;
40460
+ function leerVentana(v) {
40461
+ if (!v || typeof v !== "object" || Array.isArray(v)) return null;
40462
+ const x = v;
40463
+ const desde = String(x.desde ?? "").trim();
40464
+ const hasta = String(x.hasta ?? "").trim();
40465
+ if (!HORA.test(desde) || !HORA.test(hasta) || desde >= hasta) return null;
40466
+ const dias = Array.isArray(x.dias) ? [...new Set(x.dias.map(Number).filter((d) => Number.isInteger(d) && d >= 1 && d <= 7))].sort() : null;
40467
+ return { desde, hasta, dias: dias && dias.length ? dias : null };
40468
+ }
40469
+ function leerTopeDiario(v) {
40470
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
40471
+ return Number.isInteger(n) && n >= 1 && n <= 1e4 ? n : null;
40472
+ }
40473
+ function evaluarReserva(h) {
40474
+ if (h.freno) return { ok: false, codigo: "freno", motivo: `el freno de la instituci\xF3n est\xE1 puesto (${h.freno}): ning\xFAn puesto escribe en el HIS` };
40475
+ if (h.ventana) {
40476
+ if (h.ventana.dias && !h.ventana.dias.includes(h.diaSemana)) return { ok: false, codigo: "fuera_de_horario", motivo: "hoy no es un d\xEDa en que la funci\xF3n pueda escribir en el HIS" };
40477
+ if (h.hora < h.ventana.desde || h.hora >= h.ventana.hasta) return { ok: false, codigo: "fuera_de_horario", motivo: `la funci\xF3n escribe en el HIS de ${h.ventana.desde} a ${h.ventana.hasta}` };
40478
+ }
40479
+ if (h.tope !== null && h.escriturasHoy >= h.tope) return { ok: false, codigo: "tope_diario", motivo: `se lleg\xF3 al tope de ${h.tope} escrituras por d\xEDa de la funci\xF3n` };
40480
+ const m = h.mismaClave;
40481
+ if (m) {
40482
+ if (m.resultado === "ok") return { ok: false, codigo: "ya_escrita", motivo: "esa escritura ya se hizo: no se repite", accionId: m.id };
40483
+ if (m.resultado === "pendiente" && !m.vencida) return { ok: false, codigo: "en_curso", motivo: "esa escritura ya est\xE1 en curso", accionId: m.id };
40484
+ if (m.resultado === "incierta" || m.resultado === "pendiente" && m.vencida) {
40485
+ return { ok: false, codigo: "incierta", motivo: "una escritura anterior con esa clave qued\xF3 dudosa: verificala releyendo el HIS (ops_verificar_escritura) antes de reintentar", accionId: m.id };
40486
+ }
40487
+ }
40488
+ if (h.ocupado) return { ok: false, codigo: "registro_ocupado", motivo: "otro trabajo est\xE1 escribiendo ese mismo registro; esperar a que termine", accionId: h.ocupado.id };
40489
+ return { ok: true };
40490
+ }
40491
+ var normal = (v) => v === null || v === void 0 ? "" : typeof v === "object" ? JSON.stringify(v) : String(v).trim().toLowerCase();
40492
+ function compararLecturas(antes, ahora) {
40493
+ return Object.keys(antes).filter((k) => normal(antes[k]) !== normal(ahora[k])).sort();
40494
+ }
40495
+ function debeFrenar(resultados, fallas = FALLAS_PARA_FRENAR) {
40496
+ if (resultados.length < fallas) return false;
40497
+ return resultados.slice(0, fallas).every((r) => r === "error" || r === "incierta");
40498
+ }
40499
+ function diaDeLaSemana(fecha5) {
40500
+ const d = (/* @__PURE__ */ new Date(`${fecha5}T12:00:00Z`)).getUTCDay();
40501
+ return d === 0 ? 7 : d;
40502
+ }
40503
+
40394
40504
  // src/demanda.ts
40395
40505
  var txt = (v) => typeof v === "string" && v.trim() ? v.trim() : typeof v === "number" ? String(v) : null;
40396
40506
  var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v.trim()) ? Number(v) : null;
@@ -41053,18 +41163,18 @@ function leerRegla(v) {
41053
41163
  return { id: rid, prof: prof === null ? null : redondear2(prof), inst: inst === null ? null : redondear2(inst) };
41054
41164
  }
41055
41165
  function honorarioDe(input2) {
41056
- const { regla, monto, cantidad } = input2;
41166
+ const { regla, monto, cantidad: cantidad2 } = input2;
41057
41167
  if (!regla) return 0;
41058
- if (regla.prof !== null) return redondear2(regla.prof * cantidad);
41059
- return redondear2(monto * cantidad * ((regla.inst ?? 0) / 100));
41168
+ if (regla.prof !== null) return redondear2(regla.prof * cantidad2);
41169
+ return redondear2(monto * cantidad2 * ((regla.inst ?? 0) / 100));
41060
41170
  }
41061
41171
  function motivoCero(regla, honorario, delHis = false) {
41062
41172
  if (honorario > 0) return null;
41063
41173
  if (delHis) return "segun_his";
41064
41174
  return regla ? "regla_sin_valor" : "sin_regla";
41065
41175
  }
41066
- function honorarioLeido(r, regla, monto, cantidad) {
41067
- if (r.honorario === void 0 || r.honorario === null || r.honorario === "") return { honorario: honorarioDe({ regla, monto, cantidad }), delHis: false };
41176
+ function honorarioLeido(r, regla, monto, cantidad2) {
41177
+ if (r.honorario === void 0 || r.honorario === null || r.honorario === "") return { honorario: honorarioDe({ regla, monto, cantidad: cantidad2 }), delHis: false };
41068
41178
  const h = num3(r.honorario);
41069
41179
  return h === null || h < 0 ? null : { honorario: redondear2(h), delHis: true };
41070
41180
  }
@@ -41078,12 +41188,12 @@ function leerPrestaciones(v) {
41078
41188
  const bono = id2(r.bono_id);
41079
41189
  const f = dia(r.fecha);
41080
41190
  const monto = num3(r.monto);
41081
- const cantidad = num3(r.cantidad) ?? 1;
41191
+ const cantidad2 = num3(r.cantidad) ?? 1;
41082
41192
  if (!item || !bono || !f || monto === null) return { error: `prestaciones[${i}]: faltan item_id, bono_id, fecha o monto` };
41083
41193
  if (r.regla !== null && r.regla !== void 0 && !leerRegla(r.regla)) return { error: `prestaciones[${i}].regla: tiene que ser {id, prof, inst} o null` };
41084
- const h = honorarioLeido(r, leerRegla(r.regla), monto, cantidad > 0 ? cantidad : 1);
41194
+ const h = honorarioLeido(r, leerRegla(r.regla), monto, cantidad2 > 0 ? cantidad2 : 1);
41085
41195
  if (!h) return { error: `prestaciones[${i}].honorario: tiene que ser un n\xFAmero mayor o igual a 0` };
41086
- out.push({ item_id: item, bono_id: bono, fecha: f, prestacion: texto2(r.prestacion)?.slice(0, 160) ?? "Prestaci\xF3n", obra_social: texto2(r.obra_social)?.slice(0, 120) ?? null, monto: redondear2(monto), cantidad: cantidad > 0 ? cantidad : 1, a_liquidar: verdad2(r.a_liquidar), liquidada: verdad2(r.liquidada), regla: leerRegla(r.regla), honorario_his: h.delHis ? h.honorario : null });
41196
+ out.push({ item_id: item, bono_id: bono, fecha: f, prestacion: texto2(r.prestacion)?.slice(0, 160) ?? "Prestaci\xF3n", obra_social: texto2(r.obra_social)?.slice(0, 120) ?? null, monto: redondear2(monto), cantidad: cantidad2 > 0 ? cantidad2 : 1, a_liquidar: verdad2(r.a_liquidar), liquidada: verdad2(r.liquidada), regla: leerRegla(r.regla), honorario_his: h.delHis ? h.honorario : null });
41087
41197
  }
41088
41198
  return { prestaciones: out };
41089
41199
  }
@@ -41122,13 +41232,13 @@ function leerDebitos(v) {
41122
41232
  const ncId = nc ? id2(nc.id) : null;
41123
41233
  if (!item || !bono || monto === null || !ncId) return { error: `debitos[${i}]: faltan item_id, bono_id, monto_debitado o nota_credito.id` };
41124
41234
  const montoItem = num3(r.monto) ?? 0;
41125
- const cantidad = num3(r.cantidad) ?? 1;
41235
+ const cantidad2 = num3(r.cantidad) ?? 1;
41126
41236
  const regla = leerRegla(r.regla);
41127
41237
  const liquidada = verdad2(r.liquidada);
41128
- const hl = honorarioLeido(r, regla, montoItem, cantidad);
41238
+ const hl = honorarioLeido(r, regla, montoItem, cantidad2);
41129
41239
  if (!hl) return { error: `debitos[${i}].honorario: tiene que ser un n\xFAmero mayor o igual a 0` };
41130
41240
  const hon = liquidada ? hl.honorario : null;
41131
- const imp = liquidada ? redondear2(montoItem * cantidad) : null;
41241
+ const imp = liquidada ? redondear2(montoItem * cantidad2) : null;
41132
41242
  out.push({
41133
41243
  item_id: item,
41134
41244
  bono_id: bono,
@@ -41139,7 +41249,7 @@ function leerDebitos(v) {
41139
41249
  monto_debitado: redondear2(Math.abs(monto)),
41140
41250
  liquidada,
41141
41251
  monto: redondear2(montoItem),
41142
- cantidad: cantidad > 0 ? cantidad : 1,
41252
+ cantidad: cantidad2 > 0 ? cantidad2 : 1,
41143
41253
  regla,
41144
41254
  liquidado: hon !== null && imp !== null ? { honorario: hon, importe: imp } : null,
41145
41255
  ya_trasladado: redondear2(Math.abs(num3(r.ya_trasladado) ?? 0))
@@ -41172,12 +41282,12 @@ function leerLineasLiquidacion(v) {
41172
41282
  for (const it of r.items) {
41173
41283
  const x = it && typeof it === "object" ? it : {};
41174
41284
  const monto = num3(x.monto) ?? 0;
41175
- const cantidad = num3(x.cantidad) ?? 1;
41285
+ const cantidad2 = num3(x.cantidad) ?? 1;
41176
41286
  const regla = leerRegla(x.regla);
41177
- const hl = honorarioLeido(x, regla, monto, cantidad);
41287
+ const hl = honorarioLeido(x, regla, monto, cantidad2);
41178
41288
  if (!hl) return { error: `lineas[${i}].items: honorario tiene que ser un n\xFAmero mayor o igual a 0` };
41179
41289
  const h = hl.honorario;
41180
- importe += monto * cantidad;
41290
+ importe += monto * cantidad2;
41181
41291
  honorario += h;
41182
41292
  if (h <= 0) enCero += 1;
41183
41293
  }
@@ -41382,6 +41492,7 @@ var RESPUESTAS_FINANCIADOR = ["paga", "observa", "no_responde"];
41382
41492
  // src/workflows.ts
41383
41493
  var MODOS = ["corte", "observa", "propone", "ejecuta", "autonomo"];
41384
41494
  var MODO_POR_DEFECTO = "propone";
41495
+ var cantidad = (v) => Array.isArray(v) ? v.length : 0;
41385
41496
  function num5(v) {
41386
41497
  if (typeof v === "number" && Number.isFinite(v)) return v;
41387
41498
  if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
@@ -41416,7 +41527,7 @@ var actualizacionArancel = {
41416
41527
  ],
41417
41528
  finalesOk: ["verificado"],
41418
41529
  finalesNoOk: ["rechazado", "cancelado"],
41419
- minutosAbsorbidos: 25,
41530
+ minutosAbsorbidos: 8,
41420
41531
  transiciones: [
41421
41532
  {
41422
41533
  via: "proponer",
@@ -41669,7 +41780,7 @@ var recuperoTurno = {
41669
41780
  ],
41670
41781
  finalesOk: ["verificado"],
41671
41782
  finalesNoOk: ["sin_candidato", "rechazado", "cancelado"],
41672
- minutosAbsorbidos: 15,
41783
+ minutosAbsorbidos: 12,
41673
41784
  transiciones: [
41674
41785
  {
41675
41786
  via: "proponer",
@@ -41910,7 +42021,7 @@ var cuentaCompleta = {
41910
42021
  estados: ["detectado", "para_completar", "completado", "con_faltantes", "completa", "facturado_incompleto", "no_corresponde", "descartado"],
41911
42022
  finalesOk: ["completa"],
41912
42023
  finalesNoOk: ["facturado_incompleto", "no_corresponde", "descartado"],
41913
- minutosAbsorbidos: 10,
42024
+ minutosAbsorbidos: 4,
41914
42025
  transiciones: [
41915
42026
  {
41916
42027
  via: "observar",
@@ -42010,7 +42121,7 @@ var facturacionPendiente = {
42010
42121
  estados: ["detectado", "por_presentar", "en_prefactura", "presentado", "facturada", "no_corresponde", "descartado"],
42011
42122
  finalesOk: ["facturada"],
42012
42123
  finalesNoOk: ["no_corresponde", "descartado"],
42013
- minutosAbsorbidos: 10,
42124
+ minutosAbsorbidos: 3,
42014
42125
  transiciones: [
42015
42126
  {
42016
42127
  via: "observar",
@@ -42119,7 +42230,7 @@ var credencialProfesional = {
42119
42230
  estados: ["detectado", "vigente", "observada", "corregida", "no_corresponde", "descartado"],
42120
42231
  finalesOk: ["vigente"],
42121
42232
  finalesNoOk: ["no_corresponde", "descartado"],
42122
- minutosAbsorbidos: 8,
42233
+ minutosAbsorbidos: 5,
42123
42234
  transiciones: [
42124
42235
  {
42125
42236
  via: "verificar",
@@ -42222,7 +42333,7 @@ var demandaPendiente = {
42222
42333
  ],
42223
42334
  finalesOk: ["turno_agendado", "realizado"],
42224
42335
  finalesNoOk: ["no_quiere", "cerrado_sin_turno", "no_corresponde", "descartado"],
42225
- minutosAbsorbidos: 12,
42336
+ minutosAbsorbidos: 6,
42226
42337
  transiciones: [
42227
42338
  {
42228
42339
  via: "contactar",
@@ -42332,7 +42443,9 @@ var circularAranceles = {
42332
42443
  estados: ["recibida", "extraida", "pendiente_aprobacion", "aprobada", "aplicada", "verificada", "con_diferencias", "rechazada", "cancelada"],
42333
42444
  finalesOk: ["verificada"],
42334
42445
  finalesNoOk: ["rechazada", "cancelada"],
42335
- minutosAbsorbidos: 180,
42446
+ minutosAbsorbidos: 30,
42447
+ // Base de lectura y armado, más el cruce y la comprobación de cada valor (tope: una jornada).
42448
+ minutosSegun: (caso) => Math.min(480, 30 + 3 * cantidad(caso.datos.items)),
42336
42449
  transiciones: [
42337
42450
  {
42338
42451
  via: "extraer",
@@ -42488,7 +42601,9 @@ var cobranzaFinanciador = {
42488
42601
  estados: ["detectado", "pendiente_aprobacion", "aprobado", "reclamado", "comprometido", "escalado", "cobrado", "cerrado", "rechazado", "descartado"],
42489
42602
  finalesOk: ["cobrado"],
42490
42603
  finalesNoOk: ["cerrado", "rechazado", "descartado"],
42491
- minutosAbsorbidos: 45,
42604
+ minutosAbsorbidos: 20,
42605
+ // Base del reclamo y su seguimiento, más el armado de cada factura reclamada.
42606
+ minutosSegun: (caso) => Math.min(180, 20 + 2 * cantidad(caso.datos.items)),
42492
42607
  transiciones: [
42493
42608
  {
42494
42609
  via: "proponer",
@@ -42660,7 +42775,7 @@ var internacionAlDia = {
42660
42775
  estados: ["detectado", "al_dia", "con_pendientes", "completado", "egresada", "descartado"],
42661
42776
  finalesOk: ["egresada"],
42662
42777
  finalesNoOk: ["descartado"],
42663
- minutosAbsorbidos: 15,
42778
+ minutosAbsorbidos: 6,
42664
42779
  transiciones: [
42665
42780
  {
42666
42781
  via: "observar",
@@ -42719,7 +42834,7 @@ var honorarioEnCero = {
42719
42834
  estados: ["detectado", "para_cargar", "completado", "resuelto", "no_corresponde", "descartado"],
42720
42835
  finalesOk: ["resuelto"],
42721
42836
  finalesNoOk: ["no_corresponde", "descartado"],
42722
- minutosAbsorbidos: 15,
42837
+ minutosAbsorbidos: 10,
42723
42838
  transiciones: [
42724
42839
  {
42725
42840
  via: "observar",
@@ -42889,7 +43004,7 @@ var liquidacionProfesional = {
42889
43004
  estados: ["detectado", "pendiente_aprobacion", "aprobado", "enviada", "rechazado", "descartado"],
42890
43005
  finalesOk: ["enviada"],
42891
43006
  finalesNoOk: ["rechazado", "descartado"],
42892
- minutosAbsorbidos: 10,
43007
+ minutosAbsorbidos: 15,
42893
43008
  transiciones: [
42894
43009
  {
42895
43010
  via: "preparar",
@@ -43021,7 +43136,7 @@ function evaluarTransicion(input2) {
43021
43136
  a,
43022
43137
  via,
43023
43138
  validaciones,
43024
- minutosAbsorbidos: esFinalOk ? wf.minutosAbsorbidos : 0,
43139
+ minutosAbsorbidos: esFinalOk ? wf.minutosSegun ? wf.minutosSegun(caso) : wf.minutosAbsorbidos : 0,
43025
43140
  esFinal: esFinalOk || wf.finalesNoOk.includes(a),
43026
43141
  autoaprobado
43027
43142
  };
@@ -44267,6 +44382,9 @@ async function registrarAccion(ctx, input2) {
44267
44382
  const modo = await obtenerModo(ctx, input2.companyId, caso.workflow);
44268
44383
  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 HIS`);
44269
44384
  }
44385
+ if (input2.tipo === "escritura" && input2.actor.tipo === "agente") {
44386
+ throw new OpsError("usar_reserva", "un puesto escribe en el HIS en tres pasos: ops_reservar_escritura, ops_confirmar_escritura (releyendo justo antes de guardar) y ops_cerrar_escritura");
44387
+ }
44270
44388
  const id3 = randomUUID2();
44271
44389
  const evidencia = { ...input2.evidencia ?? {} };
44272
44390
  delete evidencia.fuera_de_flujo;
@@ -44295,6 +44413,217 @@ async function registrarAccion(ctx, input2) {
44295
44413
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
44296
44414
  };
44297
44415
  }
44416
+ var FRENO_INSTITUCION = "*";
44417
+ async function leerFreno(ctx, companyId) {
44418
+ const rows = await ctx.db.query(
44419
+ `SELECT modo, motivo FROM ${t2(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
44420
+ [companyId, FRENO_INSTITUCION]
44421
+ );
44422
+ return rows[0] && String(rows[0].modo) === "corte" ? str2(rows[0].motivo) ?? "freno puesto" : null;
44423
+ }
44424
+ async function guardarModo(ctx, companyId, workflow, modo, motivo, actor, anterior) {
44425
+ const existe = await ctx.db.query(`SELECT id FROM ${t2(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`, [companyId, workflow]);
44426
+ if (existe.length > 0) {
44427
+ await ctx.db.execute(
44428
+ `UPDATE ${t2(ctx, "politica")} SET modo = $3, motivo = $4, actor_tipo = $5, actor_id = $6, updated_at = now() WHERE company_id = $1 AND workflow = $2`,
44429
+ [companyId, workflow, modo, motivo, actor.tipo, actor.id]
44430
+ );
44431
+ } else {
44432
+ await ctx.db.execute(
44433
+ `INSERT INTO ${t2(ctx, "politica")} (id, company_id, workflow, modo, motivo, actor_tipo, actor_id, opciones) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)`,
44434
+ [randomUUID2(), companyId, workflow, modo, motivo, actor.tipo, actor.id, "{}"]
44435
+ );
44436
+ }
44437
+ await ctx.db.execute(
44438
+ `INSERT INTO ${t2(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)`,
44439
+ [randomUUID2(), companyId, workflow, anterior, modo, motivo, actor.tipo, actor.id]
44440
+ );
44441
+ }
44442
+ async function frenarFuncion(ctx, companyId, workflow) {
44443
+ const anterior = await obtenerModo(ctx, companyId, workflow);
44444
+ if (anterior === "corte") return;
44445
+ const motivo = `Freno autom\xE1tico: ${FALLAS_PARA_FRENAR} escrituras seguidas en el HIS fallaron o quedaron sin confirmar. Revisar y volver a habilitar.`;
44446
+ await guardarModo(ctx, companyId, workflow, "corte", motivo, { tipo: "sistema", id: null }, anterior);
44447
+ await ctx.activity.log({ companyId, message: `Autopilot Care: ${workflow} frenada autom\xE1ticamente (${FALLAS_PARA_FRENAR} escrituras fallidas seguidas)`, metadata: { workflow, freno: "automatico" } });
44448
+ }
44449
+ async function marcarDudosa(ctx, companyId, accion, detalle) {
44450
+ await ctx.db.execute(
44451
+ `UPDATE ${t2(ctx, "accion")} SET resultado = 'incierta', detalle = $3, cerrada_at = now() WHERE company_id = $1 AND id = $2`,
44452
+ [companyId, String(accion.id), detalle]
44453
+ );
44454
+ await ctx.db.execute(
44455
+ `UPDATE ${t2(ctx, "caso")} SET datos = datos || $3::jsonb, updated_at = now() WHERE company_id = $1 AND id = $2`,
44456
+ [companyId, String(accion.caso_id), JSON.stringify({ escritura_incierta: { accion_id: String(accion.id), objetivo: str2(accion.objetivo), detalle, desde: (/* @__PURE__ */ new Date()).toISOString(), intentos_verificacion: 0, para_persona: false } })]
44457
+ );
44458
+ await ctx.activity.log({ companyId, message: `Autopilot Care: escritura dudosa en el HIS (${str2(accion.objetivo) ?? "sin objetivo"}): ${detalle}`, metadata: { accionId: String(accion.id), casoId: String(accion.caso_id) } });
44459
+ }
44460
+ async function verificarEscritura(ctx, input2) {
44461
+ const fila = await leerAccionEscritura(ctx, input2.companyId, input2.accionId);
44462
+ if (String(fila.resultado) !== "incierta") throw new OpsError("no_dudosa", `la escritura ${input2.accionId} no est\xE1 dudosa (${String(fila.resultado)})`);
44463
+ const leido = input2.leido && typeof input2.leido === "object" && !Array.isArray(input2.leido) ? input2.leido : {};
44464
+ if (Object.keys(leido).length === 0) throw new OpsError("validacion", "leido: lo que se reley\xF3 del HIS para verificar (al menos un campo)");
44465
+ const evidencia = jsonb2(fila.evidencia);
44466
+ const caso = await leerCaso(ctx, input2.companyId, String(fila.caso_id));
44467
+ const dudosa = caso?.datos.escritura_incierta && typeof caso.datos.escritura_incierta === "object" ? caso.datos.escritura_incierta : {};
44468
+ if (input2.quedo === true || input2.quedo === false) {
44469
+ const resultado = input2.quedo ? "ok" : "error";
44470
+ const nueva = { ...evidencia, verificada_despues: leido };
44471
+ await ctx.db.execute(
44472
+ `UPDATE ${t2(ctx, "accion")} SET resultado = $3, evidencia = $4::jsonb, detalle = $5, cerrada_at = now() WHERE company_id = $1 AND id = $2`,
44473
+ [input2.companyId, input2.accionId, resultado, JSON.stringify(nueva), str2(input2.detalle) ?? (input2.quedo ? "verificada releyendo el HIS: qued\xF3 escrita" : "verificada releyendo el HIS: no se escribi\xF3")]
44474
+ );
44475
+ await ctx.db.execute(
44476
+ `UPDATE ${t2(ctx, "caso")} SET datos = datos - 'escritura_incierta', updated_at = now() WHERE company_id = $1 AND id = $2`,
44477
+ [input2.companyId, String(fila.caso_id)]
44478
+ );
44479
+ return { accion: aAccion(fila, { resultado, evidencia: nueva }), paraPersona: false };
44480
+ }
44481
+ const intentos = Number(dudosa.intentos_verificacion ?? 0) + 1;
44482
+ const desde = Date.parse(String(dudosa.desde ?? "")) || Date.now();
44483
+ const paraPersona = dudosaParaPersona(intentos, (Date.now() - desde) / 6e4);
44484
+ await ctx.db.execute(
44485
+ `UPDATE ${t2(ctx, "caso")} SET datos = datos || $3::jsonb, updated_at = now() WHERE company_id = $1 AND id = $2`,
44486
+ [input2.companyId, String(fila.caso_id), JSON.stringify({ escritura_incierta: { ...dudosa, intentos_verificacion: intentos, ultimo_intento: (/* @__PURE__ */ new Date()).toISOString(), ultima_lectura: leido, para_persona: paraPersona } })]
44487
+ );
44488
+ if (paraPersona) await ctx.activity.log({ companyId: input2.companyId, message: `Autopilot Care: escritura dudosa que el puesto no pudo verificar, para una persona (${str2(fila.objetivo) ?? "sin objetivo"})`, metadata: { accionId: input2.accionId, casoId: String(fila.caso_id) } });
44489
+ return { accion: aAccion(fila), paraPersona };
44490
+ }
44491
+ async function leerAccionEscritura(ctx, companyId, accionId) {
44492
+ const rows = await ctx.db.query(
44493
+ `SELECT id, caso_id, tipo, conector, objetivo, resultado, evidencia, vence_en, confirmada_at, clave, registro, actor_tipo, actor_id, created_at FROM ${t2(ctx, "accion")} WHERE company_id = $1 AND id = $2 LIMIT 1`,
44494
+ [companyId, accionId]
44495
+ );
44496
+ const fila = rows[0];
44497
+ if (!fila || String(fila.tipo) !== "escritura" || !str2(fila.clave)) throw new OpsError("reserva_inexistente", `no existe la reserva de escritura ${accionId} en esta instituci\xF3n`);
44498
+ return fila;
44499
+ }
44500
+ function aAccion(fila, extra = {}) {
44501
+ return {
44502
+ id: String(fila.id),
44503
+ casoId: String(fila.caso_id),
44504
+ tipo: String(fila.tipo),
44505
+ conector: String(fila.conector),
44506
+ objetivo: String(fila.objetivo),
44507
+ resultado: String(fila.resultado),
44508
+ evidencia: jsonb2(fila.evidencia),
44509
+ detalle: fila.detalle ?? null,
44510
+ actorTipo: String(fila.actor_tipo),
44511
+ actorId: fila.actor_id ?? null,
44512
+ createdAt: iso2(fila.created_at),
44513
+ ...extra
44514
+ };
44515
+ }
44516
+ async function reservarEscritura(ctx, input2) {
44517
+ const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
44518
+ if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
44519
+ if (input2.actor.tipo !== "agente") throw new OpsError("actor_no_permitido", "las reservas de escritura son de los puestos; lo que carga una persona se registra como carga manual");
44520
+ if (WORKFLOWS_SOLO_LECTURA.has(caso.workflow)) throw new OpsError("politica", `en ${caso.workflow} el puesto solo lee: no escribe en el HIS; eso lo hace el equipo`);
44521
+ const modo = await obtenerModo(ctx, input2.companyId, caso.workflow);
44522
+ if (!modoHabilita(modo, "ejecuta")) throw new OpsError("politica", `la pol\xEDtica vigente de ${caso.workflow} es "${modo}": un agente no puede escribir en el HIS`);
44523
+ const clave = str2(input2.clave);
44524
+ const registro = str2(input2.registro);
44525
+ const objetivo = str2(input2.objetivo);
44526
+ if (!clave || !registro) throw new OpsError("validacion", "clave (qu\xE9 escritura de negocio es) y registro (qu\xE9 registro del HIS toca) son obligatorios");
44527
+ if (!objetivo) throw new OpsError("objetivo_requerido", "objetivo es obligatorio (qu\xE9 pantalla, endpoint o archivo se toca)");
44528
+ if (!CONECTORES.includes(input2.conector)) throw new OpsError("conector_invalido", `conector debe ser: ${CONECTORES.join(", ")}`);
44529
+ const leido = input2.leido && typeof input2.leido === "object" && !Array.isArray(input2.leido) ? input2.leido : {};
44530
+ if (Object.keys(leido).length === 0) throw new OpsError("validacion", "leido: lo que se ley\xF3 del registro antes de escribir (al menos un campo); se compara justo antes de guardar");
44531
+ const opciones = await obtenerOpciones(ctx, input2.companyId, caso.workflow);
44532
+ const { hoy, hora, inicioDelDia } = ahoraEnHospital();
44533
+ const cuenta = await ctx.db.query(
44534
+ `SELECT count(*) AS cantidad FROM ${t2(ctx, "accion")} a JOIN ${t2(ctx, "caso")} c ON c.company_id = a.company_id AND c.id = a.caso_id WHERE a.company_id = $1 AND c.workflow = $2 AND a.tipo = 'escritura' AND a.actor_tipo = 'agente' AND a.clave IS NOT NULL AND a.resultado IN ('ok', 'pendiente', 'incierta') AND a.created_at >= $3`,
44535
+ [input2.companyId, caso.workflow, inicioDelDia.toISOString()]
44536
+ );
44537
+ const previas = await ctx.db.query(
44538
+ `SELECT id, caso_id, objetivo, resultado, vence_en FROM ${t2(ctx, "accion")} WHERE company_id = $1 AND clave = $2 AND tipo = 'escritura' ORDER BY created_at DESC LIMIT 1`,
44539
+ [input2.companyId, clave]
44540
+ );
44541
+ const ocupadas = await ctx.db.query(
44542
+ `SELECT id, caso_id FROM ${t2(ctx, "accion")} WHERE company_id = $1 AND registro = $2 AND tipo = 'escritura' AND resultado = 'pendiente' AND vence_en > now() AND caso_id <> $3 LIMIT 1`,
44543
+ [input2.companyId, registro, caso.id]
44544
+ );
44545
+ const previa = previas[0];
44546
+ const vencida = Boolean(previa && String(previa.resultado) === "pendiente" && previa.vence_en && Date.parse(iso2(previa.vence_en)) < Date.now());
44547
+ if (previa && vencida) await marcarDudosa(ctx, input2.companyId, previa, "la reserva venci\xF3 sin cerrarse: no se sabe si se escribi\xF3");
44548
+ const r = evaluarReserva({
44549
+ freno: await leerFreno(ctx, input2.companyId),
44550
+ ventana: leerVentana(opciones.ventana_escritura),
44551
+ hora,
44552
+ diaSemana: diaDeLaSemana(hoy),
44553
+ tope: leerTopeDiario(opciones.tope_escrituras_dia),
44554
+ escriturasHoy: Number(cuenta[0]?.cantidad ?? 0),
44555
+ mismaClave: previa ? { id: String(previa.id), resultado: vencida ? "incierta" : String(previa.resultado), vencida } : null,
44556
+ ocupado: ocupadas[0] ? { id: String(ocupadas[0].id), casoId: String(ocupadas[0].caso_id) } : null
44557
+ });
44558
+ if (!r.ok) throw new OpsError(r.codigo, r.motivo, r.accionId ? { accionId: r.accionId } : void 0);
44559
+ const id3 = randomUUID2();
44560
+ const evidencia = { leido };
44561
+ if (caso.workflow === "recupero_turno" && caso.estado !== "asignando") evidencia.fuera_de_flujo = true;
44562
+ const venceEn = new Date(Date.now() + MINUTOS_RESERVA * 6e4).toISOString();
44563
+ await ctx.db.execute(
44564
+ `INSERT INTO ${t2(ctx, "accion")} (id, company_id, caso_id, tipo, conector, objetivo, resultado, evidencia, detalle, actor_tipo, actor_id, run_id, clave, registro, vence_en) VALUES ($1, $2, $3, 'escritura', $4, $5, 'pendiente', $6::jsonb, $7, $8, $9, $10, $11, $12, $13)`,
44565
+ [id3, input2.companyId, caso.id, input2.conector, objetivo, JSON.stringify(evidencia), null, input2.actor.tipo, input2.actor.id, input2.actor.runId ?? null, clave, registro, venceEn]
44566
+ );
44567
+ await ctx.db.execute(`UPDATE ${t2(ctx, "caso")} SET updated_at = now() WHERE company_id = $1 AND id = $2`, [input2.companyId, caso.id]);
44568
+ return { id: id3, casoId: caso.id, tipo: "escritura", conector: input2.conector, objetivo, resultado: "pendiente", evidencia, detalle: null, actorTipo: input2.actor.tipo, actorId: input2.actor.id, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
44569
+ }
44570
+ async function confirmarEscritura(ctx, input2) {
44571
+ const fila = await leerAccionEscritura(ctx, input2.companyId, input2.accionId);
44572
+ if (String(fila.resultado) !== "pendiente") throw new OpsError("reserva_cerrada", `la reserva ${input2.accionId} ya est\xE1 cerrada (${String(fila.resultado)})`);
44573
+ if (fila.vence_en && Date.parse(iso2(fila.vence_en)) < Date.now()) {
44574
+ await marcarDudosa(ctx, input2.companyId, fila, "la reserva venci\xF3 antes de confirmarse");
44575
+ throw new OpsError("reserva_vencida", "la reserva venci\xF3: una persona tiene que mirar el HIS antes de reintentar");
44576
+ }
44577
+ const evidencia = jsonb2(fila.evidencia);
44578
+ const leidoAntes = evidencia.leido && typeof evidencia.leido === "object" ? evidencia.leido : {};
44579
+ const leido = input2.leido && typeof input2.leido === "object" && !Array.isArray(input2.leido) ? input2.leido : {};
44580
+ const cambios = compararLecturas(leidoAntes, leido);
44581
+ const nueva = { ...evidencia, leido_al_confirmar: leido };
44582
+ if (cambios.length > 0) {
44583
+ await ctx.db.execute(
44584
+ `UPDATE ${t2(ctx, "accion")} SET resultado = 'descartada', evidencia = $3::jsonb, detalle = $4, cerrada_at = now() WHERE company_id = $1 AND id = $2`,
44585
+ [input2.companyId, input2.accionId, JSON.stringify(nueva), `el registro cambi\xF3 desde que se ley\xF3 (${cambios.join(", ")}): no se escribi\xF3`]
44586
+ );
44587
+ throw new OpsError("registro_cambiado", `el registro cambi\xF3 desde que se ley\xF3 (${cambios.join(", ")}): no se escribe; volver a proponer con la lectura nueva`, { diferencias: cambios });
44588
+ }
44589
+ await ctx.db.execute(
44590
+ `UPDATE ${t2(ctx, "accion")} SET confirmada_at = now(), evidencia = $3::jsonb WHERE company_id = $1 AND id = $2`,
44591
+ [input2.companyId, input2.accionId, JSON.stringify(nueva)]
44592
+ );
44593
+ return aAccion(fila, { evidencia: nueva });
44594
+ }
44595
+ async function cerrarEscritura(ctx, input2) {
44596
+ if (!RESULTADOS_CIERRE.includes(input2.resultado)) throw new OpsError("resultado_invalido", `resultado debe ser: ${RESULTADOS_CIERRE.join(", ")}`);
44597
+ const fila = await leerAccionEscritura(ctx, input2.companyId, input2.accionId);
44598
+ if (String(fila.resultado) !== "pendiente") throw new OpsError("reserva_cerrada", `la reserva ${input2.accionId} ya est\xE1 cerrada (${String(fila.resultado)})`);
44599
+ const casoDeLaEscritura = await leerCaso(ctx, input2.companyId, String(fila.caso_id));
44600
+ const exigeRelectura = Boolean(casoDeLaEscritura && getWorkflow(casoDeLaEscritura.workflow)?.releerAntesDeEscribir);
44601
+ if (input2.resultado === "ok" && exigeRelectura && !fila.confirmada_at) throw new OpsError("sin_confirmar", "en esta funci\xF3n no se puede dar por hecha una escritura que no se confirm\xF3 releyendo el registro justo antes de guardar");
44602
+ const evidencia = { ...jsonb2(fila.evidencia), ...input2.evidencia ?? {} };
44603
+ const detalle = str2(input2.detalle);
44604
+ if (input2.resultado === "incierta") {
44605
+ await ctx.db.execute(`UPDATE ${t2(ctx, "accion")} SET evidencia = $3::jsonb WHERE company_id = $1 AND id = $2`, [input2.companyId, input2.accionId, JSON.stringify(evidencia)]);
44606
+ await marcarDudosa(ctx, input2.companyId, fila, detalle ?? "el puesto no pudo comprobar si la escritura qued\xF3 hecha");
44607
+ } else {
44608
+ await ctx.db.execute(
44609
+ `UPDATE ${t2(ctx, "accion")} SET resultado = $3, evidencia = $4::jsonb, detalle = $5, cerrada_at = now() WHERE company_id = $1 AND id = $2`,
44610
+ [input2.companyId, input2.accionId, input2.resultado, JSON.stringify(evidencia), detalle]
44611
+ );
44612
+ }
44613
+ const caso = await leerCaso(ctx, input2.companyId, String(fila.caso_id));
44614
+ let frenada = false;
44615
+ if (caso && input2.resultado !== "ok") {
44616
+ const ultimas = await ctx.db.query(
44617
+ `SELECT a.resultado FROM ${t2(ctx, "accion")} a JOIN ${t2(ctx, "caso")} c ON c.company_id = a.company_id AND c.id = a.caso_id WHERE a.company_id = $1 AND c.workflow = $2 AND a.tipo = 'escritura' AND a.actor_tipo = 'agente' AND a.cerrada_at IS NOT NULL AND a.resultado IN ('ok', 'error', 'incierta') ORDER BY a.cerrada_at DESC LIMIT ${FALLAS_PARA_FRENAR}`,
44618
+ [input2.companyId, caso.workflow]
44619
+ );
44620
+ if (debeFrenar(ultimas.map((u) => String(u.resultado)))) {
44621
+ await frenarFuncion(ctx, input2.companyId, caso.workflow);
44622
+ frenada = true;
44623
+ }
44624
+ }
44625
+ return { accion: aAccion(fila, { resultado: input2.resultado, evidencia, detalle }), frenada };
44626
+ }
44298
44627
  function numero(v) {
44299
44628
  if (typeof v === "number" && Number.isFinite(v)) return v;
44300
44629
  if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
@@ -45436,6 +45765,28 @@ Transiciones registradas: ${detalle.transiciones.length}; acciones registradas:
45436
45765
  });
45437
45766
  return { content: `Acci\xF3n registrada ${accion.id} (${accion.tipo} \xB7 ${accion.conector} \xB7 ${accion.resultado})`, data: { accion } };
45438
45767
  }));
45768
+ ctx.tools.register(TOOL_NAMES.reservarEscritura, toolSchema(TOOL_NAMES.reservarEscritura), async (params, runCtx) => conErrores(async () => {
45769
+ const p = obj(params);
45770
+ const accion = await reservarEscritura(ctx, { companyId: runCtx.companyId, casoId: str3(p.casoId) ?? "", clave: str3(p.clave) ?? "", registro: str3(p.registro) ?? "", conector: str3(p.conector) ?? "", objetivo: str3(p.objetivo) ?? "", leido: obj(p.leido), actor: actorAgente(runCtx) });
45771
+ return { content: `Escritura reservada ${accion.id}: rele\xE9 el registro justo antes de guardar y confirmala con ops_confirmar_escritura.`, data: { accion } };
45772
+ }));
45773
+ ctx.tools.register(TOOL_NAMES.confirmarEscritura, toolSchema(TOOL_NAMES.confirmarEscritura), async (params, runCtx) => conErrores(async () => {
45774
+ const p = obj(params);
45775
+ const accion = await confirmarEscritura(ctx, { companyId: runCtx.companyId, accionId: str3(p.accionId) ?? "", leido: obj(p.leido), actor: actorAgente(runCtx) });
45776
+ return { content: `Confirmada ${accion.id}: el registro no cambi\xF3. Guard\xE1 en el HIS y cerr\xE1 con ops_cerrar_escritura.`, data: { accion } };
45777
+ }));
45778
+ ctx.tools.register(TOOL_NAMES.cerrarEscritura, toolSchema(TOOL_NAMES.cerrarEscritura), async (params, runCtx) => conErrores(async () => {
45779
+ const p = obj(params);
45780
+ const r = await cerrarEscritura(ctx, { companyId: runCtx.companyId, accionId: str3(p.accionId) ?? "", resultado: str3(p.resultado) ?? "", evidencia: obj(p.evidencia), detalle: str3(p.detalle), actor: actorAgente(runCtx) });
45781
+ return { content: `Escritura ${r.accion.id} cerrada: ${r.accion.resultado}${r.frenada ? " \xB7 la funci\xF3n qued\xF3 frenada por fallas seguidas" : ""}`, data: r };
45782
+ }));
45783
+ ctx.tools.register(TOOL_NAMES.verificarEscritura, toolSchema(TOOL_NAMES.verificarEscritura), async (params, runCtx) => conErrores(async () => {
45784
+ const p = obj(params);
45785
+ const quedo = p.quedo === true ? true : p.quedo === false ? false : null;
45786
+ const r = await verificarEscritura(ctx, { companyId: runCtx.companyId, accionId: str3(p.accionId) ?? "", quedo, leido: obj(p.leido), detalle: str3(p.detalle), actor: actorAgente(runCtx) });
45787
+ const texto7 = quedo === true ? "qued\xF3 escrita: se da por hecha" : quedo === false ? "no se escribi\xF3: se puede reintentar" : r.paraPersona ? "no se pudo saber: pasa a una persona" : "no se pudo saber: volv\xE9 a verificar m\xE1s tarde";
45788
+ return { content: `Escritura ${r.accion.id}: ${texto7}`, data: r };
45789
+ }));
45439
45790
  ctx.tools.register(TOOL_NAMES.listarReglas, toolSchema(TOOL_NAMES.listarReglas), async (params, runCtx) => conErrores(async () => {
45440
45791
  const p = obj(params);
45441
45792
  const reglas = await listarReglas(ctx, runCtx.companyId, { workflow: str3(p.workflow) ?? "demanda_pendiente", estado: str3(p.estado) });
@@ -0,0 +1,13 @@
1
+ -- Minutos de trabajo humano revisados el 17/10/2026 (ver docs/ESTIMACION-MINUTOS.md): se corrigen los casos ya cerrados
2
+ -- que contaron minutos, para que el histórico y lo nuevo se midan igual. Los casos que no contaron (0) no cambian.
3
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = 8 WHERE workflow = 'actualizacion_arancel' AND minutos_absorbidos > 0;
4
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = 12 WHERE workflow = 'recupero_turno' AND minutos_absorbidos > 0;
5
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = 4 WHERE workflow = 'cuenta_completa' AND minutos_absorbidos > 0;
6
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = 3 WHERE workflow = 'facturacion_pendiente' AND minutos_absorbidos > 0;
7
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = 5 WHERE workflow = 'credencial_profesional' AND minutos_absorbidos > 0;
8
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = 6 WHERE workflow = 'demanda_pendiente' AND minutos_absorbidos > 0;
9
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = LEAST(480, 30 + 3 * jsonb_array_length(CASE WHEN jsonb_typeof(datos->'items') = 'array' THEN datos->'items' ELSE '[]'::jsonb END)) WHERE workflow = 'circular_aranceles' AND minutos_absorbidos > 0;
10
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = LEAST(180, 20 + 2 * jsonb_array_length(CASE WHEN jsonb_typeof(datos->'items') = 'array' THEN datos->'items' ELSE '[]'::jsonb END)) WHERE workflow = 'cobranza_financiador' AND minutos_absorbidos > 0;
11
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = 6 WHERE workflow = 'internacion_al_dia' AND minutos_absorbidos > 0;
12
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = 10 WHERE workflow = 'honorario_en_cero' AND minutos_absorbidos > 0;
13
+ UPDATE plugin_autopilot_ops_1a68482ede.caso SET minutos_absorbidos = 15 WHERE workflow = 'liquidacion_profesional' AND minutos_absorbidos > 0;
@@ -0,0 +1,16 @@
1
+ -- Escrituras seguras sobre el HIS (src/escrituras.ts): una escritura de un puesto se reserva con una clave de negocio
2
+ -- y el registro que toca, se confirma releyendo justo antes de guardar y se cierra con su resultado.
3
+ -- clave: la misma escritura no se repite. registro: dos trabajos no escriben a la vez el mismo registro.
4
+ -- vence_en: una reserva sin cerrar a tiempo se considera dudosa.
5
+
6
+ ALTER TABLE plugin_autopilot_ops_1a68482ede.accion ADD COLUMN clave text;
7
+ ALTER TABLE plugin_autopilot_ops_1a68482ede.accion ADD COLUMN registro text;
8
+ ALTER TABLE plugin_autopilot_ops_1a68482ede.accion ADD COLUMN vence_en timestamptz;
9
+ ALTER TABLE plugin_autopilot_ops_1a68482ede.accion ADD COLUMN confirmada_at timestamptz;
10
+ ALTER TABLE plugin_autopilot_ops_1a68482ede.accion ADD COLUMN cerrada_at timestamptz;
11
+
12
+ CREATE INDEX accion_clave_idx
13
+ ON plugin_autopilot_ops_1a68482ede.accion (company_id, clave);
14
+
15
+ CREATE INDEX accion_registro_idx
16
+ ON plugin_autopilot_ops_1a68482ede.accion (company_id, registro);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-autopilot-ops",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
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": [