paperclip-plugin-autopilot-ops 0.1.13 → 0.1.14

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/worker.js CHANGED
@@ -844,14 +844,14 @@ function promiseAllObject(promisesObj) {
844
844
  }
845
845
  function randomString(length = 10) {
846
846
  const chars = "abcdefghijklmnopqrstuvwxyz";
847
- let str3 = "";
847
+ let str4 = "";
848
848
  for (let i = 0; i < length; i++) {
849
- str3 += chars[Math.floor(Math.random() * chars.length)];
849
+ str4 += chars[Math.floor(Math.random() * chars.length)];
850
850
  }
851
- return str3;
851
+ return str4;
852
852
  }
853
- function esc(str3) {
854
- return JSON.stringify(str3);
853
+ function esc(str4) {
854
+ return JSON.stringify(str4);
855
855
  }
856
856
  function slugify(input2) {
857
857
  return input2.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -913,8 +913,8 @@ function numKeys(data) {
913
913
  return keyCount;
914
914
  }
915
915
  var getParsedType = (data) => {
916
- const t3 = typeof data;
917
- switch (t3) {
916
+ const t4 = typeof data;
917
+ switch (t4) {
918
918
  case "undefined":
919
919
  return "undefined";
920
920
  case "string":
@@ -953,7 +953,7 @@ var getParsedType = (data) => {
953
953
  }
954
954
  return "object";
955
955
  default:
956
- throw new Error(`Unknown data type: ${t3}`);
956
+ throw new Error(`Unknown data type: ${t4}`);
957
957
  }
958
958
  };
959
959
  var propertyKeyTypes = /* @__PURE__ */ new Set(["string", "number", "symbol"]);
@@ -965,8 +965,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
965
965
  "symbol",
966
966
  "undefined"
967
967
  ]);
968
- function escapeRegex(str3) {
969
- return str3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
968
+ function escapeRegex(str4) {
969
+ return str4.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
970
970
  }
971
971
  function clone(inst, def2, params) {
972
972
  const cl = new inst._zod.constr(def2 ?? inst._zod.def);
@@ -1232,13 +1232,13 @@ function getSizableOrigin(input2) {
1232
1232
  return "unknown";
1233
1233
  }
1234
1234
  var highSurrogate = /[\uD800-\uDBFF]/;
1235
- function codePointLength(str3) {
1236
- const units = str3.length;
1237
- if (!highSurrogate.test(str3))
1235
+ function codePointLength(str4) {
1236
+ const units = str4.length;
1237
+ if (!highSurrogate.test(str4))
1238
1238
  return units;
1239
1239
  let count = units;
1240
1240
  for (let i = 0; i < units - 1; i++) {
1241
- if ((str3.charCodeAt(i) & 64512) === 55296 && (str3.charCodeAt(i + 1) & 64512) === 56320) {
1241
+ if ((str4.charCodeAt(i) & 64512) === 55296 && (str4.charCodeAt(i + 1) & 64512) === 56320) {
1242
1242
  count--;
1243
1243
  i++;
1244
1244
  }
@@ -1253,8 +1253,8 @@ function getLengthableOrigin(input2) {
1253
1253
  return "unknown";
1254
1254
  }
1255
1255
  function parsedType(data) {
1256
- const t3 = typeof data;
1257
- switch (t3) {
1256
+ const t4 = typeof data;
1257
+ switch (t4) {
1258
1258
  case "number": {
1259
1259
  return Number.isNaN(data) ? "nan" : "number";
1260
1260
  }
@@ -1271,7 +1271,7 @@ function parsedType(data) {
1271
1271
  }
1272
1272
  }
1273
1273
  }
1274
- return t3;
1274
+ return t4;
1275
1275
  }
1276
1276
  function issue(...args) {
1277
1277
  const [iss, input2, inst] = args;
@@ -3495,7 +3495,7 @@ function handleCatchall(proms, input2, payload, ctx, def2, inst, abortEarly) {
3495
3495
  const unrecognized = [];
3496
3496
  const keySet = def2.keySet;
3497
3497
  const _catchall = def2.catchall._zod;
3498
- const t3 = _catchall.def.type;
3498
+ const t4 = _catchall.def.type;
3499
3499
  const optin = _catchall.optin;
3500
3500
  const optout = _catchall.optout;
3501
3501
  let seen = 0;
@@ -3508,11 +3508,11 @@ function handleCatchall(proms, input2, payload, ctx, def2, inst, abortEarly) {
3508
3508
  if (keySet.has(key))
3509
3509
  continue;
3510
3510
  if (key === "__proto__") {
3511
- if (t3 === "never")
3511
+ if (t4 === "never")
3512
3512
  unrecognized.push(key);
3513
3513
  continue;
3514
3514
  }
3515
- if (t3 === "never") {
3515
+ if (t4 === "never") {
3516
3516
  unrecognized.push(key);
3517
3517
  continue;
3518
3518
  }
@@ -7748,16 +7748,16 @@ var error20 = () => {
7748
7748
  number: { unit: "", shortLabel: "\u05E7\u05D8\u05DF", longLabel: "\u05D2\u05D3\u05D5\u05DC" }
7749
7749
  // no unit
7750
7750
  };
7751
- const typeEntry = (t3) => t3 ? TypeNames[t3] : void 0;
7752
- const typeLabel = (t3) => {
7753
- const e = typeEntry(t3);
7751
+ const typeEntry = (t4) => t4 ? TypeNames[t4] : void 0;
7752
+ const typeLabel = (t4) => {
7753
+ const e = typeEntry(t4);
7754
7754
  if (e)
7755
7755
  return e.label;
7756
- return t3 ?? TypeNames.unknown.label;
7756
+ return t4 ?? TypeNames.unknown.label;
7757
7757
  };
7758
- const withDefinite = (t3) => `\u05D4${typeLabel(t3)}`;
7759
- const verbFor = (t3) => {
7760
- const e = typeEntry(t3);
7758
+ const withDefinite = (t4) => `\u05D4${typeLabel(t4)}`;
7759
+ const verbFor = (t4) => {
7760
+ const e = typeEntry(t4);
7761
7761
  const gender = e?.gender ?? "m";
7762
7762
  return gender === "f" ? "\u05E6\u05E8\u05D9\u05DB\u05D4 \u05DC\u05D4\u05D9\u05D5\u05EA" : "\u05E6\u05E8\u05D9\u05DA \u05DC\u05D4\u05D9\u05D5\u05EA";
7763
7763
  };
@@ -16638,7 +16638,7 @@ function stringifyKeyNames(bySchema, json2, visited) {
16638
16638
  json2 = { ...json2, [keyword]: mapped };
16639
16639
  }
16640
16640
  const types = Array.isArray(json2.type) ? json2.type : [json2.type];
16641
- const numericType = !types.includes("string") && types.some((t3) => t3 === "number" || t3 === "integer");
16641
+ const numericType = !types.includes("string") && types.some((t4) => t4 === "number" || t4 === "integer");
16642
16642
  const values = json2.enum ?? (json2.const !== void 0 ? [json2.const] : void 0);
16643
16643
  if (!numericType && !values?.some((v) => typeof v === "number"))
16644
16644
  return json2;
@@ -19141,8 +19141,8 @@ function convertBaseSchema(schema, ctx) {
19141
19141
  }
19142
19142
  const type = schema.type;
19143
19143
  if (Array.isArray(type)) {
19144
- const typeSchemas = type.map((t3) => {
19145
- const typeSchema = { ...schema, type: t3 };
19144
+ const typeSchemas = type.map((t4) => {
19145
+ const typeSchema = { ...schema, type: t4 };
19146
19146
  return convertBaseSchema(typeSchema, ctx);
19147
19147
  });
19148
19148
  if (typeSchemas.length === 0) {
@@ -31178,7 +31178,7 @@ var pluginManifestV1Schema = external_exports.object({
31178
31178
  }
31179
31179
  }
31180
31180
  if (manifest2.tools) {
31181
- const toolNames = manifest2.tools.map((t3) => t3.name);
31181
+ const toolNames = manifest2.tools.map((t4) => t4.name);
31182
31182
  const duplicates = toolNames.filter((name, i) => toolNames.indexOf(name) !== i);
31183
31183
  if (duplicates.length > 0) {
31184
31184
  ctx.addIssue({
@@ -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.13";
39935
+ var PLUGIN_VERSION = "0.1.14";
39936
39936
  var DB_NAMESPACE_SLUG = "autopilot_ops";
39937
39937
  var PAGE_ROUTE = "autopilot";
39938
39938
  var TOOL_NAMES = {
@@ -39943,7 +39943,9 @@ var TOOL_NAMES = {
39943
39943
  registrarAccion: "ops_registrar_accion",
39944
39944
  consultarIndicador: "ops_consultar_indicador",
39945
39945
  publicarTablero: "ops_publicar_tablero",
39946
- listarTableros: "ops_listar_tableros"
39946
+ listarTableros: "ops_listar_tableros",
39947
+ listarReglas: "ops_listar_reglas",
39948
+ informarPruebaRegla: "ops_informar_prueba_regla"
39947
39949
  };
39948
39950
  var DATA_KEYS = {
39949
39951
  tablero: "tablero",
@@ -40003,6 +40005,33 @@ var manifest = {
40003
40005
  }
40004
40006
  }
40005
40007
  },
40008
+ {
40009
+ name: TOOL_NAMES.listarReglas,
40010
+ displayName: "Listar reglas",
40011
+ description: "Lista las reglas que el hospital arm\xF3 para un puesto (demanda_pendiente): tipo, par\xE1metros, contacto (canal, intentos, horario), cierre, estado y versi\xF3n. Filtro opcional: estado (activa). Solo lectura: las reglas las crea y activa el hospital desde el portal.",
40012
+ parametersSchema: {
40013
+ type: "object",
40014
+ properties: {
40015
+ workflow: { type: "string" },
40016
+ estado: { type: "string", enum: ["borrador", "activa", "pausada"] }
40017
+ }
40018
+ }
40019
+ },
40020
+ {
40021
+ name: TOOL_NAMES.informarPruebaRegla,
40022
+ displayName: "Informar prueba de regla",
40023
+ description: "Informa el resultado de probar una regla sobre los datos reales: a cu\xE1ntos pacientes alcanza hoy (alcanza) y hasta 10 referencias de muestra (orden:<id>, solicitud:<id>, control:<persona>:<regla>; nunca datos personales). Sin esta prueba de la versi\xF3n vigente, el hospital no puede activar la regla.",
40024
+ parametersSchema: {
40025
+ type: "object",
40026
+ properties: {
40027
+ reglaId: { type: "string" },
40028
+ alcanza: { type: "number" },
40029
+ muestra: { type: "array", items: { type: "string" } },
40030
+ consulta: { type: "string", description: "La consulta que se corri\xF3 (opcional, para auditor\xEDa)." }
40031
+ },
40032
+ required: ["reglaId", "alcanza"]
40033
+ }
40034
+ },
40006
40035
  {
40007
40036
  name: TOOL_NAMES.leerCaso,
40008
40037
  displayName: "Leer caso",
@@ -40016,7 +40045,7 @@ var manifest = {
40016
40045
  {
40017
40046
  name: TOOL_NAMES.abrirCaso,
40018
40047
  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"), `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), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal del hospital), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
40048
+ description: 'Abre un caso nuevo en un workflow, en su estado inicial. Workflows: `demanda_pendiente` (referencia obligatoria `orden:<id>`, `solicitud:<id>` o `control:<persona_id>:<regla_id>`; datos: regla_id obligatorio y la regla tiene que estar activa, persona_id, tipo de origen, estudio o especialidad o prestacion, fecha_origen AAAA-MM-DD, dias; el plugin fija regla_version, canal, intentos, vence_el; nunca nombre, documento ni tel\xE9fono del paciente),`actualizacion_arancel`, `tarea_operativa`, `recupero_turno` (datos: fecha AAAA-MM-DD, hora HH:MM, profesional, especialidad del turno libre; referencia "AAAA-MM-DD HH:MM \xB7 Profesional"), `cuenta_completa` (una prestaci\xF3n pendiente de facturar con faltantes; referencia obligatoria "bono:<n\xFAmero>", datos: bono_id, bono_numero, obra_social, plan, prestacion, fecha_prestacion AAAA-MM-DD, monto_pesos), `facturacion_pendiente` (una prestaci\xF3n sin facturar; misma referencia `bono:<n\xFAmero>` y mismos datos; el plugin calcula plazo_dias, vence_el y plazo_confirmado con los plazos por obra social de la instituci\xF3n), `credencial_profesional` (un profesional con agenda; referencia obligatoria `profesional:<id>`; datos: profesional_id, profesional (apellido y nombre, es personal del hospital), matricula_nacional, matricula_provincial, codigo_provincia, especialidades). `referencia` identifica el objeto real (convenio, cuenta, turno) y evita duplicados: si ya existe un caso abierto con la misma referencia, se devuelve ese. Pasar `idempotencyKey` para reintentos seguros.',
40020
40049
  parametersSchema: {
40021
40050
  type: "object",
40022
40051
  properties: {
@@ -40033,7 +40062,7 @@ var manifest = {
40033
40062
  {
40034
40063
  name: TOOL_NAMES.transicionar,
40035
40064
  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; si la instituci\xF3n pide consulta previa (ops_leer_caso lo dice), `ofrecer` con canal y esperar `aceptar`/`no_acepta`; **antes de escribir en el sistema, `autorizar`** (sin esa autorizaci\xF3n no se escribe); despu\xE9s de escribir, `asignar` con accion_id; `verificar` con leido_fecha, leido_hora, leido_profesional y leido_solicitud_id; `sin_candidato` y `descartar` con motivo. En `cuenta_completa` el agente solo lee: `observar` con bono_id, bono_estado (no_confirmado, confirmado o prefacturado), faltantes [{regla, detalle, item?}] con reglas del cat\xE1logo (autorizacion_faltante, validacion_faltante, afiliado_faltante, orden_faltante, prestacion_sin_valor, epicrisis_faltante), lectura_id (la lectura registrada con ops_registrar_accion, conector "base") y fuente; `comprobar` con faltantes_leidos (lista vac\xEDa si ya est\xE1 completo), bono_estado_leido y lectura_id; `descartar` con motivo si el bono se elimin\xF3 o anul\xF3. En `facturacion_pendiente` (solo lee): `observar` con bono_id, bono_estado, fecha_prestacion, obra_social, lectura_id (conector "base") y fuente; `comprobar` con bono_estado_leido y lectura_id (facturado/refacturado \u2192 facturada; prefacturado \u2192 en prefactura; si no, sigue por presentar); `descartar` con motivo. En `credencial_profesional` (solo lee): `verificar` con profesional_id, matricula y tipo_matricula (nacional|provincial) si hay matr\xEDcula, resultado = el objeto que devuelve verificar-matriculas.mjs tal cual ({sin_matricula, encontrada, coincide_nombre, situacion, sancion, profesion, provincia, emisor}), lectura_id (conector "api", la consulta al registro) y fuente; el plugin decide vigente u observada y fija la observaci\xF3n (sin_matricula, no_figura, otra_persona, inhabilitada, sancion); `descartar` con motivo si el profesional ya no est\xE1 activo.',
40065
+ description: 'Aplica una transici\xF3n del workflow al caso (p. ej. `proponer`, `aplicar`, `verificar`). El plugin valida contra la tabla de transiciones, la pol\xEDtica vigente (observa / propone / ejecuta / corte) y las reglas de negocio; si algo falla, el caso no cambia y se devuelve el motivo. Las transiciones humanas (aprobar, rechazar, corregir) no est\xE1n disponibles para agentes. Cuando una persona 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. En `facturacion_pendiente` (solo lee): `observar` con bono_id, bono_estado, fecha_prestacion, obra_social, lectura_id (conector "base") y fuente; `comprobar` con bono_estado_leido y lectura_id (facturado/refacturado \u2192 facturada; prefacturado \u2192 en prefactura; si no, sigue por presentar); `descartar` con motivo. En `demanda_pendiente`: `contactar` (nivel ejecuta; datos: canal igual al de la regla y accion_id de la escritura registrada; el plugin controla regla activa, intentos, espera entre intentos y horario), `responder` (respuesta: acepta|rechaza|no_contesta), `comprobar` (lectura_id de la relectura de Alephoo, turno_id y turno_estado del turno encontrado), `cerrar` (motivo; solo con los intentos agotados o el plazo vencido), `descartar` (motivo). `credencial_profesional` (solo lee): `verificar` con profesional_id, matricula y tipo_matricula (nacional|provincial) si hay matr\xEDcula, resultado = el objeto que devuelve verificar-matriculas.mjs tal cual ({sin_matricula, encontrada, coincide_nombre, situacion, sancion, profesion, provincia, emisor}), lectura_id (conector "api", la consulta al registro) y fuente; el plugin decide vigente u observada y fija la observaci\xF3n (sin_matricula, no_figura, otra_persona, inhabilitada, sancion); `descartar` con motivo si el profesional ya no est\xE1 activo.',
40037
40066
  parametersSchema: {
40038
40067
  type: "object",
40039
40068
  properties: {
@@ -40232,6 +40261,54 @@ var manifest = {
40232
40261
  auth: "board",
40233
40262
  capability: "api.routes.register",
40234
40263
  companyResolution: { from: "query", key: "companyId" }
40264
+ },
40265
+ {
40266
+ routeKey: "reglas",
40267
+ method: "GET",
40268
+ path: "/reglas",
40269
+ auth: "board",
40270
+ capability: "api.routes.register",
40271
+ companyResolution: { from: "query", key: "companyId" }
40272
+ },
40273
+ {
40274
+ routeKey: "crear-regla",
40275
+ method: "POST",
40276
+ path: "/reglas",
40277
+ auth: "board",
40278
+ capability: "api.routes.register",
40279
+ companyResolution: { from: "body", key: "companyId" }
40280
+ },
40281
+ {
40282
+ routeKey: "editar-regla",
40283
+ method: "POST",
40284
+ path: "/reglas/:reglaId",
40285
+ auth: "board",
40286
+ capability: "api.routes.register",
40287
+ companyResolution: { from: "body", key: "companyId" }
40288
+ },
40289
+ {
40290
+ routeKey: "estado-regla",
40291
+ method: "POST",
40292
+ path: "/reglas/:reglaId/estado",
40293
+ auth: "board",
40294
+ capability: "api.routes.register",
40295
+ companyResolution: { from: "body", key: "companyId" }
40296
+ },
40297
+ {
40298
+ routeKey: "probar-regla",
40299
+ method: "POST",
40300
+ path: "/reglas/:reglaId/probar",
40301
+ auth: "board",
40302
+ capability: "api.routes.register",
40303
+ companyResolution: { from: "body", key: "companyId" }
40304
+ },
40305
+ {
40306
+ routeKey: "contacto",
40307
+ method: "POST",
40308
+ path: "/casos/:casoId/contacto",
40309
+ auth: "board",
40310
+ capability: "api.routes.register",
40311
+ companyResolution: { from: "body", key: "companyId" }
40235
40312
  }
40236
40313
  ],
40237
40314
  ui: {
@@ -40256,7 +40333,7 @@ var manifest = {
40256
40333
  var manifest_default = manifest;
40257
40334
 
40258
40335
  // src/ops.ts
40259
- import { randomUUID } from "node:crypto";
40336
+ import { randomUUID as randomUUID2 } from "node:crypto";
40260
40337
 
40261
40338
  // src/workflows.ts
40262
40339
  var MODOS = ["corte", "observa", "propone", "ejecuta", "autonomo"];
@@ -40459,8 +40536,8 @@ function fechaAISO(v) {
40459
40536
  if (fechaISO(s.slice(0, 10))) return s.slice(0, 10);
40460
40537
  const m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})/.exec(s);
40461
40538
  if (!m) return null;
40462
- const iso3 = `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
40463
- return fechaISO(iso3) ? iso3 : null;
40539
+ const iso4 = `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
40540
+ return fechaISO(iso4) ? iso4 : null;
40464
40541
  }
40465
40542
  function leerCola(v) {
40466
40543
  if (!Array.isArray(v) || v.length === 0) return null;
@@ -41043,6 +41120,151 @@ var credencialProfesional = {
41043
41120
  }
41044
41121
  ]
41045
41122
  };
41123
+ var TIPOS_REGLA_DEMANDA = ["orden_sin_turno", "solicitud_pendiente", "control_periodico"];
41124
+ var CANALES_CONTACTO = ["whatsapp", "sms", "mail", "llamada"];
41125
+ var RESPUESTAS_CONTACTO = ["acepta", "rechaza", "no_contesta"];
41126
+ var TURNO_ATENDIDO = ["M", "R", "V"];
41127
+ function diasEntre(desde, hasta) {
41128
+ const a = fechaAISO(desde);
41129
+ const b = fechaAISO(hasta);
41130
+ if (!a || !b) return null;
41131
+ return Math.round((Date.parse(`${b}T00:00:00Z`) - Date.parse(`${a}T00:00:00Z`)) / 864e5);
41132
+ }
41133
+ function horaDentro(hora, desde, hasta) {
41134
+ const h = horaHM(hora);
41135
+ const d = horaHM(desde);
41136
+ const t4 = horaHM(hasta);
41137
+ if (!h || !d || !t4) return false;
41138
+ return h >= d && h <= t4;
41139
+ }
41140
+ function codigoContacto(v) {
41141
+ const s = texto(v);
41142
+ return s ? s.toLowerCase().replace(/\s+/g, "_") : null;
41143
+ }
41144
+ function respuestaContacto(v) {
41145
+ const s = codigoContacto(v);
41146
+ return s && RESPUESTAS_CONTACTO.includes(s) ? s : null;
41147
+ }
41148
+ function canalContacto(v) {
41149
+ const s = codigoContacto(v);
41150
+ return s && CANALES_CONTACTO.includes(s) ? s : null;
41151
+ }
41152
+ var demandaPendiente = {
41153
+ clave: "demanda_pendiente",
41154
+ nombre: "Demanda pendiente",
41155
+ puesto: "Pacientes y agenda",
41156
+ descripcion: "Mueve la demanda que el hospital ya gener\xF3 y no se ejecut\xF3: \xF3rdenes de estudio sin turno, solicitudes de turno pendientes y controles peri\xF3dicos vencidos. Las reglas las arma el hospital en el portal; el plugin las aplica en cada contacto; el caso se cierra cuando el turno queda agendado.",
41157
+ inicial: "detectado",
41158
+ estados: [
41159
+ "detectado",
41160
+ "contactado",
41161
+ "aceptado",
41162
+ "turno_agendado",
41163
+ "realizado",
41164
+ "no_quiere",
41165
+ "cerrado_sin_turno",
41166
+ "no_corresponde",
41167
+ "descartado"
41168
+ ],
41169
+ finalesOk: ["turno_agendado", "realizado"],
41170
+ finalesNoOk: ["no_quiere", "cerrado_sin_turno", "no_corresponde", "descartado"],
41171
+ minutosAbsorbidos: 12,
41172
+ transiciones: [
41173
+ {
41174
+ via: "contactar",
41175
+ de: ["detectado", "contactado"],
41176
+ a: "contactado",
41177
+ actor: ["agente", "humano"],
41178
+ nivelRequerido: "ejecuta",
41179
+ descripcion: "Contactar al paciente por el canal de la regla (el agente por el CRM de Alephoo; una persona del equipo, por donde lo hizo). Exige la escritura registrada (accion_id).",
41180
+ validar: (_caso, datos, contexto) => {
41181
+ const out = [];
41182
+ const regla = contexto.regla ?? null;
41183
+ const agente = contexto.actor === "agente";
41184
+ out.push(regla && regla.estado === "activa" ? ok("regla_activa") : falla("regla_activa", regla ? `la regla est\xE1 ${regla.estado}: no se contacta hasta que se active` : "la regla que abri\xF3 el caso ya no existe"));
41185
+ const canal = canalContacto(datos.canal);
41186
+ out.push(canal ? ok("canal_informado") : falla("canal_informado", `canal es obligatorio: ${CANALES_CONTACTO.join(", ")}`));
41187
+ if (agente && regla && canal) {
41188
+ out.push(canal === regla.contacto.canal ? ok("canal_de_la_regla") : falla("canal_de_la_regla", `la regla contacta por ${regla.contacto.canal}, no por ${canal}`));
41189
+ }
41190
+ if (regla) {
41191
+ const hechos = contexto.intentosContacto ?? 0;
41192
+ out.push(hechos < regla.contacto.max_intentos ? ok("intentos_disponibles", `${hechos} de ${regla.contacto.max_intentos}`) : falla("intentos_disponibles", `ya se hicieron ${hechos} contactos (tope ${regla.contacto.max_intentos}): cerrar sin turno`));
41193
+ if (agente) {
41194
+ const desdeUltimo = contexto.ultimoContacto ? diasEntre(contexto.ultimoContacto, contexto.hoy) : null;
41195
+ out.push(desdeUltimo === null || desdeUltimo >= regla.contacto.dias_entre_intentos ? ok("espera_entre_intentos") : falla("espera_entre_intentos", `el \xFAltimo contacto fue hace ${desdeUltimo} d\xEDas y la regla pide esperar ${regla.contacto.dias_entre_intentos}`));
41196
+ out.push(horaDentro(contexto.horaActual, regla.contacto.hora_desde, regla.contacto.hora_hasta) ? ok("horario_permitido") : falla("horario_permitido", `son las ${contexto.horaActual ?? "?"} y la regla contacta de ${regla.contacto.hora_desde} a ${regla.contacto.hora_hasta}`));
41197
+ }
41198
+ }
41199
+ out.push(texto(datos.accion_id) ? ok("accion_registrada") : falla("accion_registrada", "contactar requiere accion_id: la escritura (mensaje enviado o llamada hecha) registrada antes"));
41200
+ return out;
41201
+ }
41202
+ },
41203
+ {
41204
+ via: "responder",
41205
+ de: ["contactado"],
41206
+ a: (_caso, datos) => {
41207
+ const r = respuestaContacto(datos.respuesta);
41208
+ return r === "acepta" ? "aceptado" : r === "rechaza" ? "no_quiere" : "contactado";
41209
+ },
41210
+ actor: ["agente", "humano"],
41211
+ nivelRequerido: "observa",
41212
+ descripcion: "Registrar la respuesta del paciente: acepta (va a sacar turno), rechaza o no_contesta (queda para el pr\xF3ximo intento).",
41213
+ validar: (_caso, datos) => [
41214
+ respuestaContacto(datos.respuesta) ? ok("respuesta_valida") : falla("respuesta_valida", `respuesta debe ser ${RESPUESTAS_CONTACTO.join(", ")}`)
41215
+ ]
41216
+ },
41217
+ {
41218
+ via: "comprobar",
41219
+ de: ["detectado", "contactado", "aceptado"],
41220
+ a: (_caso, datos) => TURNO_ATENDIDO.includes(String(datos.turno_estado ?? "").toUpperCase()) ? "realizado" : "turno_agendado",
41221
+ actor: ["agente", "humano"],
41222
+ nivelRequerido: "observa",
41223
+ descripcion: "Releer Alephoo y encontrar el turno de esa orden, solicitud o control: con el turno, el caso se cierra como movido.",
41224
+ validar: (_caso, datos) => [
41225
+ texto(datos.lectura_id) ? ok("lectura_registrada") : falla("lectura_registrada", "comprobar requiere lectura_id: la relectura de Alephoo registrada antes"),
41226
+ refExterna(datos.turno_id) ? ok("turno_presente") : falla("turno_presente", "comprobar requiere turno_id: el turno encontrado en Alephoo; sin turno no hay nada que comprobar")
41227
+ ]
41228
+ },
41229
+ {
41230
+ via: "cerrar",
41231
+ de: ["detectado", "contactado"],
41232
+ a: "cerrado_sin_turno",
41233
+ actor: ["agente", "humano"],
41234
+ nivelRequerido: "observa",
41235
+ descripcion: "Cerrar sin turno: solo cuando se agotaron los intentos o venci\xF3 el plazo de la regla.",
41236
+ validar: (_caso, datos, contexto) => {
41237
+ const out = [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "cerrar requiere motivo")];
41238
+ const regla = contexto.regla ?? null;
41239
+ if (regla) {
41240
+ const hechos = contexto.intentosContacto ?? 0;
41241
+ const dias = contexto.diasAbierto ?? 0;
41242
+ const agotados = hechos >= regla.contacto.max_intentos;
41243
+ const vencido = dias >= regla.cierre.dias_para_cerrar;
41244
+ out.push(agotados || vencido ? ok("cierre_habilitado", agotados ? `${hechos} contactos hechos` : `${dias} d\xEDas abierto`) : falla("cierre_habilitado", `van ${hechos} de ${regla.contacto.max_intentos} contactos y ${dias} de ${regla.cierre.dias_para_cerrar} d\xEDas: todav\xEDa no se cierra`));
41245
+ }
41246
+ return out;
41247
+ }
41248
+ },
41249
+ {
41250
+ via: "descartar",
41251
+ de: ["detectado", "contactado", "aceptado"],
41252
+ a: "descartado",
41253
+ actor: ["agente", "humano"],
41254
+ nivelRequerido: "observa",
41255
+ descripcion: "Descartar con motivo: el dato era err\xF3neo, el paciente ya lo hizo afuera, falleci\xF3 o pidi\xF3 no ser contactado.",
41256
+ validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "descartar requiere motivo")]
41257
+ },
41258
+ {
41259
+ via: "no_corresponde",
41260
+ de: ["detectado", "contactado", "aceptado"],
41261
+ a: "no_corresponde",
41262
+ actor: ["humano"],
41263
+ descripcion: "Una persona del hospital decide que este caso no corresponde a la regla.",
41264
+ validar: (_caso, datos) => [texto(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "no_corresponde requiere motivo")]
41265
+ }
41266
+ ]
41267
+ };
41046
41268
  var WORKFLOWS_SOLO_LECTURA = /* @__PURE__ */ new Set([cuentaCompleta.clave, facturacionPendiente.clave, credencialProfesional.clave]);
41047
41269
  var WORKFLOWS = {
41048
41270
  [actualizacionArancel.clave]: actualizacionArancel,
@@ -41050,7 +41272,8 @@ var WORKFLOWS = {
41050
41272
  [recuperoTurno.clave]: recuperoTurno,
41051
41273
  [cuentaCompleta.clave]: cuentaCompleta,
41052
41274
  [facturacionPendiente.clave]: facturacionPendiente,
41053
- [credencialProfesional.clave]: credencialProfesional
41275
+ [credencialProfesional.clave]: credencialProfesional,
41276
+ [demandaPendiente.clave]: demandaPendiente
41054
41277
  };
41055
41278
  function getWorkflow(clave) {
41056
41279
  return WORKFLOWS[clave] ?? null;
@@ -41072,9 +41295,9 @@ function evaluarTransicion(input2) {
41072
41295
  if (!wf) {
41073
41296
  return { ok: false, codigo: "workflow_desconocido", motivo: `workflow desconocido: ${caso.workflow}`, validaciones: [] };
41074
41297
  }
41075
- const def2 = wf.transiciones.find((t3) => t3.via === via);
41298
+ const def2 = wf.transiciones.find((t4) => t4.via === via);
41076
41299
  if (!def2) {
41077
- const disponibles = wf.transiciones.filter((t3) => t3.de.includes(caso.estado)).map((t3) => t3.via);
41300
+ const disponibles = wf.transiciones.filter((t4) => t4.de.includes(caso.estado)).map((t4) => t4.via);
41078
41301
  return { ok: false, codigo: "via_desconocida", motivo: `la transici\xF3n "${via}" no existe en ${wf.clave}; desde "${caso.estado}" se puede: ${disponibles.join(", ") || "ninguna"}`, validaciones: [] };
41079
41302
  }
41080
41303
  if (!def2.de.includes(caso.estado)) {
@@ -41117,15 +41340,333 @@ function evaluarTransicion(input2) {
41117
41340
  function transicionesDisponibles(caso, actor) {
41118
41341
  const wf = getWorkflow(caso.workflow);
41119
41342
  if (!wf) return [];
41120
- return wf.transiciones.filter((t3) => t3.de.includes(caso.estado) && (!actor || t3.actor.includes(actor)));
41343
+ return wf.transiciones.filter((t4) => t4.de.includes(caso.estado) && (!actor || t4.actor.includes(actor)));
41121
41344
  }
41122
41345
  function transicionCargaManual(caso) {
41123
- return transicionesDisponibles(caso, "humano").find((t3) => t3.cargaManual) ?? null;
41346
+ return transicionesDisponibles(caso, "humano").find((t4) => t4.cargaManual) ?? null;
41124
41347
  }
41125
41348
  function esModo(v) {
41126
41349
  return typeof v === "string" && MODOS.includes(v);
41127
41350
  }
41128
41351
 
41352
+ // src/reglas.ts
41353
+ import { randomUUID } from "node:crypto";
41354
+ var ESTADOS_REGLA = ["borrador", "activa", "pausada"];
41355
+ var LIMITES_REGLA = {
41356
+ dias_desde: [1, 365, 30],
41357
+ cada_meses: [1, 60, 12],
41358
+ max_intentos: [1, 5, 2],
41359
+ dias_entre_intentos: [1, 30, 7],
41360
+ dias_para_cerrar: [7, 180, 45],
41361
+ edad: [0, 120, 0]
41362
+ };
41363
+ var SIN_QUE = ["turno", "informe"];
41364
+ function t(ctx, table) {
41365
+ return `${ctx.db.namespace}.${table}`;
41366
+ }
41367
+ function str(v) {
41368
+ return typeof v === "string" && v.trim().length > 0 ? v.trim().replace(/\s+/g, " ") : null;
41369
+ }
41370
+ function iso(v) {
41371
+ if (v instanceof Date) return v.toISOString();
41372
+ if (typeof v === "string") return v;
41373
+ return "";
41374
+ }
41375
+ function jsonb(v) {
41376
+ if (v && typeof v === "object" && !Array.isArray(v)) return v;
41377
+ if (typeof v === "string") {
41378
+ try {
41379
+ const parsed = JSON.parse(v);
41380
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
41381
+ } catch {
41382
+ return {};
41383
+ }
41384
+ }
41385
+ return {};
41386
+ }
41387
+ function jsonArr(v) {
41388
+ if (Array.isArray(v)) return v;
41389
+ if (typeof v === "string") {
41390
+ try {
41391
+ const parsed = JSON.parse(v);
41392
+ return Array.isArray(parsed) ? parsed : [];
41393
+ } catch {
41394
+ return [];
41395
+ }
41396
+ }
41397
+ return [];
41398
+ }
41399
+ function entero(v) {
41400
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
41401
+ return Number.isInteger(n) ? n : null;
41402
+ }
41403
+ function numeroEn(problemas, campo, v, nombre = campo) {
41404
+ const [min, max, def2] = LIMITES_REGLA[campo];
41405
+ if (v === void 0 || v === null || v === "") return def2;
41406
+ const n = entero(v);
41407
+ if (n === null || n < min || n > max) {
41408
+ problemas.push(`${nombre}: debe ser un entero entre ${min} y ${max}`);
41409
+ return def2;
41410
+ }
41411
+ return n;
41412
+ }
41413
+ function textoCorto(problemas, campo, v, max, obligatorio) {
41414
+ if (v === void 0 || v === null || v === "") {
41415
+ if (obligatorio) problemas.push(`${campo}: es obligatorio`);
41416
+ return null;
41417
+ }
41418
+ const s = typeof v === "string" ? v.trim().replace(/\s+/g, " ") : "";
41419
+ if (!s) {
41420
+ problemas.push(`${campo}: debe ser un texto`);
41421
+ return null;
41422
+ }
41423
+ if (s.length > max) {
41424
+ problemas.push(`${campo}: como m\xE1ximo ${max} caracteres`);
41425
+ return s.slice(0, max);
41426
+ }
41427
+ return s;
41428
+ }
41429
+ function poblacion(problemas, p) {
41430
+ const out = {};
41431
+ if (p.edad_desde !== void 0 && p.edad_desde !== null && p.edad_desde !== "") out.edad_desde = numeroEn(problemas, "edad", p.edad_desde, "edad_desde");
41432
+ if (p.edad_hasta !== void 0 && p.edad_hasta !== null && p.edad_hasta !== "") out.edad_hasta = numeroEn(problemas, "edad", p.edad_hasta, "edad_hasta");
41433
+ if (typeof out.edad_desde === "number" && typeof out.edad_hasta === "number" && out.edad_desde > out.edad_hasta) problemas.push("edad_desde: no puede ser mayor que edad_hasta");
41434
+ if (p.sexo !== void 0 && p.sexo !== null && p.sexo !== "") {
41435
+ const s = String(p.sexo).trim().toUpperCase();
41436
+ if (s === "F" || s === "M") out.sexo = s;
41437
+ else problemas.push("sexo: debe ser F o M");
41438
+ }
41439
+ if (p.obras_sociales !== void 0 && p.obras_sociales !== null) {
41440
+ const lista = Array.isArray(p.obras_sociales) ? p.obras_sociales.map((x) => str(x)).filter((x) => x !== null) : [];
41441
+ if (!Array.isArray(p.obras_sociales)) problemas.push("obras_sociales: debe ser una lista de nombres");
41442
+ else if (lista.length > 20) problemas.push("obras_sociales: como m\xE1ximo 20");
41443
+ else if (lista.length > 0) out.obras_sociales = lista.map((x) => x.slice(0, 80));
41444
+ }
41445
+ return out;
41446
+ }
41447
+ function validarRegla(input2) {
41448
+ const problemas = [];
41449
+ const nombre = textoCorto(problemas, "nombre", input2.nombre, 80, true) ?? "";
41450
+ if (nombre && nombre.length < 3) problemas.push("nombre: al menos 3 caracteres");
41451
+ const tipoCrudo = str(input2.tipo);
41452
+ const tipo = tipoCrudo && TIPOS_REGLA_DEMANDA.includes(tipoCrudo) ? tipoCrudo : null;
41453
+ if (!tipo) problemas.push(`tipo: debe ser ${TIPOS_REGLA_DEMANDA.join(", ")}`);
41454
+ const p = jsonb(input2.parametros);
41455
+ const parametros = poblacion(problemas, p);
41456
+ if (tipo === "orden_sin_turno") {
41457
+ const estudio = textoCorto(problemas, "estudio", p.estudio, 80, false);
41458
+ if (estudio) parametros.estudio = estudio;
41459
+ parametros.dias_desde = numeroEn(problemas, "dias_desde", p.dias_desde);
41460
+ const sin = Array.isArray(p.sin) ? p.sin.map((x) => String(x).trim().toLowerCase()) : [...SIN_QUE];
41461
+ if (sin.length === 0 || sin.some((x) => !SIN_QUE.includes(x))) problemas.push(`sin: lista con ${SIN_QUE.join(" y/o ")}`);
41462
+ parametros.sin = [...new Set(sin)].filter((x) => SIN_QUE.includes(x));
41463
+ } else if (tipo === "solicitud_pendiente") {
41464
+ const especialidad = textoCorto(problemas, "especialidad", p.especialidad, 80, false);
41465
+ if (especialidad) parametros.especialidad = especialidad;
41466
+ const prioridad = textoCorto(problemas, "prioridad", p.prioridad, 40, false);
41467
+ if (prioridad) parametros.prioridad = prioridad;
41468
+ parametros.dias_desde = numeroEn(problemas, "dias_desde", p.dias_desde);
41469
+ } else if (tipo === "control_periodico") {
41470
+ const diagnostico = textoCorto(problemas, "diagnostico", p.diagnostico, 10, true);
41471
+ if (diagnostico) {
41472
+ if (!/^[A-Za-z][0-9]{2}(\.[0-9A-Za-z]{1,2})?$/.test(diagnostico) && !/^[A-Za-z][0-9]{1,2}$/.test(diagnostico)) problemas.push("diagnostico: un c\xF3digo CIE-10 o su prefijo (p. ej. E11 o I10)");
41473
+ else parametros.diagnostico = diagnostico.toUpperCase();
41474
+ }
41475
+ const prestacion = textoCorto(problemas, "prestacion", p.prestacion, 80, true);
41476
+ if (prestacion) parametros.prestacion = prestacion;
41477
+ parametros.cada_meses = numeroEn(problemas, "cada_meses", p.cada_meses);
41478
+ }
41479
+ const c = jsonb(input2.contacto);
41480
+ const canalCrudo = str(c.canal)?.toLowerCase() ?? null;
41481
+ const canal = canalCrudo && CANALES_CONTACTO.includes(canalCrudo) ? canalCrudo : null;
41482
+ if (!canal) problemas.push(`contacto.canal: debe ser ${CANALES_CONTACTO.join(", ")}`);
41483
+ const plantilla = textoCorto(problemas, "contacto.plantilla", c.plantilla, 600, true) ?? "";
41484
+ if (plantilla && plantilla.length < 10) problemas.push("contacto.plantilla: al menos 10 caracteres");
41485
+ const maxIntentos = numeroEn(problemas, "max_intentos", c.max_intentos, "contacto.max_intentos");
41486
+ const diasEntreIntentos = numeroEn(problemas, "dias_entre_intentos", c.dias_entre_intentos, "contacto.dias_entre_intentos");
41487
+ const horaDesde = c.hora_desde === void 0 || c.hora_desde === null || c.hora_desde === "" ? "09:00" : horaHM(c.hora_desde);
41488
+ const horaHasta = c.hora_hasta === void 0 || c.hora_hasta === null || c.hora_hasta === "" ? "19:00" : horaHM(c.hora_hasta);
41489
+ if (!horaDesde) problemas.push("contacto.hora_desde: hora HH:MM");
41490
+ if (!horaHasta) problemas.push("contacto.hora_hasta: hora HH:MM");
41491
+ if (horaDesde && horaHasta && horaDesde >= horaHasta) problemas.push("contacto.hora_desde: tiene que ser anterior a hora_hasta");
41492
+ const ci = jsonb(input2.cierre);
41493
+ const diasParaCerrar = numeroEn(problemas, "dias_para_cerrar", ci.dias_para_cerrar, "cierre.dias_para_cerrar");
41494
+ if (problemas.length > 0) throw new OpsError("validacion", problemas.join("; "), { problemas });
41495
+ return {
41496
+ nombre,
41497
+ tipo,
41498
+ parametros,
41499
+ contacto: { canal, plantilla, max_intentos: maxIntentos, dias_entre_intentos: diasEntreIntentos, hora_desde: horaDesde, hora_hasta: horaHasta },
41500
+ cierre: { dias_para_cerrar: diasParaCerrar }
41501
+ };
41502
+ }
41503
+ function reglaParaContexto(r) {
41504
+ return {
41505
+ id: r.id,
41506
+ estado: r.estado,
41507
+ version: r.version,
41508
+ contacto: { canal: r.contacto.canal, max_intentos: r.contacto.max_intentos, dias_entre_intentos: r.contacto.dias_entre_intentos, hora_desde: r.contacto.hora_desde, hora_hasta: r.contacto.hora_hasta },
41509
+ cierre: { dias_para_cerrar: r.cierre.dias_para_cerrar }
41510
+ };
41511
+ }
41512
+ var COLS = "id, company_id, workflow, nombre, tipo, parametros, contacto, cierre, estado, version, historial, ultima_prueba, actor_tipo, actor_id, created_at, updated_at";
41513
+ function mapRegla(r) {
41514
+ const contacto = jsonb(r.contacto);
41515
+ const cierre = jsonb(r.cierre);
41516
+ const prueba = r.ultima_prueba ? jsonb(r.ultima_prueba) : null;
41517
+ return {
41518
+ id: String(r.id),
41519
+ companyId: String(r.company_id),
41520
+ workflow: String(r.workflow),
41521
+ nombre: String(r.nombre),
41522
+ tipo: String(r.tipo),
41523
+ parametros: jsonb(r.parametros),
41524
+ contacto: {
41525
+ canal: String(contacto.canal ?? "whatsapp"),
41526
+ plantilla: String(contacto.plantilla ?? ""),
41527
+ max_intentos: Number(contacto.max_intentos ?? LIMITES_REGLA.max_intentos[2]),
41528
+ dias_entre_intentos: Number(contacto.dias_entre_intentos ?? LIMITES_REGLA.dias_entre_intentos[2]),
41529
+ hora_desde: String(contacto.hora_desde ?? "09:00"),
41530
+ hora_hasta: String(contacto.hora_hasta ?? "19:00")
41531
+ },
41532
+ cierre: { dias_para_cerrar: Number(cierre.dias_para_cerrar ?? LIMITES_REGLA.dias_para_cerrar[2]) },
41533
+ estado: ESTADOS_REGLA.includes(String(r.estado)) ? String(r.estado) : "borrador",
41534
+ version: Number(r.version ?? 1),
41535
+ historial: jsonArr(r.historial).map((h) => {
41536
+ const x = jsonb(h);
41537
+ return { version: Number(x.version ?? 0), fecha: String(x.fecha ?? ""), actorTipo: String(x.actorTipo ?? ""), actorId: x.actorId ?? null, cambio: String(x.cambio ?? "") };
41538
+ }),
41539
+ ultimaPrueba: prueba && (prueba.estado === "pedida" || prueba.estado === "lista") ? {
41540
+ estado: prueba.estado,
41541
+ version: Number(prueba.version ?? 0),
41542
+ fecha: String(prueba.fecha ?? ""),
41543
+ ...prueba.alcanza !== void 0 ? { alcanza: Number(prueba.alcanza) } : {},
41544
+ ...Array.isArray(prueba.muestra) ? { muestra: prueba.muestra.map((m) => String(m)) } : {},
41545
+ ...prueba.consulta !== void 0 ? { consulta: prueba.consulta ?? null } : {},
41546
+ ...prueba.por !== void 0 ? { por: prueba.por ?? null } : {}
41547
+ } : null,
41548
+ actorTipo: String(r.actor_tipo),
41549
+ actorId: r.actor_id ?? null,
41550
+ createdAt: iso(r.created_at),
41551
+ updatedAt: iso(r.updated_at)
41552
+ };
41553
+ }
41554
+ async function listarReglas(ctx, companyId, filtro = {}) {
41555
+ const params = [companyId];
41556
+ let where = "company_id = $1";
41557
+ if (filtro.workflow) {
41558
+ params.push(filtro.workflow);
41559
+ where += ` AND workflow = $${params.length}`;
41560
+ }
41561
+ if (filtro.estado) {
41562
+ params.push(filtro.estado);
41563
+ where += ` AND estado = $${params.length}`;
41564
+ }
41565
+ const rows = await ctx.db.query(`SELECT ${COLS} FROM ${t(ctx, "regla")} WHERE ${where} ORDER BY created_at ASC`, params);
41566
+ return rows.map(mapRegla);
41567
+ }
41568
+ async function leerRegla(ctx, companyId, id) {
41569
+ const rows = await ctx.db.query(`SELECT ${COLS} FROM ${t(ctx, "regla")} WHERE company_id = $1 AND id = $2 LIMIT 1`, [companyId, id]);
41570
+ return rows[0] ? mapRegla(rows[0]) : null;
41571
+ }
41572
+ async function requerirRegla(ctx, companyId, id) {
41573
+ const r = await leerRegla(ctx, companyId, id);
41574
+ if (!r) throw new OpsError("regla_inexistente", `no existe la regla ${id} en esta instituci\xF3n`);
41575
+ return r;
41576
+ }
41577
+ function cambio(version2, actor, texto3) {
41578
+ return { version: version2, fecha: (/* @__PURE__ */ new Date()).toISOString(), actorTipo: actor.tipo, actorId: actor.id, cambio: texto3 };
41579
+ }
41580
+ function soloPersonas(actor, que) {
41581
+ if (actor.tipo !== "humano") throw new OpsError("actor_no_permitido", `${que} lo hace una persona del hospital desde el portal; el agente solo lee las reglas e informa la prueba`);
41582
+ }
41583
+ async function crearRegla(ctx, input2) {
41584
+ soloPersonas(input2.actor, "crear una regla");
41585
+ if (input2.workflow !== "demanda_pendiente") throw new OpsError("workflow_sin_reglas", `el puesto ${input2.workflow} no tiene reglas configurables`);
41586
+ const v = validarRegla(input2);
41587
+ const id = randomUUID();
41588
+ const historial = [cambio(1, input2.actor, "creada")];
41589
+ await ctx.db.execute(
41590
+ `INSERT INTO ${t(ctx, "regla")} (id, company_id, workflow, nombre, tipo, parametros, contacto, cierre, estado, version, historial, actor_tipo, actor_id) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8::jsonb, $9, $10, $11::jsonb, $12, $13)`,
41591
+ [id, input2.companyId, input2.workflow, v.nombre, v.tipo, JSON.stringify(v.parametros), JSON.stringify(v.contacto), JSON.stringify(v.cierre), "borrador", 1, JSON.stringify(historial), input2.actor.tipo, input2.actor.id]
41592
+ );
41593
+ await ctx.activity.log({ companyId: input2.companyId, message: `Autopilot Care: regla \xAB${v.nombre}\xBB creada (${v.tipo}, borrador)`, metadata: { reglaId: id, workflow: input2.workflow } });
41594
+ const now = (/* @__PURE__ */ new Date()).toISOString();
41595
+ return { id, companyId: input2.companyId, workflow: input2.workflow, ...v, estado: "borrador", version: 1, historial, ultimaPrueba: null, actorTipo: input2.actor.tipo, actorId: input2.actor.id, createdAt: now, updatedAt: now };
41596
+ }
41597
+ async function editarRegla(ctx, input2) {
41598
+ soloPersonas(input2.actor, "editar una regla");
41599
+ const actual = await requerirRegla(ctx, input2.companyId, input2.id);
41600
+ const v = validarRegla({
41601
+ nombre: input2.nombre ?? actual.nombre,
41602
+ tipo: input2.tipo ?? actual.tipo,
41603
+ parametros: input2.parametros ?? actual.parametros,
41604
+ contacto: input2.contacto ?? actual.contacto,
41605
+ cierre: input2.cierre ?? actual.cierre
41606
+ });
41607
+ const version2 = actual.version + 1;
41608
+ const estado = actual.estado === "activa" ? "pausada" : actual.estado;
41609
+ const detalle = `editada${input2.motivo ? `: ${str(input2.motivo)}` : ""}${actual.estado === "activa" ? " \xB7 pausada hasta probar y activar la versi\xF3n nueva" : ""}`;
41610
+ const historial = [...actual.historial, cambio(version2, input2.actor, detalle)];
41611
+ await ctx.db.execute(
41612
+ `UPDATE ${t(ctx, "regla")} SET nombre = $3, tipo = $4, parametros = $5::jsonb, contacto = $6::jsonb, cierre = $7::jsonb, estado = $8, version = $9, historial = $10::jsonb, actor_tipo = $11, actor_id = $12, updated_at = now() WHERE company_id = $1 AND id = $2`,
41613
+ [input2.companyId, input2.id, v.nombre, v.tipo, JSON.stringify(v.parametros), JSON.stringify(v.contacto), JSON.stringify(v.cierre), estado, version2, JSON.stringify(historial), input2.actor.tipo, input2.actor.id]
41614
+ );
41615
+ await ctx.activity.log({ companyId: input2.companyId, message: `Autopilot Care: regla \xAB${v.nombre}\xBB editada (versi\xF3n ${version2}, ${estado})`, metadata: { reglaId: input2.id } });
41616
+ return { ...actual, ...v, estado, version: version2, historial, actorTipo: input2.actor.tipo, actorId: input2.actor.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
41617
+ }
41618
+ async function cambiarEstadoRegla(ctx, input2) {
41619
+ soloPersonas(input2.actor, "activar o pausar una regla");
41620
+ const actual = await requerirRegla(ctx, input2.companyId, input2.id);
41621
+ const estado = str(input2.estado);
41622
+ if (estado !== "activa" && estado !== "pausada") throw new OpsError("estado_invalido", "estado debe ser activa o pausada");
41623
+ if (estado === actual.estado) return actual;
41624
+ if (estado === "activa") {
41625
+ const prueba = actual.ultimaPrueba;
41626
+ if (!prueba || prueba.estado !== "lista" || prueba.version !== actual.version) {
41627
+ throw new OpsError("prueba_requerida", `la versi\xF3n ${actual.version} de \xAB${actual.nombre}\xBB no se prob\xF3 todav\xEDa: pedir la prueba y activar cuando llegue el resultado`);
41628
+ }
41629
+ }
41630
+ if (estado === "pausada" && actual.estado !== "activa") throw new OpsError("estado_invalido", `una regla en ${actual.estado} no se pausa`);
41631
+ const historial = [...actual.historial, cambio(actual.version, input2.actor, `${estado}${input2.motivo ? `: ${str(input2.motivo)}` : ""}`)];
41632
+ await ctx.db.execute(
41633
+ `UPDATE ${t(ctx, "regla")} SET estado = $3, historial = $4::jsonb, actor_tipo = $5, actor_id = $6, updated_at = now() WHERE company_id = $1 AND id = $2`,
41634
+ [input2.companyId, input2.id, estado, JSON.stringify(historial), input2.actor.tipo, input2.actor.id]
41635
+ );
41636
+ await ctx.activity.log({ companyId: input2.companyId, message: `Autopilot Care: regla \xAB${actual.nombre}\xBB ${estado} (versi\xF3n ${actual.version})`, metadata: { reglaId: input2.id } });
41637
+ return { ...actual, estado, historial, actorTipo: input2.actor.tipo, actorId: input2.actor.id, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
41638
+ }
41639
+ async function pedirPruebaRegla(ctx, input2) {
41640
+ soloPersonas(input2.actor, "pedir la prueba de una regla");
41641
+ const actual = await requerirRegla(ctx, input2.companyId, input2.id);
41642
+ const prueba = { estado: "pedida", version: actual.version, fecha: (/* @__PURE__ */ new Date()).toISOString() };
41643
+ await ctx.db.execute(`UPDATE ${t(ctx, "regla")} SET ultima_prueba = $3::jsonb, updated_at = now() WHERE company_id = $1 AND id = $2`, [input2.companyId, input2.id, JSON.stringify(prueba)]);
41644
+ return { ...actual, ultimaPrueba: prueba, updatedAt: prueba.fecha };
41645
+ }
41646
+ var REF_MUESTRA = /^(orden|solicitud|control):[A-Za-z0-9:_-]{1,80}$/;
41647
+ async function informarPruebaRegla(ctx, input2) {
41648
+ const actual = await requerirRegla(ctx, input2.companyId, input2.id);
41649
+ const alcanza = entero(input2.alcanza);
41650
+ if (alcanza === null || alcanza < 0) throw new OpsError("validacion", "alcanza: entero mayor o igual a 0 (pacientes que la regla alcanza hoy)");
41651
+ const crudos = Array.isArray(input2.muestra) ? input2.muestra : [];
41652
+ if (crudos.length > 10) throw new OpsError("validacion", "muestra: como m\xE1ximo 10 referencias");
41653
+ const muestra = crudos.map((m) => String(m).trim());
41654
+ const mala = muestra.find((m) => !REF_MUESTRA.test(m));
41655
+ if (mala) throw new OpsError("validacion", `muestra: solo referencias orden:<id>, solicitud:<id> o control:<persona>:<regla>; \xAB${mala.slice(0, 40)}\xBB no lo es (sin datos personales)`);
41656
+ const prueba = {
41657
+ estado: "lista",
41658
+ version: actual.version,
41659
+ fecha: (/* @__PURE__ */ new Date()).toISOString(),
41660
+ alcanza,
41661
+ muestra,
41662
+ consulta: str(input2.consulta)?.slice(0, 4e3) ?? null,
41663
+ por: input2.actor.id
41664
+ };
41665
+ await ctx.db.execute(`UPDATE ${t(ctx, "regla")} SET ultima_prueba = $3::jsonb, updated_at = now() WHERE company_id = $1 AND id = $2`, [input2.companyId, input2.id, JSON.stringify(prueba)]);
41666
+ await ctx.activity.log({ companyId: input2.companyId, message: `Autopilot Care: prueba de la regla \xAB${actual.nombre}\xBB (versi\xF3n ${actual.version}): alcanza a ${alcanza}`, metadata: { reglaId: input2.id, alcanza } });
41667
+ return { ...actual, ultimaPrueba: prueba, updatedAt: prueba.fecha };
41668
+ }
41669
+
41129
41670
  // src/ops.ts
41130
41671
  var OpsError = class extends Error {
41131
41672
  constructor(codigo, motivo, detalle) {
@@ -41140,18 +41681,18 @@ var OpsError = class extends Error {
41140
41681
  /** Mensaje sin el prefijo de código. */
41141
41682
  motivo;
41142
41683
  };
41143
- function t(ctx, table) {
41684
+ function t2(ctx, table) {
41144
41685
  return `${ctx.db.namespace}.${table}`;
41145
41686
  }
41146
- function str(v) {
41687
+ function str2(v) {
41147
41688
  return typeof v === "string" && v.trim().length > 0 ? v.trim() : null;
41148
41689
  }
41149
- function iso(v) {
41690
+ function iso2(v) {
41150
41691
  if (v instanceof Date) return v.toISOString();
41151
41692
  if (typeof v === "string") return v;
41152
41693
  return "";
41153
41694
  }
41154
- function jsonb(v) {
41695
+ function jsonb2(v) {
41155
41696
  if (v && typeof v === "object" && !Array.isArray(v)) return v;
41156
41697
  if (typeof v === "string") {
41157
41698
  try {
@@ -41163,7 +41704,7 @@ function jsonb(v) {
41163
41704
  }
41164
41705
  return {};
41165
41706
  }
41166
- function jsonArr(v) {
41707
+ function jsonArr2(v) {
41167
41708
  if (Array.isArray(v)) return v;
41168
41709
  if (typeof v === "string") {
41169
41710
  try {
@@ -41183,15 +41724,15 @@ function mapCaso(r) {
41183
41724
  estado: String(r.estado),
41184
41725
  referencia: r.referencia ?? null,
41185
41726
  titulo: String(r.titulo),
41186
- datos: jsonb(r.datos),
41727
+ datos: jsonb2(r.datos),
41187
41728
  abiertoPorTipo: String(r.abierto_por_tipo),
41188
41729
  abiertoPorId: r.abierto_por_id ?? null,
41189
41730
  agenteId: r.agente_id ?? null,
41190
41731
  issueId: r.issue_id ?? null,
41191
41732
  minutosAbsorbidos: Number(r.minutos_absorbidos ?? 0),
41192
- createdAt: iso(r.created_at),
41193
- updatedAt: iso(r.updated_at),
41194
- cerradoAt: r.cerrado_at ? iso(r.cerrado_at) : null
41733
+ createdAt: iso2(r.created_at),
41734
+ updatedAt: iso2(r.updated_at),
41735
+ cerradoAt: r.cerrado_at ? iso2(r.cerrado_at) : null
41195
41736
  };
41196
41737
  }
41197
41738
  function mapTransicion(r) {
@@ -41204,9 +41745,9 @@ function mapTransicion(r) {
41204
41745
  actorTipo: String(r.actor_tipo),
41205
41746
  actorId: r.actor_id ?? null,
41206
41747
  motivo: r.motivo ?? null,
41207
- datos: jsonb(r.datos),
41208
- validaciones: jsonArr(r.validaciones),
41209
- createdAt: iso(r.created_at)
41748
+ datos: jsonb2(r.datos),
41749
+ validaciones: jsonArr2(r.validaciones),
41750
+ createdAt: iso2(r.created_at)
41210
41751
  };
41211
41752
  }
41212
41753
  function mapAccion(r) {
@@ -41217,17 +41758,17 @@ function mapAccion(r) {
41217
41758
  conector: String(r.conector),
41218
41759
  objetivo: String(r.objetivo),
41219
41760
  resultado: String(r.resultado),
41220
- evidencia: jsonb(r.evidencia),
41761
+ evidencia: jsonb2(r.evidencia),
41221
41762
  detalle: r.detalle ?? null,
41222
41763
  actorTipo: String(r.actor_tipo),
41223
41764
  actorId: r.actor_id ?? null,
41224
- createdAt: iso(r.created_at)
41765
+ createdAt: iso2(r.created_at)
41225
41766
  };
41226
41767
  }
41227
41768
  var CASO_COLS = "id, company_id, workflow, estado, referencia, titulo, datos, abierto_por_tipo, abierto_por_id, agente_id, issue_id, minutos_absorbidos, created_at, updated_at, cerrado_at";
41228
41769
  async function obtenerModo(ctx, companyId, workflow) {
41229
41770
  const rows = await ctx.db.query(
41230
- `SELECT modo FROM ${t(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
41771
+ `SELECT modo FROM ${t2(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
41231
41772
  [companyId, workflow]
41232
41773
  );
41233
41774
  const modo = rows[0]?.modo;
@@ -41235,14 +41776,14 @@ async function obtenerModo(ctx, companyId, workflow) {
41235
41776
  }
41236
41777
  async function obtenerOpciones(ctx, companyId, workflow) {
41237
41778
  const rows = await ctx.db.query(
41238
- `SELECT opciones FROM ${t(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
41779
+ `SELECT opciones FROM ${t2(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
41239
41780
  [companyId, workflow]
41240
41781
  );
41241
- return jsonb(rows[0]?.opciones);
41782
+ return jsonb2(rows[0]?.opciones);
41242
41783
  }
41243
41784
  async function listarPoliticas(ctx, companyId) {
41244
41785
  const rows = await ctx.db.query(
41245
- `SELECT workflow, modo, motivo, opciones, updated_at FROM ${t(ctx, "politica")} WHERE company_id = $1`,
41786
+ `SELECT workflow, modo, motivo, opciones, updated_at FROM ${t2(ctx, "politica")} WHERE company_id = $1`,
41246
41787
  [companyId]
41247
41788
  );
41248
41789
  const porWorkflow = new Map(rows.map((r) => [String(r.workflow), r]));
@@ -41254,9 +41795,9 @@ async function listarPoliticas(ctx, companyId) {
41254
41795
  puesto: wf.puesto,
41255
41796
  modo: r ? String(r.modo) : MODO_POR_DEFECTO,
41256
41797
  motivo: r ? r.motivo ?? null : null,
41257
- actualizadaEn: r ? iso(r.updated_at) : null,
41798
+ actualizadaEn: r ? iso2(r.updated_at) : null,
41258
41799
  porDefecto: !r,
41259
- opciones: r ? jsonb(r.opciones) : {}
41800
+ opciones: r ? jsonb2(r.opciones) : {}
41260
41801
  };
41261
41802
  });
41262
41803
  }
@@ -41290,23 +41831,23 @@ async function cambiarPolitica(ctx, input2) {
41290
41831
  const cambioOpciones = describirOpciones(opcionesAntes, opciones);
41291
41832
  const motivo = [input2.motivo ?? null, cambioOpciones].filter(Boolean).join(" \xB7 ") || null;
41292
41833
  const existe = await ctx.db.query(
41293
- `SELECT id FROM ${t(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
41834
+ `SELECT id FROM ${t2(ctx, "politica")} WHERE company_id = $1 AND workflow = $2 LIMIT 1`,
41294
41835
  [input2.companyId, input2.workflow]
41295
41836
  );
41296
41837
  if (existe.length > 0) {
41297
41838
  await ctx.db.execute(
41298
- `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`,
41839
+ `UPDATE ${t2(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`,
41299
41840
  [input2.companyId, input2.workflow, input2.modo, motivo, input2.actor.tipo, input2.actor.id, JSON.stringify(opciones)]
41300
41841
  );
41301
41842
  } else {
41302
41843
  await ctx.db.execute(
41303
- `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)`,
41304
- [randomUUID(), input2.companyId, input2.workflow, input2.modo, motivo, input2.actor.tipo, input2.actor.id, JSON.stringify(opciones)]
41844
+ `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)`,
41845
+ [randomUUID2(), input2.companyId, input2.workflow, input2.modo, motivo, input2.actor.tipo, input2.actor.id, JSON.stringify(opciones)]
41305
41846
  );
41306
41847
  }
41307
41848
  await ctx.db.execute(
41308
- `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)`,
41309
- [randomUUID(), input2.companyId, input2.workflow, anterior, input2.modo, motivo, input2.actor.tipo, input2.actor.id]
41849
+ `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)`,
41850
+ [randomUUID2(), input2.companyId, input2.workflow, anterior, input2.modo, motivo, input2.actor.tipo, input2.actor.id]
41310
41851
  );
41311
41852
  await ctx.activity.log({
41312
41853
  companyId: input2.companyId,
@@ -41318,7 +41859,7 @@ async function cambiarPolitica(ctx, input2) {
41318
41859
  }
41319
41860
  async function leerCaso(ctx, companyId, casoId) {
41320
41861
  const rows = await ctx.db.query(
41321
- `SELECT ${CASO_COLS} FROM ${t(ctx, "caso")} WHERE company_id = $1 AND id = $2 LIMIT 1`,
41862
+ `SELECT ${CASO_COLS} FROM ${t2(ctx, "caso")} WHERE company_id = $1 AND id = $2 LIMIT 1`,
41322
41863
  [companyId, casoId]
41323
41864
  );
41324
41865
  return rows[0] ? mapCaso(rows[0]) : null;
@@ -41328,11 +41869,11 @@ async function leerCasoCompleto(ctx, companyId, casoId) {
41328
41869
  if (!caso) return null;
41329
41870
  const [transiciones, acciones] = await Promise.all([
41330
41871
  ctx.db.query(
41331
- `SELECT id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, motivo, datos, validaciones, created_at FROM ${t(ctx, "transicion")} WHERE company_id = $1 AND caso_id = $2 ORDER BY created_at ASC`,
41872
+ `SELECT id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, motivo, datos, validaciones, created_at FROM ${t2(ctx, "transicion")} WHERE company_id = $1 AND caso_id = $2 ORDER BY created_at ASC`,
41332
41873
  [companyId, casoId]
41333
41874
  ),
41334
41875
  ctx.db.query(
41335
- `SELECT id, caso_id, tipo, conector, objetivo, resultado, evidencia, detalle, actor_tipo, actor_id, created_at FROM ${t(ctx, "accion")} WHERE company_id = $1 AND caso_id = $2 ORDER BY created_at ASC`,
41876
+ `SELECT id, caso_id, tipo, conector, objetivo, resultado, evidencia, detalle, actor_tipo, actor_id, created_at FROM ${t2(ctx, "accion")} WHERE company_id = $1 AND caso_id = $2 ORDER BY created_at ASC`,
41336
41877
  [companyId, casoId]
41337
41878
  )
41338
41879
  ]);
@@ -41363,7 +41904,7 @@ async function listarCasos(ctx, input2) {
41363
41904
  const limit = Math.min(Math.max(Number(input2.limit ?? 50) || 50, 1), 100);
41364
41905
  params.push(limit);
41365
41906
  const rows = await ctx.db.query(
41366
- `SELECT ${CASO_COLS} FROM ${t(ctx, "caso")} WHERE ${where.join(" AND ")} ORDER BY updated_at DESC LIMIT $${params.length}`,
41907
+ `SELECT ${CASO_COLS} FROM ${t2(ctx, "caso")} WHERE ${where.join(" AND ")} ORDER BY updated_at DESC LIMIT $${params.length}`,
41367
41908
  params
41368
41909
  );
41369
41910
  return rows.map(mapCaso);
@@ -41371,21 +41912,21 @@ async function listarCasos(ctx, input2) {
41371
41912
  async function abrirCaso(ctx, input2) {
41372
41913
  const wf = getWorkflow(input2.workflow);
41373
41914
  if (!wf) throw new OpsError("workflow_desconocido", `workflow desconocido: ${input2.workflow}. Disponibles: ${listarWorkflows().map((w) => w.clave).join(", ")}`);
41374
- const titulo = str(input2.titulo);
41915
+ const titulo = str2(input2.titulo);
41375
41916
  if (!titulo) throw new OpsError("titulo_requerido", "titulo es obligatorio");
41376
41917
  if (input2.actor.tipo === "agente") {
41377
41918
  const modo = await obtenerModo(ctx, input2.companyId, input2.workflow);
41378
41919
  if (modo === "corte") throw new OpsError("politica", `el workflow ${input2.workflow} est\xE1 cortado para agentes en esta instituci\xF3n`);
41379
41920
  }
41380
- const idem = str(input2.idempotencyKey);
41921
+ const idem = str2(input2.idempotencyKey);
41381
41922
  if (idem) {
41382
41923
  const prev = await ctx.db.query(
41383
- `SELECT ${CASO_COLS} FROM ${t(ctx, "caso")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`,
41924
+ `SELECT ${CASO_COLS} FROM ${t2(ctx, "caso")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`,
41384
41925
  [input2.companyId, idem]
41385
41926
  );
41386
41927
  if (prev[0]) return { caso: mapCaso(prev[0]), creado: false };
41387
41928
  }
41388
- const referencia = str(input2.referencia);
41929
+ const referencia = str2(input2.referencia);
41389
41930
  if (wf.clave === "cuenta_completa" || wf.clave === "facturacion_pendiente") {
41390
41931
  const bono = refExterna(input2.datos?.bono_id);
41391
41932
  if (!referencia || !/^bono:\S+$/.test(referencia)) throw new OpsError("referencia_requerida", `en ${wf.clave} la referencia es obligatoria: bono:<n\xFAmero de bono>`);
@@ -41396,17 +41937,36 @@ async function abrirCaso(ctx, input2) {
41396
41937
  if (!referencia || !/^profesional:\S+$/.test(referencia)) throw new OpsError("referencia_requerida", "en credencial_profesional la referencia es obligatoria: profesional:<id del profesional>");
41397
41938
  if (prof && referencia !== `profesional:${prof}`) throw new OpsError("referencia_requerida", `la referencia ${referencia} no coincide con el profesional_id ${prof}`);
41398
41939
  }
41940
+ const extra = {};
41941
+ if (wf.clave === "demanda_pendiente") {
41942
+ if (!referencia || !/^(orden|solicitud|control):\S+$/.test(referencia)) {
41943
+ throw new OpsError("referencia_requerida", "en demanda_pendiente la referencia es obligatoria: orden:<id>, solicitud:<id> o control:<persona_id>:<regla_id>");
41944
+ }
41945
+ const reglaId = str2(input2.datos?.regla_id);
41946
+ if (!reglaId) throw new OpsError("regla_requerida", "en demanda_pendiente datos.regla_id es obligatorio: la regla activa que detect\xF3 el caso");
41947
+ const regla = await leerRegla(ctx, input2.companyId, reglaId);
41948
+ if (!regla || regla.workflow !== wf.clave) throw new OpsError("regla_inexistente", `no existe la regla ${reglaId} en esta instituci\xF3n`);
41949
+ if (regla.estado !== "activa") throw new OpsError("regla_inactiva", `la regla \xAB${regla.nombre}\xBB est\xE1 ${regla.estado}: no abre casos hasta que se active`);
41950
+ const { hoy } = ahoraEnHospital();
41951
+ extra.regla_nombre = regla.nombre;
41952
+ extra.regla_version = regla.version;
41953
+ extra.tipo = regla.tipo;
41954
+ extra.canal = regla.contacto.canal;
41955
+ extra.intentos = 0;
41956
+ extra.vence_el = sumarDias(hoy, regla.cierre.dias_para_cerrar);
41957
+ extra.detectado_el = hoy;
41958
+ }
41399
41959
  if (referencia) {
41400
41960
  const abiertos = await ctx.db.query(
41401
- `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`,
41961
+ `SELECT ${CASO_COLS} FROM ${t2(ctx, "caso")} WHERE company_id = $1 AND workflow = $2 AND referencia = $3 AND cerrado_at IS NULL ORDER BY created_at DESC LIMIT 1`,
41402
41962
  [input2.companyId, input2.workflow, referencia]
41403
41963
  );
41404
41964
  if (abiertos[0]) return { caso: mapCaso(abiertos[0]), creado: false };
41405
41965
  }
41406
- const id = randomUUID();
41407
- const datos = input2.datos ?? {};
41966
+ const id = randomUUID2();
41967
+ const datos = { ...input2.datos ?? {}, ...extra };
41408
41968
  await ctx.db.execute(
41409
- `INSERT INTO ${t(ctx, "caso")} (id, company_id, workflow, estado, referencia, titulo, datos, abierto_por_tipo, abierto_por_id, agente_id, issue_id, idempotency_key) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10, $11, $12)`,
41969
+ `INSERT INTO ${t2(ctx, "caso")} (id, company_id, workflow, estado, referencia, titulo, datos, abierto_por_tipo, abierto_por_id, agente_id, issue_id, idempotency_key) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10, $11, $12)`,
41410
41970
  [id, input2.companyId, wf.clave, wf.inicial, referencia, titulo, JSON.stringify(datos), input2.actor.tipo, input2.actor.id, input2.agenteId ?? null, input2.issueId ?? null, idem]
41411
41971
  );
41412
41972
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -41445,11 +42005,24 @@ async function contextoDe(ctx, companyId, caso, via, datos) {
41445
42005
  contexto.plazosPresentacion = leerPlazos((await obtenerOpciones(ctx, companyId, caso.workflow)).plazos);
41446
42006
  return contexto;
41447
42007
  }
42008
+ if (caso.workflow === "demanda_pendiente") {
42009
+ const reglaId = str2(caso.datos.regla_id);
42010
+ const regla = reglaId ? await leerRegla(ctx, companyId, reglaId) : null;
42011
+ contexto.regla = regla ? reglaParaContexto(regla) : null;
42012
+ const contactos = (await ctx.db.query(
42013
+ `SELECT caso_id, via, created_at FROM ${t2(ctx, "transicion")} WHERE company_id = $1 AND caso_id = $2 AND via = $3 ORDER BY created_at DESC LIMIT 50`,
42014
+ [companyId, caso.id, "contactar"]
42015
+ )).filter((r) => String(r.caso_id) === caso.id && String(r.via) === "contactar");
42016
+ contexto.intentosContacto = contactos.length;
42017
+ contexto.ultimoContacto = contactos[0]?.created_at ? ahoraEnHospital(new Date(iso2(contactos[0].created_at))).hoy : null;
42018
+ contexto.diasAbierto = caso.createdAt ? Math.max(0, diasEntre(ahoraEnHospital(new Date(caso.createdAt)).hoy, hoy) ?? 0) : 0;
42019
+ return contexto;
42020
+ }
41448
42021
  if (caso.workflow !== "recupero_turno") return contexto;
41449
42022
  contexto.ofrecerPrimero = (await obtenerOpciones(ctx, companyId, caso.workflow)).ofrecer_primero === true;
41450
42023
  if (via !== "proponer") return contexto;
41451
42024
  const otros = await ctx.db.query(
41452
- `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`,
42025
+ `SELECT id, datos->>'solicitud_id' AS solicitud_id FROM ${t2(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`,
41453
42026
  [companyId, "recupero_turno", caso.id, "sin_candidato", "rechazado", "cancelado"]
41454
42027
  );
41455
42028
  contexto.solicitudesEnCurso = [...new Set(otros.map((r) => String(r.solicitud_id)))];
@@ -41457,7 +42030,7 @@ async function contextoDe(ctx, companyId, caso, via, datos) {
41457
42030
  const mismo = solicitud ? otros.find((r) => String(r.solicitud_id) === solicitud) : void 0;
41458
42031
  contexto.solicitudEnOtroCaso = mismo ? String(mismo.id) : null;
41459
42032
  const asignados = await ctx.db.query(
41460
- `SELECT count(*)::int AS cantidad FROM ${t(ctx, "transicion")} WHERE company_id = $1 AND via = $2 AND created_at >= $3`,
42033
+ `SELECT count(*)::int AS cantidad FROM ${t2(ctx, "transicion")} WHERE company_id = $1 AND via = $2 AND created_at >= $3`,
41461
42034
  [companyId, "asignar", inicioDelDia.toISOString()]
41462
42035
  );
41463
42036
  contexto.asignacionesHoy = Number(asignados[0]?.cantidad ?? 0);
@@ -41466,10 +42039,10 @@ async function contextoDe(ctx, companyId, caso, via, datos) {
41466
42039
  async function transicionar(ctx, input2) {
41467
42040
  const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
41468
42041
  if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
41469
- const idem = str(input2.idempotencyKey);
42042
+ const idem = str2(input2.idempotencyKey);
41470
42043
  if (idem) {
41471
42044
  const prev = await ctx.db.query(
41472
- `SELECT id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, motivo, datos, validaciones, created_at FROM ${t(ctx, "transicion")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`,
42045
+ `SELECT id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, motivo, datos, validaciones, created_at FROM ${t2(ctx, "transicion")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`,
41473
42046
  [input2.companyId, idem]
41474
42047
  );
41475
42048
  if (prev[0]) return { caso, transicion: mapTransicion(prev[0]), repetida: true };
@@ -41482,10 +42055,10 @@ async function transicionar(ctx, input2) {
41482
42055
  if (!resultado.ok) {
41483
42056
  throw new OpsError(resultado.codigo, resultado.motivo, { validaciones: resultado.validaciones, estado: caso.estado });
41484
42057
  }
41485
- const accionId = str(datos.accion_id);
42058
+ const accionId = str2(datos.accion_id);
41486
42059
  if (accionId) {
41487
42060
  const rows = await ctx.db.query(
41488
- `SELECT id, caso_id, tipo, resultado FROM ${t(ctx, "accion")} WHERE company_id = $1 AND id = $2 LIMIT 1`,
42061
+ `SELECT id, caso_id, tipo, resultado FROM ${t2(ctx, "accion")} WHERE company_id = $1 AND id = $2 LIMIT 1`,
41489
42062
  [input2.companyId, accionId]
41490
42063
  );
41491
42064
  const accion = rows[0];
@@ -41494,21 +42067,21 @@ async function transicionar(ctx, input2) {
41494
42067
  throw new OpsError("validacion", `accion_registrada: ${problema}`, { validaciones: [{ regla: "accion_registrada", ok: false, detalle: problema }], estado: caso.estado });
41495
42068
  }
41496
42069
  }
41497
- const lecturaId = str(datos.lectura_id);
42070
+ const lecturaId = str2(datos.lectura_id);
41498
42071
  if (lecturaId) {
41499
42072
  const rows = await ctx.db.query(
41500
- `SELECT id, caso_id, tipo, resultado, created_at FROM ${t(ctx, "accion")} WHERE company_id = $1 AND id = $2 LIMIT 1`,
42073
+ `SELECT id, caso_id, tipo, resultado, created_at FROM ${t2(ctx, "accion")} WHERE company_id = $1 AND id = $2 LIMIT 1`,
41501
42074
  [input2.companyId, lecturaId]
41502
42075
  );
41503
42076
  const lectura = rows[0];
41504
42077
  let vieja = false;
41505
42078
  if (lectura && resultado.via === "comprobar") {
41506
42079
  const ultima = await ctx.db.query(
41507
- `SELECT max(created_at) AS ultima FROM ${t(ctx, "transicion")} WHERE company_id = $1 AND caso_id = $2`,
42080
+ `SELECT max(created_at) AS ultima FROM ${t2(ctx, "transicion")} WHERE company_id = $1 AND caso_id = $2`,
41508
42081
  [input2.companyId, caso.id]
41509
42082
  );
41510
- const desde = ultima[0]?.ultima ? Date.parse(iso(ultima[0].ultima)) : NaN;
41511
- vieja = Number.isFinite(desde) && Date.parse(iso(lectura.created_at)) <= desde;
42083
+ const desde = ultima[0]?.ultima ? Date.parse(iso2(ultima[0].ultima)) : NaN;
42084
+ vieja = Number.isFinite(desde) && Date.parse(iso2(lectura.created_at)) <= desde;
41512
42085
  }
41513
42086
  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;
41514
42087
  if (problema) {
@@ -41535,6 +42108,23 @@ async function transicionar(ctx, input2) {
41535
42108
  const fecha = String(datos.fecha_prestacion ?? caso.datos.fecha_prestacion ?? "");
41536
42109
  Object.assign(datos, calcularVencimiento(fecha, datos.obra_social ?? caso.datos.obra_social, contexto));
41537
42110
  }
42111
+ if (caso.workflow === "demanda_pendiente") {
42112
+ delete datos.intentos;
42113
+ delete datos.proximo_contacto;
42114
+ if (resultado.via === "contactar") {
42115
+ const hechos = (contexto.intentosContacto ?? 0) + 1;
42116
+ const regla = contexto.regla ?? null;
42117
+ datos.intentos = hechos;
42118
+ datos.ultimo_contacto = contexto.hoy ?? null;
42119
+ datos.canal = canalContacto(datos.canal);
42120
+ datos.proximo_contacto = regla && hechos < regla.contacto.max_intentos && contexto.hoy ? sumarDias(contexto.hoy, regla.contacto.dias_entre_intentos) : null;
42121
+ }
42122
+ if (resultado.via === "responder") {
42123
+ datos.respuesta = respuestaContacto(datos.respuesta);
42124
+ datos.respondio_el = contexto.hoy ?? null;
42125
+ }
42126
+ if (resultado.via === "comprobar") datos.turno_encontrado_el = contexto.hoy ?? null;
42127
+ }
41538
42128
  if (caso.workflow === "recupero_turno" && resultado.via === "no_acepta") {
41539
42129
  const previas = Array.isArray(caso.datos.declinaron) ? caso.datos.declinaron.map((v) => refExterna(v)).filter((v) => v !== null) : [];
41540
42130
  const esta = refExterna(caso.datos.solicitud_id);
@@ -41551,18 +42141,18 @@ async function transicionar(ctx, input2) {
41551
42141
  datos.largo_cola = total;
41552
42142
  const cola = leerCola(datos.cola);
41553
42143
  const elegida = cola?.find((e) => e.solicitud_id === refExterna(datos.solicitud_id));
41554
- if (elegida?.prioridad && !str(datos.solicitud_prioridad)) datos.solicitud_prioridad = elegida.prioridad;
42144
+ if (elegida?.prioridad && !str2(datos.solicitud_prioridad)) datos.solicitud_prioridad = elegida.prioridad;
41555
42145
  if (cola) datos.cola = ordenarCola(cola).map((e) => enCurso.has(e.solicitud_id) && e.solicitud_id !== elegida?.solicitud_id ? { ...e, en_otro_recupero: true } : e);
41556
42146
  }
41557
- const transicionId = randomUUID();
42147
+ const transicionId = randomUUID2();
41558
42148
  await ctx.db.execute(
41559
- `INSERT INTO ${t(ctx, "transicion")} (id, company_id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, run_id, motivo, datos, validaciones, idempotency_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12::jsonb, $13)`,
42149
+ `INSERT INTO ${t2(ctx, "transicion")} (id, company_id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, run_id, motivo, datos, validaciones, idempotency_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12::jsonb, $13)`,
41560
42150
  [transicionId, input2.companyId, caso.id, resultado.de, resultado.a, resultado.via, input2.actor.tipo, input2.actor.id, input2.actor.runId ?? null, resultado.autoaprobado ? input2.motivo ?? "Aprobado autom\xE1ticamente: pol\xEDtica aut\xF3noma, sin excepciones" : input2.motivo ?? null, JSON.stringify(resultado.autoaprobado ? { ...datos, autoaprobado: true } : datos), JSON.stringify(resultado.validaciones), idem]
41561
42151
  );
41562
42152
  const nuevosDatos = { ...caso.datos, ...datos };
41563
42153
  delete nuevosDatos.motivo;
41564
42154
  await ctx.db.execute(
41565
- `UPDATE ${t(ctx, "caso")} SET estado = $3, datos = $4::jsonb, minutos_absorbidos = minutos_absorbidos + $5::int, cerrado_at = CASE WHEN $6::boolean THEN now() ELSE NULL END, updated_at = now() WHERE company_id = $1 AND id = $2`,
42155
+ `UPDATE ${t2(ctx, "caso")} SET estado = $3, datos = $4::jsonb, minutos_absorbidos = minutos_absorbidos + $5::int, cerrado_at = CASE WHEN $6::boolean THEN now() ELSE NULL END, updated_at = now() WHERE company_id = $1 AND id = $2`,
41566
42156
  [input2.companyId, caso.id, resultado.a, JSON.stringify(nuevosDatos), resultado.minutosAbsorbidos, resultado.esFinal]
41567
42157
  );
41568
42158
  await ctx.activity.log({
@@ -41598,7 +42188,7 @@ async function registrarAccion(ctx, input2) {
41598
42188
  if (!TIPOS_ACCION.includes(input2.tipo)) throw new OpsError("tipo_invalido", `tipo debe ser: ${TIPOS_ACCION.join(", ")}`);
41599
42189
  if (!CONECTORES.includes(input2.conector)) throw new OpsError("conector_invalido", `conector debe ser: ${CONECTORES.join(", ")}`);
41600
42190
  if (!RESULTADOS_ACCION.includes(input2.resultado)) throw new OpsError("resultado_invalido", `resultado debe ser: ${RESULTADOS_ACCION.join(", ")}`);
41601
- const objetivo = str(input2.objetivo);
42191
+ const objetivo = str2(input2.objetivo);
41602
42192
  if (!objetivo) throw new OpsError("objetivo_requerido", "objetivo es obligatorio (qu\xE9 pantalla, endpoint o archivo se toc\xF3)");
41603
42193
  if (WORKFLOWS_SOLO_LECTURA.has(caso.workflow) && input2.tipo === "escritura" && input2.actor.tipo === "agente") {
41604
42194
  throw new OpsError("politica", `en ${caso.workflow} el puesto solo lee: no registra escrituras sobre el sistema del hospital; eso lo hace el equipo`);
@@ -41607,18 +42197,18 @@ async function registrarAccion(ctx, input2) {
41607
42197
  const modo = await obtenerModo(ctx, input2.companyId, caso.workflow);
41608
42198
  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`);
41609
42199
  }
41610
- const id = randomUUID();
42200
+ const id = randomUUID2();
41611
42201
  const evidencia = { ...input2.evidencia ?? {} };
41612
42202
  delete evidencia.fuera_de_flujo;
41613
42203
  if (caso.workflow === "recupero_turno" && input2.tipo === "escritura" && input2.actor.tipo === "agente" && caso.estado !== "asignando") {
41614
42204
  evidencia.fuera_de_flujo = true;
41615
42205
  }
41616
42206
  await ctx.db.execute(
41617
- `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)`,
42207
+ `INSERT INTO ${t2(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)`,
41618
42208
  [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]
41619
42209
  );
41620
42210
  await ctx.db.execute(
41621
- `UPDATE ${t(ctx, "caso")} SET updated_at = now() WHERE company_id = $1 AND id = $2`,
42211
+ `UPDATE ${t2(ctx, "caso")} SET updated_at = now() WHERE company_id = $1 AND id = $2`,
41622
42212
  [input2.companyId, caso.id]
41623
42213
  );
41624
42214
  return {
@@ -41642,10 +42232,10 @@ function numero(v) {
41642
42232
  }
41643
42233
  function motivoSinCarga(caso) {
41644
42234
  const wf = getWorkflow(caso.workflow);
41645
- if (!wf || !wf.transiciones.some((t3) => t3.cargaManual)) return `el workflow ${caso.workflow} no tiene carga manual en el sistema de gesti\xF3n`;
42235
+ if (!wf || !wf.transiciones.some((t4) => t4.cargaManual)) return `el workflow ${caso.workflow} no tiene carga manual en el sistema de gesti\xF3n`;
41646
42236
  if (wf.finalesOk.includes(caso.estado)) return "ya est\xE1 cargado y comprobado";
41647
42237
  if (wf.finalesNoOk.includes(caso.estado)) return `est\xE1 ${caso.estado}: no se carga`;
41648
- if (wf.transiciones.some((t3) => t3.cargaManual && t3.a === caso.estado)) return "ya figura como cargado; falta la comprobaci\xF3n del puesto";
42238
+ if (wf.transiciones.some((t4) => t4.cargaManual && t4.a === caso.estado)) return "ya figura como cargado; falta la comprobaci\xF3n del puesto";
41649
42239
  return "todav\xEDa no est\xE1 aprobado: no hay nada para cargar";
41650
42240
  }
41651
42241
  async function registrarCargaManual(ctx, input2) {
@@ -41654,10 +42244,10 @@ async function registrarCargaManual(ctx, input2) {
41654
42244
  }
41655
42245
  const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
41656
42246
  if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
41657
- const idem = str(input2.idempotencyKey);
42247
+ const idem = str2(input2.idempotencyKey);
41658
42248
  if (idem) {
41659
42249
  const prev = await ctx.db.query(
41660
- `SELECT id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, motivo, datos, validaciones, created_at FROM ${t(ctx, "transicion")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`,
42250
+ `SELECT id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, motivo, datos, validaciones, created_at FROM ${t2(ctx, "transicion")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`,
41661
42251
  [input2.companyId, idem]
41662
42252
  );
41663
42253
  if (prev[0]) {
@@ -41675,8 +42265,8 @@ async function registrarCargaManual(ctx, input2) {
41675
42265
  const contextoPrevio = await contextoDe(ctx, input2.companyId, caso, def2.via, {});
41676
42266
  const previa = evaluarTransicion({ caso, via: def2.via, actor: "humano", datos: { accion_id: "(por registrar)" }, modo: "ejecuta", contexto: contextoPrevio });
41677
42267
  if (!previa.ok) throw new OpsError(previa.codigo, previa.motivo, { validaciones: previa.validaciones, estado: caso.estado });
41678
- const persona = str(input2.persona);
41679
- const comentario = str(input2.comentario);
42268
+ const persona = str2(input2.persona);
42269
+ const comentario = str2(input2.comentario);
41680
42270
  const accion = await registrarAccion(ctx, {
41681
42271
  companyId: input2.companyId,
41682
42272
  casoId: caso.id,
@@ -41710,7 +42300,7 @@ async function registrarCargaManual(ctx, input2) {
41710
42300
  } catch (err) {
41711
42301
  if (idem) {
41712
42302
  const prev = await ctx.db.query(
41713
- `SELECT id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, motivo, datos, validaciones, created_at FROM ${t(ctx, "transicion")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`,
42303
+ `SELECT id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, motivo, datos, validaciones, created_at FROM ${t2(ctx, "transicion")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`,
41714
42304
  [input2.companyId, idem]
41715
42305
  );
41716
42306
  if (prev[0]) {
@@ -41726,11 +42316,75 @@ async function registrarRespuestaPaciente(ctx, input2) {
41726
42316
  const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
41727
42317
  if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
41728
42318
  if (caso.workflow !== "recupero_turno") throw new OpsError("estado_invalido", "solo las citas ofrecidas al paciente llevan respuesta");
41729
- const persona = str(input2.persona);
41730
- const comentario = str(input2.comentario);
42319
+ const persona = str2(input2.persona);
42320
+ const comentario = str2(input2.comentario);
41731
42321
  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 } : {} };
41732
42322
  return transicionar(ctx, { companyId: input2.companyId, casoId: caso.id, via: input2.acepto ? "aceptar" : "no_acepta", datos, motivo: comentario, idempotencyKey: input2.idempotencyKey, actor: input2.actor });
41733
42323
  }
42324
+ async function registrarContacto(ctx, input2) {
42325
+ if (input2.actor.tipo !== "humano") throw new OpsError("actor_no_permitido", "el contacto manual lo declara una persona del hospital; un agente registra la escritura y usa ops_transicionar con contactar");
42326
+ const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
42327
+ if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
42328
+ if (caso.workflow !== "demanda_pendiente") throw new OpsError("estado_invalido", "solo los casos de demanda pendiente llevan contacto");
42329
+ const idem = str2(input2.idempotencyKey);
42330
+ if (idem) {
42331
+ const prev = await ctx.db.query(
42332
+ `SELECT id, caso_id, de_estado, a_estado, via, actor_tipo, actor_id, motivo, datos, validaciones, created_at FROM ${t2(ctx, "transicion")} WHERE company_id = $1 AND idempotency_key = $2 LIMIT 1`,
42333
+ [input2.companyId, idem]
42334
+ );
42335
+ if (prev[0]) return { caso, transicion: mapTransicion(prev[0]), accion: null, respondio: null, repetida: true };
42336
+ }
42337
+ const canal = canalContacto(input2.canal);
42338
+ const respuesta2 = input2.respuesta === void 0 || input2.respuesta === null || input2.respuesta === "" ? null : respuestaContacto(input2.respuesta);
42339
+ if (input2.respuesta !== void 0 && input2.respuesta !== null && input2.respuesta !== "" && !respuesta2) {
42340
+ throw new OpsError("validacion", "respuesta_valida: respuesta debe ser acepta, rechaza o no_contesta", { estado: caso.estado });
42341
+ }
42342
+ const contextoPrevio = await contextoDe(ctx, input2.companyId, caso, "contactar", {});
42343
+ const previa = evaluarTransicion({ caso, via: "contactar", actor: "humano", datos: { accion_id: "(por registrar)", canal }, modo: "ejecuta", contexto: contextoPrevio });
42344
+ if (!previa.ok) throw new OpsError(previa.codigo, previa.motivo, { validaciones: previa.validaciones, estado: caso.estado });
42345
+ if (respuesta2) {
42346
+ const despues = evaluarTransicion({ caso: { ...caso, estado: "contactado" }, via: "responder", actor: "humano", datos: { respuesta: respuesta2 }, modo: "ejecuta", contexto: contextoPrevio });
42347
+ if (!despues.ok) throw new OpsError(despues.codigo, despues.motivo, { validaciones: despues.validaciones, estado: caso.estado });
42348
+ }
42349
+ const persona = str2(input2.persona);
42350
+ const comentario = str2(input2.comentario);
42351
+ const accion = await registrarAccion(ctx, {
42352
+ companyId: input2.companyId,
42353
+ casoId: caso.id,
42354
+ tipo: "escritura",
42355
+ conector: "manual",
42356
+ objetivo: `Paciente \xB7 ${caso.titulo}${caso.referencia ? ` (ref ${caso.referencia})` : ""}`,
42357
+ resultado: "ok",
42358
+ evidencia: { canal, ...persona ? { contactado_por: persona } : {}, ...respuesta2 ? { respuesta: respuesta2 } : {}, ...comentario ? { nota: comentario } : {} },
42359
+ detalle: comentario,
42360
+ actor: input2.actor
42361
+ });
42362
+ const r = await transicionar(ctx, {
42363
+ companyId: input2.companyId,
42364
+ casoId: caso.id,
42365
+ via: "contactar",
42366
+ datos: { accion_id: accion.id, canal, ...persona ? { contactado_por: persona } : {} },
42367
+ motivo: comentario,
42368
+ idempotencyKey: idem,
42369
+ actor: input2.actor
42370
+ });
42371
+ let respondio = null;
42372
+ let final = r.caso;
42373
+ if (respuesta2) {
42374
+ const r2 = await transicionar(ctx, {
42375
+ companyId: input2.companyId,
42376
+ casoId: caso.id,
42377
+ via: "responder",
42378
+ datos: { respuesta: respuesta2, ...persona ? { registrado_por: persona } : {} },
42379
+ motivo: comentario,
42380
+ idempotencyKey: idem ? `${idem}:respuesta` : null,
42381
+ actor: input2.actor
42382
+ });
42383
+ respondio = r2.transicion;
42384
+ final = r2.caso;
42385
+ }
42386
+ return { caso: final, transicion: r.transicion, accion, respondio, repetida: false };
42387
+ }
41734
42388
  function finalesOk() {
41735
42389
  return [...new Set(listarWorkflows().flatMap((w) => w.finalesOk))];
41736
42390
  }
@@ -41741,20 +42395,20 @@ async function tablero(ctx, companyId, diasInput) {
41741
42395
  const dias = Math.min(Math.max(Number(diasInput ?? 30) || 30, 1), 365);
41742
42396
  const p1 = [companyId];
41743
42397
  const porEstadoRows = await ctx.db.query(
41744
- `SELECT workflow, estado, count(*)::int AS cantidad FROM ${t(ctx, "caso")} WHERE company_id = $1 GROUP BY workflow, estado ORDER BY workflow, estado`,
42398
+ `SELECT workflow, estado, count(*)::int AS cantidad FROM ${t2(ctx, "caso")} WHERE company_id = $1 GROUP BY workflow, estado ORDER BY workflow, estado`,
41745
42399
  p1
41746
42400
  );
41747
42401
  const p2 = [companyId, dias];
41748
42402
  const cerradosRows = await ctx.db.query(
41749
- `SELECT workflow, estado, count(*)::int AS cantidad, coalesce(sum(minutos_absorbidos), 0)::int AS minutos FROM ${t(ctx, "caso")} WHERE company_id = $1 AND cerrado_at IS NOT NULL AND cerrado_at >= now() - ($2::int * interval '1 day') GROUP BY workflow, estado`,
42403
+ `SELECT workflow, estado, count(*)::int AS cantidad, coalesce(sum(minutos_absorbidos), 0)::int AS minutos FROM ${t2(ctx, "caso")} WHERE company_id = $1 AND cerrado_at IS NOT NULL AND cerrado_at >= now() - ($2::int * interval '1 day') GROUP BY workflow, estado`,
41750
42404
  p2
41751
42405
  );
41752
42406
  const totalRows = await ctx.db.query(
41753
- `SELECT coalesce(sum(minutos_absorbidos), 0)::int AS minutos FROM ${t(ctx, "caso")} WHERE company_id = $1`,
42407
+ `SELECT coalesce(sum(minutos_absorbidos), 0)::int AS minutos FROM ${t2(ctx, "caso")} WHERE company_id = $1`,
41754
42408
  p1
41755
42409
  );
41756
42410
  const accionesRows = await ctx.db.query(
41757
- `SELECT tipo, count(*)::int AS cantidad FROM ${t(ctx, "accion")} WHERE company_id = $1 AND created_at >= now() - ($2::int * interval '1 day') GROUP BY tipo`,
42411
+ `SELECT tipo, count(*)::int AS cantidad FROM ${t2(ctx, "accion")} WHERE company_id = $1 AND created_at >= now() - ($2::int * interval '1 day') GROUP BY tipo`,
41758
42412
  p2
41759
42413
  );
41760
42414
  const [pendientes, recientes, politicas] = await Promise.all([
@@ -41826,15 +42480,15 @@ async function serie(ctx, companyId, diasInput) {
41826
42480
  const p = [companyId, dias];
41827
42481
  const [abiertos, cerrados, acciones] = await Promise.all([
41828
42482
  ctx.db.query(
41829
- `SELECT to_char(created_at, 'YYYY-MM-DD') AS dia, count(*)::int AS cantidad FROM ${t(ctx, "caso")} WHERE company_id = $1 AND created_at >= now() - ($2::int * interval '1 day') GROUP BY 1 ORDER BY 1`,
42483
+ `SELECT to_char(created_at, 'YYYY-MM-DD') AS dia, count(*)::int AS cantidad FROM ${t2(ctx, "caso")} WHERE company_id = $1 AND created_at >= now() - ($2::int * interval '1 day') GROUP BY 1 ORDER BY 1`,
41830
42484
  p
41831
42485
  ),
41832
42486
  ctx.db.query(
41833
- `SELECT to_char(cerrado_at, 'YYYY-MM-DD') AS dia, estado, count(*)::int AS cantidad, coalesce(sum(minutos_absorbidos), 0)::int AS minutos FROM ${t(ctx, "caso")} WHERE company_id = $1 AND cerrado_at IS NOT NULL AND cerrado_at >= now() - ($2::int * interval '1 day') GROUP BY 1, 2 ORDER BY 1`,
42487
+ `SELECT to_char(cerrado_at, 'YYYY-MM-DD') AS dia, estado, count(*)::int AS cantidad, coalesce(sum(minutos_absorbidos), 0)::int AS minutos FROM ${t2(ctx, "caso")} WHERE company_id = $1 AND cerrado_at IS NOT NULL AND cerrado_at >= now() - ($2::int * interval '1 day') GROUP BY 1, 2 ORDER BY 1`,
41834
42488
  p
41835
42489
  ),
41836
42490
  ctx.db.query(
41837
- `SELECT to_char(created_at, 'YYYY-MM-DD') AS dia, count(*)::int AS cantidad FROM ${t(ctx, "accion")} WHERE company_id = $1 AND created_at >= now() - ($2::int * interval '1 day') GROUP BY 1 ORDER BY 1`,
42491
+ `SELECT to_char(created_at, 'YYYY-MM-DD') AS dia, count(*)::int AS cantidad FROM ${t2(ctx, "accion")} WHERE company_id = $1 AND created_at >= now() - ($2::int * interval '1 day') GROUP BY 1 ORDER BY 1`,
41838
42492
  p
41839
42493
  )
41840
42494
  ]);
@@ -41868,7 +42522,7 @@ async function serie(ctx, companyId, diasInput) {
41868
42522
  async function historialPoliticas(ctx, companyId, limitInput) {
41869
42523
  const limit = Math.min(Math.max(Number(limitInput ?? 50) || 50, 1), 200);
41870
42524
  const rows = await ctx.db.query(
41871
- `SELECT id, workflow, modo_anterior, modo_nuevo, motivo, actor_tipo, actor_id, created_at FROM ${t(ctx, "politica_historial")} WHERE company_id = $1 ORDER BY created_at DESC LIMIT $2`,
42525
+ `SELECT id, workflow, modo_anterior, modo_nuevo, motivo, actor_tipo, actor_id, created_at FROM ${t2(ctx, "politica_historial")} WHERE company_id = $1 ORDER BY created_at DESC LIMIT $2`,
41872
42526
  [companyId, limit]
41873
42527
  );
41874
42528
  return rows.map((r) => ({
@@ -41879,7 +42533,7 @@ async function historialPoliticas(ctx, companyId, limitInput) {
41879
42533
  motivo: r.motivo ?? null,
41880
42534
  actorTipo: String(r.actor_tipo),
41881
42535
  actorId: r.actor_id ?? null,
41882
- createdAt: iso(r.created_at)
42536
+ createdAt: iso2(r.created_at)
41883
42537
  }));
41884
42538
  }
41885
42539
  async function indicador(ctx, input2) {
@@ -41931,14 +42585,14 @@ function metricaPorId(id) {
41931
42585
  }
41932
42586
 
41933
42587
  // src/tableros.ts
41934
- import { randomUUID as randomUUID2 } from "node:crypto";
42588
+ import { randomUUID as randomUUID3 } from "node:crypto";
41935
42589
  var TIPOS = ["indicador", "barras", "linea", "tabla", "texto"];
41936
42590
  var TONOS = ["neutro", "ambar", "senal", "corte"];
41937
42591
  var MAX_WIDGETS = 24;
41938
- function t2(ctx, table) {
42592
+ function t3(ctx, table) {
41939
42593
  return `${ctx.db.namespace}.${table}`;
41940
42594
  }
41941
- function iso2(v) {
42595
+ function iso3(v) {
41942
42596
  if (v instanceof Date) return v.toISOString();
41943
42597
  if (typeof v === "string") return v;
41944
42598
  return "";
@@ -42150,18 +42804,18 @@ function mapTablero(r) {
42150
42804
  publicadoPorTipo: String(r.publicado_por_tipo),
42151
42805
  publicadoPorId: r.publicado_por_id ?? null,
42152
42806
  versiones: Number(r.versiones ?? 1),
42153
- createdAt: iso2(r.created_at),
42154
- updatedAt: iso2(r.updated_at)
42807
+ createdAt: iso3(r.created_at),
42808
+ updatedAt: iso3(r.updated_at)
42155
42809
  };
42156
42810
  }
42157
- var COLS = "id, company_id, clave, titulo, descripcion, pedido, periodo, fuentes, widgets, agente_id, issue_id, publicado_por_tipo, publicado_por_id, versiones, created_at, updated_at";
42811
+ var COLS2 = "id, company_id, clave, titulo, descripcion, pedido, periodo, fuentes, widgets, agente_id, issue_id, publicado_por_tipo, publicado_por_id, versiones, created_at, updated_at";
42158
42812
  var CLAVE_RE = /^[a-z0-9][a-z0-9-]{1,60}$/;
42159
42813
  async function listarTableros(ctx, companyId) {
42160
- const rows = await ctx.db.query(`SELECT ${COLS} FROM ${t2(ctx, "tablero")} WHERE company_id = $1 ORDER BY updated_at DESC`, [companyId]);
42814
+ const rows = await ctx.db.query(`SELECT ${COLS2} FROM ${t3(ctx, "tablero")} WHERE company_id = $1 ORDER BY updated_at DESC`, [companyId]);
42161
42815
  return rows.map(mapTablero);
42162
42816
  }
42163
42817
  async function leerTablero(ctx, companyId, clave) {
42164
- const rows = await ctx.db.query(`SELECT ${COLS} FROM ${t2(ctx, "tablero")} WHERE company_id = $1 AND clave = $2 LIMIT 1`, [companyId, clave]);
42818
+ const rows = await ctx.db.query(`SELECT ${COLS2} FROM ${t3(ctx, "tablero")} WHERE company_id = $1 AND clave = $2 LIMIT 1`, [companyId, clave]);
42165
42819
  return rows[0] ? mapTablero(rows[0]) : null;
42166
42820
  }
42167
42821
  async function publicarTablero(ctx, input2) {
@@ -42176,7 +42830,7 @@ async function publicarTablero(ctx, input2) {
42176
42830
  const now = (/* @__PURE__ */ new Date()).toISOString();
42177
42831
  if (previo) {
42178
42832
  await ctx.db.execute(
42179
- `UPDATE ${t2(ctx, "tablero")} SET titulo = $3, descripcion = $4, pedido = COALESCE($5, pedido), periodo = $6, fuentes = $7::jsonb, widgets = $8::jsonb, agente_id = COALESCE($9, agente_id), issue_id = COALESCE($10, issue_id), publicado_por_tipo = $11, publicado_por_id = $12, versiones = versiones + 1, updated_at = now() WHERE company_id = $1 AND clave = $2`,
42833
+ `UPDATE ${t3(ctx, "tablero")} SET titulo = $3, descripcion = $4, pedido = COALESCE($5, pedido), periodo = $6, fuentes = $7::jsonb, widgets = $8::jsonb, agente_id = COALESCE($9, agente_id), issue_id = COALESCE($10, issue_id), publicado_por_tipo = $11, publicado_por_id = $12, versiones = versiones + 1, updated_at = now() WHERE company_id = $1 AND clave = $2`,
42180
42834
  [input2.companyId, clave, titulo, limpio(input2.descripcion, 600), limpio(input2.pedido, 1e3), limpio(input2.periodo, 80), JSON.stringify(fuentes), JSON.stringify(widgets), input2.agenteId ?? null, input2.issueId ?? null, input2.actor.tipo, input2.actor.id]
42181
42835
  );
42182
42836
  await ctx.activity.log({ companyId: input2.companyId, message: `Autopilot Care: tablero \xAB${titulo}\xBB actualizado (${clave}, versi\xF3n ${previo.versiones + 1})`, metadata: { clave, widgets: widgets.length, agentId: input2.agenteId ?? null, issueId: input2.issueId ?? null } });
@@ -42185,9 +42839,9 @@ async function publicarTablero(ctx, input2) {
42185
42839
  tablero: { ...previo, titulo, descripcion: limpio(input2.descripcion, 600), pedido: limpio(input2.pedido, 1e3) ?? previo.pedido, periodo: limpio(input2.periodo, 80), fuentes, widgets, agenteId: input2.agenteId ?? previo.agenteId, issueId: input2.issueId ?? previo.issueId, publicadoPorTipo: input2.actor.tipo, publicadoPorId: input2.actor.id, versiones: previo.versiones + 1, updatedAt: now }
42186
42840
  };
42187
42841
  }
42188
- const id = randomUUID2();
42842
+ const id = randomUUID3();
42189
42843
  await ctx.db.execute(
42190
- `INSERT INTO ${t2(ctx, "tablero")} (id, company_id, clave, titulo, descripcion, pedido, periodo, fuentes, widgets, agente_id, issue_id, publicado_por_tipo, publicado_por_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10, $11, $12, $13)`,
42844
+ `INSERT INTO ${t3(ctx, "tablero")} (id, company_id, clave, titulo, descripcion, pedido, periodo, fuentes, widgets, agente_id, issue_id, publicado_por_tipo, publicado_por_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10, $11, $12, $13)`,
42191
42845
  [id, input2.companyId, clave, titulo, limpio(input2.descripcion, 600), limpio(input2.pedido, 1e3), limpio(input2.periodo, 80), JSON.stringify(fuentes), JSON.stringify(widgets), input2.agenteId ?? null, input2.issueId ?? null, input2.actor.tipo, input2.actor.id]
42192
42846
  );
42193
42847
  await ctx.activity.log({ companyId: input2.companyId, message: `Autopilot Care: tablero \xAB${titulo}\xBB publicado (${clave})`, metadata: { clave, widgets: widgets.length, agentId: input2.agenteId ?? null, issueId: input2.issueId ?? null } });
@@ -42198,14 +42852,14 @@ async function publicarTablero(ctx, input2) {
42198
42852
  }
42199
42853
 
42200
42854
  // src/worker.ts
42201
- function str2(v) {
42855
+ function str3(v) {
42202
42856
  return typeof v === "string" && v.trim().length > 0 ? v.trim() : null;
42203
42857
  }
42204
42858
  function obj(v) {
42205
42859
  return v && typeof v === "object" && !Array.isArray(v) ? v : {};
42206
42860
  }
42207
42861
  function uuidOrNull(v) {
42208
- const s = str2(v);
42862
+ const s = str3(v);
42209
42863
  return s && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s) ? s : null;
42210
42864
  }
42211
42865
  function numOrNull(v) {
@@ -42213,12 +42867,12 @@ function numOrNull(v) {
42213
42867
  return Number.isFinite(n) ? n : null;
42214
42868
  }
42215
42869
  function companyFrom(params, fallback) {
42216
- const companyId = str2(params.companyId) ?? str2(fallback);
42870
+ const companyId = str3(params.companyId) ?? str3(fallback);
42217
42871
  if (!companyId) throw new OpsError("company_requerida", "companyId es obligatorio");
42218
42872
  return companyId;
42219
42873
  }
42220
42874
  function toolSchema(name) {
42221
- const decl = manifest_default.tools?.find((t3) => t3.name === name);
42875
+ const decl = manifest_default.tools?.find((t4) => t4.name === name);
42222
42876
  return {
42223
42877
  displayName: decl?.displayName ?? name,
42224
42878
  description: decl?.description ?? "",
@@ -42241,7 +42895,7 @@ async function conErrores(fn) {
42241
42895
  function registrarHerramientas(ctx) {
42242
42896
  ctx.tools.register(TOOL_NAMES.listarCasos, toolSchema(TOOL_NAMES.listarCasos), async (params, runCtx) => conErrores(async () => {
42243
42897
  const p = obj(params);
42244
- const casos = await listarCasos(ctx, { companyId: runCtx.companyId, workflow: str2(p.workflow), estado: str2(p.estado), limit: numOrNull(p.limit) });
42898
+ const casos = await listarCasos(ctx, { companyId: runCtx.companyId, workflow: str3(p.workflow), estado: str3(p.estado), limit: numOrNull(p.limit) });
42245
42899
  return {
42246
42900
  content: casos.length ? casos.map(resumenCaso).join("\n") : "No hay casos que coincidan.",
42247
42901
  data: { casos }
@@ -42249,7 +42903,7 @@ function registrarHerramientas(ctx) {
42249
42903
  }));
42250
42904
  ctx.tools.register(TOOL_NAMES.leerCaso, toolSchema(TOOL_NAMES.leerCaso), async (params, runCtx) => conErrores(async () => {
42251
42905
  const p = obj(params);
42252
- const casoId = str2(p.casoId);
42906
+ const casoId = str3(p.casoId);
42253
42907
  if (!casoId) throw new OpsError("caso_requerido", "casoId es obligatorio");
42254
42908
  const detalle = await leerCasoCompleto(ctx, runCtx.companyId, casoId);
42255
42909
  if (!detalle) throw new OpsError("caso_inexistente", `no existe el caso ${casoId} en esta instituci\xF3n`);
@@ -42268,11 +42922,11 @@ Transiciones registradas: ${detalle.transiciones.length}; acciones registradas:
42268
42922
  const p = obj(params);
42269
42923
  const { caso, creado } = await abrirCaso(ctx, {
42270
42924
  companyId: runCtx.companyId,
42271
- workflow: str2(p.workflow) ?? "",
42272
- titulo: str2(p.titulo) ?? "",
42273
- referencia: str2(p.referencia),
42925
+ workflow: str3(p.workflow) ?? "",
42926
+ titulo: str3(p.titulo) ?? "",
42927
+ referencia: str3(p.referencia),
42274
42928
  datos: obj(p.datos),
42275
- idempotencyKey: str2(p.idempotencyKey),
42929
+ idempotencyKey: str3(p.idempotencyKey),
42276
42930
  actor: actorAgente(runCtx),
42277
42931
  agenteId: runCtx.agentId,
42278
42932
  issueId: uuidOrNull(p.issueId)
@@ -42284,16 +42938,16 @@ Transiciones registradas: ${detalle.transiciones.length}; acciones registradas:
42284
42938
  }));
42285
42939
  ctx.tools.register(TOOL_NAMES.transicionar, toolSchema(TOOL_NAMES.transicionar), async (params, runCtx) => conErrores(async () => {
42286
42940
  const p = obj(params);
42287
- const casoId = str2(p.casoId);
42288
- const via = str2(p.via);
42941
+ const casoId = str3(p.casoId);
42942
+ const via = str3(p.via);
42289
42943
  if (!casoId || !via) throw new OpsError("parametros", "casoId y via son obligatorios");
42290
42944
  const r = await transicionar(ctx, {
42291
42945
  companyId: runCtx.companyId,
42292
42946
  casoId,
42293
42947
  via,
42294
42948
  datos: obj(p.datos),
42295
- motivo: str2(p.motivo),
42296
- idempotencyKey: str2(p.idempotencyKey),
42949
+ motivo: str3(p.motivo),
42950
+ idempotencyKey: str3(p.idempotencyKey),
42297
42951
  actor: actorAgente(runCtx)
42298
42952
  });
42299
42953
  return {
@@ -42305,32 +42959,45 @@ Transiciones registradas: ${detalle.transiciones.length}; acciones registradas:
42305
42959
  const p = obj(params);
42306
42960
  const accion = await registrarAccion(ctx, {
42307
42961
  companyId: runCtx.companyId,
42308
- casoId: str2(p.casoId) ?? "",
42309
- tipo: str2(p.tipo) ?? "",
42310
- conector: str2(p.conector) ?? "",
42311
- objetivo: str2(p.objetivo) ?? "",
42312
- resultado: str2(p.resultado) ?? "",
42962
+ casoId: str3(p.casoId) ?? "",
42963
+ tipo: str3(p.tipo) ?? "",
42964
+ conector: str3(p.conector) ?? "",
42965
+ objetivo: str3(p.objetivo) ?? "",
42966
+ resultado: str3(p.resultado) ?? "",
42313
42967
  evidencia: obj(p.evidencia),
42314
- detalle: str2(p.detalle),
42968
+ detalle: str3(p.detalle),
42315
42969
  actor: actorAgente(runCtx)
42316
42970
  });
42317
42971
  return { content: `Acci\xF3n registrada ${accion.id} (${accion.tipo} \xB7 ${accion.conector} \xB7 ${accion.resultado})`, data: { accion } };
42318
42972
  }));
42973
+ ctx.tools.register(TOOL_NAMES.listarReglas, toolSchema(TOOL_NAMES.listarReglas), async (params, runCtx) => conErrores(async () => {
42974
+ const p = obj(params);
42975
+ const reglas = await listarReglas(ctx, runCtx.companyId, { workflow: str3(p.workflow) ?? "demanda_pendiente", estado: str3(p.estado) });
42976
+ const resumen = reglas.map((r) => `${r.id} \xB7 ${r.nombre} \xB7 ${r.tipo} \xB7 ${r.estado} \xB7 v${r.version}${r.ultimaPrueba?.estado === "pedida" && r.ultimaPrueba.version === r.version ? " \xB7 PRUEBA PEDIDA" : ""}`);
42977
+ return { content: resumen.length ? resumen.join("\n") : "No hay reglas.", data: { reglas } };
42978
+ }));
42979
+ ctx.tools.register(TOOL_NAMES.informarPruebaRegla, toolSchema(TOOL_NAMES.informarPruebaRegla), async (params, runCtx) => conErrores(async () => {
42980
+ const p = obj(params);
42981
+ const reglaId = str3(p.reglaId);
42982
+ if (!reglaId) throw new OpsError("regla_requerida", "reglaId es obligatorio");
42983
+ const regla = await informarPruebaRegla(ctx, { companyId: runCtx.companyId, id: reglaId, alcanza: p.alcanza, muestra: p.muestra, consulta: str3(p.consulta), actor: actorAgente(runCtx) });
42984
+ return { content: `Prueba informada: la regla \xAB${regla.nombre}\xBB (versi\xF3n ${regla.version}) alcanza hoy a ${regla.ultimaPrueba?.alcanza ?? 0}`, data: { regla } };
42985
+ }));
42319
42986
  ctx.tools.register(TOOL_NAMES.consultarIndicador, toolSchema(TOOL_NAMES.consultarIndicador), async (params, runCtx) => conErrores(async () => {
42320
42987
  const p = obj(params);
42321
- const nombre = str2(p.indicador) ?? "resumen";
42322
- const data = await indicador(ctx, { companyId: runCtx.companyId, indicador: nombre, workflow: str2(p.workflow), dias: numOrNull(p.dias) });
42988
+ const nombre = str3(p.indicador) ?? "resumen";
42989
+ const data = await indicador(ctx, { companyId: runCtx.companyId, indicador: nombre, workflow: str3(p.workflow), dias: numOrNull(p.dias) });
42323
42990
  return { content: JSON.stringify(data, null, 2), data };
42324
42991
  }));
42325
42992
  ctx.tools.register(TOOL_NAMES.publicarTablero, toolSchema(TOOL_NAMES.publicarTablero), async (params, runCtx) => conErrores(async () => {
42326
42993
  const p = obj(params);
42327
42994
  const r = await publicarTablero(ctx, {
42328
42995
  companyId: runCtx.companyId,
42329
- clave: str2(p.clave) ?? "",
42330
- titulo: str2(p.titulo) ?? "",
42331
- descripcion: str2(p.descripcion),
42332
- pedido: str2(p.pedido),
42333
- periodo: str2(p.periodo),
42996
+ clave: str3(p.clave) ?? "",
42997
+ titulo: str3(p.titulo) ?? "",
42998
+ descripcion: str3(p.descripcion),
42999
+ pedido: str3(p.pedido),
43000
+ periodo: str3(p.periodo),
42334
43001
  fuentes: p.fuentes,
42335
43002
  widgets: p.widgets,
42336
43003
  actor: actorAgente(runCtx),
@@ -42366,12 +43033,12 @@ function registrarDatosYAcciones(ctx) {
42366
43033
  finalesOk: w.finalesOk,
42367
43034
  finalesNoOk: w.finalesNoOk,
42368
43035
  minutosAbsorbidos: w.minutosAbsorbidos,
42369
- transiciones: w.transiciones.map((t3) => ({ via: t3.via, de: t3.de, a: typeof t3.a === "string" ? t3.a : "(seg\xFAn datos)", actor: t3.actor, nivelRequerido: t3.nivelRequerido ?? null, descripcion: t3.descripcion }))
43036
+ transiciones: w.transiciones.map((t4) => ({ via: t4.via, de: t4.de, a: typeof t4.a === "string" ? t4.a : "(seg\xFAn datos)", actor: t4.actor, nivelRequerido: t4.nivelRequerido ?? null, descripcion: t4.descripcion }))
42370
43037
  })));
42371
43038
  ctx.data.register(DATA_KEYS.tablero, async (params) => tablero(ctx, companyFrom(params), numOrNull(params.dias)));
42372
- ctx.data.register(DATA_KEYS.casos, async (params) => listarCasos(ctx, { companyId: companyFrom(params), workflow: str2(params.workflow), estado: str2(params.estado), limit: numOrNull(params.limit) }));
43039
+ ctx.data.register(DATA_KEYS.casos, async (params) => listarCasos(ctx, { companyId: companyFrom(params), workflow: str3(params.workflow), estado: str3(params.estado), limit: numOrNull(params.limit) }));
42373
43040
  ctx.data.register(DATA_KEYS.caso, async (params) => {
42374
- const casoId = str2(params.casoId);
43041
+ const casoId = str3(params.casoId);
42375
43042
  if (!casoId) throw new OpsError("caso_requerido", "casoId es obligatorio");
42376
43043
  return leerCasoCompleto(ctx, companyFrom(params), casoId);
42377
43044
  });
@@ -42379,40 +43046,40 @@ function registrarDatosYAcciones(ctx) {
42379
43046
  ctx.data.register(DATA_KEYS.serie, async (params) => serie(ctx, companyFrom(params), numOrNull(params.dias)));
42380
43047
  ctx.data.register(DATA_KEYS.historialPoliticas, async (params) => historialPoliticas(ctx, companyFrom(params), numOrNull(params.limit)));
42381
43048
  ctx.actions.register(ACTION_KEYS.aprobar, async (params, context) => {
42382
- const casoId = str2(params.casoId);
43049
+ const casoId = str3(params.casoId);
42383
43050
  if (!casoId) throw new OpsError("caso_requerido", "casoId es obligatorio");
42384
- return transicionar(ctx, { companyId: companyFrom(params, context.companyId), casoId, via: "aprobar", motivo: str2(params.motivo), datos: obj(params.datos), actor: actorHumano(context) });
43051
+ return transicionar(ctx, { companyId: companyFrom(params, context.companyId), casoId, via: "aprobar", motivo: str3(params.motivo), datos: obj(params.datos), actor: actorHumano(context) });
42385
43052
  });
42386
43053
  ctx.actions.register(ACTION_KEYS.rechazar, async (params, context) => {
42387
- const casoId = str2(params.casoId);
43054
+ const casoId = str3(params.casoId);
42388
43055
  if (!casoId) throw new OpsError("caso_requerido", "casoId es obligatorio");
42389
- return transicionar(ctx, { companyId: companyFrom(params, context.companyId), casoId, via: "rechazar", motivo: str2(params.motivo), datos: obj(params.datos), actor: actorHumano(context) });
43056
+ return transicionar(ctx, { companyId: companyFrom(params, context.companyId), casoId, via: "rechazar", motivo: str3(params.motivo), datos: obj(params.datos), actor: actorHumano(context) });
42390
43057
  });
42391
43058
  ctx.actions.register(ACTION_KEYS.marcarCargado, async (params, context) => {
42392
- const casoId = str2(params.casoId);
43059
+ const casoId = str3(params.casoId);
42393
43060
  if (!casoId) throw new OpsError("caso_requerido", "casoId es obligatorio");
42394
43061
  return registrarCargaManual(ctx, {
42395
43062
  companyId: companyFrom(params, context.companyId),
42396
43063
  casoId,
42397
43064
  valorCargado: params.valorCargado,
42398
- comentario: str2(params.comentario),
42399
- persona: str2(params.cargadoPor),
43065
+ comentario: str3(params.comentario),
43066
+ persona: str3(params.cargadoPor),
42400
43067
  actor: actorHumano(context)
42401
43068
  });
42402
43069
  });
42403
43070
  ctx.actions.register(ACTION_KEYS.cambiarPolitica, async (params, context) => {
42404
- const workflow = str2(params.workflow);
43071
+ const workflow = str3(params.workflow);
42405
43072
  const modo = params.modo;
42406
43073
  if (!workflow) throw new OpsError("workflow_requerido", "workflow es obligatorio");
42407
43074
  if (!esModo(modo)) throw new OpsError("modo_invalido", "modo debe ser corte, observa, propone, ejecuta o autonomo");
42408
- return cambiarPolitica(ctx, { companyId: companyFrom(params, context.companyId), workflow, modo, motivo: str2(params.motivo), actor: actorHumano(context) });
43075
+ return cambiarPolitica(ctx, { companyId: companyFrom(params, context.companyId), workflow, modo, motivo: str3(params.motivo), actor: actorHumano(context) });
42409
43076
  });
42410
43077
  ctx.actions.register(ACTION_KEYS.abrirCaso, async (params, context) => {
42411
43078
  return abrirCaso(ctx, {
42412
43079
  companyId: companyFrom(params, context.companyId),
42413
- workflow: str2(params.workflow) ?? "",
42414
- titulo: str2(params.titulo) ?? "",
42415
- referencia: str2(params.referencia),
43080
+ workflow: str3(params.workflow) ?? "",
43081
+ titulo: str3(params.titulo) ?? "",
43082
+ referencia: str3(params.referencia),
42416
43083
  datos: obj(params.datos),
42417
43084
  actor: actorHumano(context)
42418
43085
  });
@@ -42425,10 +43092,10 @@ async function manejarApi(ctx, input2) {
42425
43092
  const companyId = input2.companyId;
42426
43093
  const body = obj(input2.body);
42427
43094
  const q = input2.query;
42428
- const qs = (k) => Array.isArray(q[k]) ? str2(q[k][0]) : str2(q[k]);
43095
+ const qs = (k) => Array.isArray(q[k]) ? str3(q[k][0]) : str3(q[k]);
42429
43096
  const actor = input2.actor.actorType === "agent" ? { tipo: "agente", id: input2.actor.agentId ?? input2.actor.actorId, runId: input2.actor.runId ?? null } : { tipo: "humano", id: input2.actor.userId ?? input2.actor.actorId };
42430
43097
  const quien = obj(body.decididoPor);
42431
- const decididoPor = str2(quien.nombre) ? `${str2(quien.nombre)}${str2(quien.email) ? ` (${str2(quien.email)})` : ""}` : null;
43098
+ const decididoPor = str3(quien.nombre) ? `${str3(quien.nombre)}${str3(quien.email) ? ` (${str3(quien.email)})` : ""}` : null;
42432
43099
  const conPersona = (datos) => decididoPor ? { ...datos, decidido_por: decididoPor } : datos;
42433
43100
  const motivoConPersona = (motivo) => decididoPor ? `${motivo ? `${motivo} \xB7 ` : ""}decidido por ${decididoPor}` : motivo ?? null;
42434
43101
  switch (input2.routeKey) {
@@ -42441,20 +43108,20 @@ async function manejarApi(ctx, input2) {
42441
43108
  return detalle ? respuesta(200, detalle) : respuesta(404, { error: "caso_inexistente" });
42442
43109
  }
42443
43110
  case "aprobar":
42444
- return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "aprobar", motivo: str2(body.motivo), datos: conPersona(obj(body.datos)), actor }));
43111
+ return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "aprobar", motivo: str3(body.motivo), datos: conPersona(obj(body.datos)), actor }));
42445
43112
  case "rechazar":
42446
- return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "rechazar", motivo: str2(body.motivo), datos: conPersona(obj(body.datos)), actor }));
43113
+ return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "rechazar", motivo: str3(body.motivo), datos: conPersona(obj(body.datos)), actor }));
42447
43114
  case "marcar-cargado":
42448
- 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 }));
43115
+ return respuesta(200, await registrarCargaManual(ctx, { companyId, casoId: input2.params.casoId ?? "", valorCargado: body.valorCargado, comentario: str3(body.comentario), persona: decididoPor, idempotencyKey: str3(body.idempotencyKey), actor }));
42449
43116
  case "respuesta-paciente":
42450
- 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 }));
43117
+ return respuesta(200, await registrarRespuestaPaciente(ctx, { companyId, casoId: input2.params.casoId ?? "", acepto: body.acepto === true, comentario: str3(body.comentario), persona: decididoPor, idempotencyKey: str3(body.idempotencyKey), actor }));
42451
43118
  case "no-corresponde": {
42452
43119
  if (actor.tipo !== "humano") return respuesta(403, { error: "actor_no_permitido", motivo: "no_corresponde lo decide una persona del hospital" });
42453
- if (!str2(body.motivo)) return respuesta(422, { error: "validacion", motivo: "motivo_presente: no_corresponde requiere motivo" });
42454
- 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 }));
43120
+ if (!str3(body.motivo)) return respuesta(422, { error: "validacion", motivo: "motivo_presente: no_corresponde requiere motivo" });
43121
+ return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "no_corresponde", motivo: motivoConPersona(str3(body.motivo)), datos: conPersona(obj(body.datos)), idempotencyKey: str3(body.idempotencyKey), actor }));
42455
43122
  }
42456
43123
  case "version":
42457
- return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos", "ofrecer-primero", "cuenta-completa", "metricas-certificadas", "facturacion-pendiente", "credenciales"], workflows: listarWorkflows().map((w) => w.clave) });
43124
+ return respuesta(200, { version: manifest_default.version, capacidades: ["carga-manual", "recupero-turnos", "ofrecer-primero", "cuenta-completa", "metricas-certificadas", "facturacion-pendiente", "credenciales", "demanda-pendiente", "reglas"], workflows: listarWorkflows().map((w) => w.clave) });
42458
43125
  case "politicas":
42459
43126
  return respuesta(200, { politicas: await listarPoliticas(ctx, companyId) });
42460
43127
  case "serie":
@@ -42469,12 +43136,33 @@ async function manejarApi(ctx, input2) {
42469
43136
  const tb = await leerTablero(ctx, companyId, (input2.params.clave ?? "").toLowerCase());
42470
43137
  return tb ? respuesta(200, tb) : respuesta(404, { error: "tablero_inexistente" });
42471
43138
  }
43139
+ case "reglas":
43140
+ return respuesta(200, { reglas: await listarReglas(ctx, companyId, { workflow: qs("workflow") ?? "demanda_pendiente", estado: qs("estado") }) });
43141
+ case "crear-regla": {
43142
+ if (actor.tipo !== "humano") return respuesta(403, { error: "actor_no_permitido", motivo: "las reglas las crea una persona del hospital" });
43143
+ const workflow = str3(body.workflow) ?? "demanda_pendiente";
43144
+ return respuesta(200, { regla: await crearRegla(ctx, { companyId, workflow, nombre: body.nombre, tipo: body.tipo, parametros: body.parametros, contacto: body.contacto, cierre: body.cierre, actor: { tipo: "humano", id: decididoPor ?? actor.id } }) });
43145
+ }
43146
+ case "editar-regla": {
43147
+ if (actor.tipo !== "humano") return respuesta(403, { error: "actor_no_permitido", motivo: "las reglas las edita una persona del hospital" });
43148
+ return respuesta(200, { regla: await editarRegla(ctx, { companyId, id: input2.params.reglaId ?? "", nombre: body.nombre, tipo: body.tipo, parametros: body.parametros, contacto: body.contacto, cierre: body.cierre, motivo: str3(body.motivo), actor: { tipo: "humano", id: decididoPor ?? actor.id } }) });
43149
+ }
43150
+ case "estado-regla": {
43151
+ if (actor.tipo !== "humano") return respuesta(403, { error: "actor_no_permitido", motivo: "activar o pausar una regla lo hace una persona del hospital" });
43152
+ return respuesta(200, { regla: await cambiarEstadoRegla(ctx, { companyId, id: input2.params.reglaId ?? "", estado: body.estado, motivo: str3(body.motivo), actor: { tipo: "humano", id: decididoPor ?? actor.id } }) });
43153
+ }
43154
+ case "probar-regla": {
43155
+ if (actor.tipo !== "humano") return respuesta(403, { error: "actor_no_permitido", motivo: "la prueba la pide una persona del hospital; el agente la informa con ops_informar_prueba_regla" });
43156
+ return respuesta(200, { regla: await pedirPruebaRegla(ctx, { companyId, id: input2.params.reglaId ?? "", actor: { tipo: "humano", id: decididoPor ?? actor.id } }) });
43157
+ }
43158
+ case "contacto":
43159
+ return respuesta(200, await registrarContacto(ctx, { companyId, casoId: input2.params.casoId ?? "", canal: body.canal, respuesta: body.respuesta, comentario: str3(body.comentario), persona: decididoPor, idempotencyKey: str3(body.idempotencyKey), actor }));
42472
43160
  case "cambiar-politica": {
42473
- const workflow = str2(body.workflow);
43161
+ const workflow = str3(body.workflow);
42474
43162
  if (!workflow) return respuesta(400, { error: "workflow_requerido" });
42475
43163
  if (!esModo(body.modo)) return respuesta(400, { error: "modo_invalido", detalle: "modo debe ser corte, observa, propone, ejecuta o autonomo" });
42476
43164
  const opciones = body.opciones && typeof body.opciones === "object" && !Array.isArray(body.opciones) ? body.opciones : null;
42477
- return respuesta(200, await cambiarPolitica(ctx, { companyId, workflow, modo: body.modo, motivo: motivoConPersona(str2(body.motivo)), opciones, actor }));
43165
+ return respuesta(200, await cambiarPolitica(ctx, { companyId, workflow, modo: body.modo, motivo: motivoConPersona(str3(body.motivo)), opciones, actor }));
42478
43166
  }
42479
43167
  default:
42480
43168
  return respuesta(404, { error: "ruta_desconocida", routeKey: input2.routeKey });