paperclip-plugin-autopilot-ops 0.1.36 → 0.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/manifest.js +9 -1
- package/dist/ui/index.js +9 -1
- package/dist/worker.js +79 -17
- package/package.json +1 -1
package/dist/manifest.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/manifest.ts
|
|
2
2
|
var PLUGIN_ID = "alephoo.autopilot-ops";
|
|
3
|
-
var PLUGIN_VERSION = "0.1.
|
|
3
|
+
var PLUGIN_VERSION = "0.1.38";
|
|
4
4
|
var DB_NAMESPACE_SLUG = "autopilot_ops";
|
|
5
5
|
var PAGE_ROUTE = "autopilot";
|
|
6
6
|
var TOOL_NAMES = {
|
|
@@ -458,6 +458,14 @@ var manifest = {
|
|
|
458
458
|
capability: "api.routes.register",
|
|
459
459
|
companyResolution: { from: "body", key: "companyId" }
|
|
460
460
|
},
|
|
461
|
+
{
|
|
462
|
+
routeKey: "respuesta-equipo",
|
|
463
|
+
method: "POST",
|
|
464
|
+
path: "/casos/:casoId/respuesta-equipo",
|
|
465
|
+
auth: "board",
|
|
466
|
+
capability: "api.routes.register",
|
|
467
|
+
companyResolution: { from: "body", key: "companyId" }
|
|
468
|
+
},
|
|
461
469
|
{
|
|
462
470
|
routeKey: "envio-nota",
|
|
463
471
|
method: "POST",
|
package/dist/ui/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
// src/manifest.ts
|
|
10
10
|
var PLUGIN_ID = "alephoo.autopilot-ops";
|
|
11
|
-
var PLUGIN_VERSION = "0.1.
|
|
11
|
+
var PLUGIN_VERSION = "0.1.38";
|
|
12
12
|
var DB_NAMESPACE_SLUG = "autopilot_ops";
|
|
13
13
|
var PAGE_ROUTE = "autopilot";
|
|
14
14
|
var TOOL_NAMES = {
|
|
@@ -466,6 +466,14 @@ var manifest = {
|
|
|
466
466
|
capability: "api.routes.register",
|
|
467
467
|
companyResolution: { from: "body", key: "companyId" }
|
|
468
468
|
},
|
|
469
|
+
{
|
|
470
|
+
routeKey: "respuesta-equipo",
|
|
471
|
+
method: "POST",
|
|
472
|
+
path: "/casos/:casoId/respuesta-equipo",
|
|
473
|
+
auth: "board",
|
|
474
|
+
capability: "api.routes.register",
|
|
475
|
+
companyResolution: { from: "body", key: "companyId" }
|
|
476
|
+
},
|
|
469
477
|
{
|
|
470
478
|
routeKey: "envio-nota",
|
|
471
479
|
method: "POST",
|
package/dist/worker.js
CHANGED
|
@@ -39932,7 +39932,7 @@ function startWorkerRpcHost(options) {
|
|
|
39932
39932
|
|
|
39933
39933
|
// src/manifest.ts
|
|
39934
39934
|
var PLUGIN_ID = "alephoo.autopilot-ops";
|
|
39935
|
-
var PLUGIN_VERSION = "0.1.
|
|
39935
|
+
var PLUGIN_VERSION = "0.1.38";
|
|
39936
39936
|
var DB_NAMESPACE_SLUG = "autopilot_ops";
|
|
39937
39937
|
var PAGE_ROUTE = "autopilot";
|
|
39938
39938
|
var TOOL_NAMES = {
|
|
@@ -40390,6 +40390,14 @@ var manifest = {
|
|
|
40390
40390
|
capability: "api.routes.register",
|
|
40391
40391
|
companyResolution: { from: "body", key: "companyId" }
|
|
40392
40392
|
},
|
|
40393
|
+
{
|
|
40394
|
+
routeKey: "respuesta-equipo",
|
|
40395
|
+
method: "POST",
|
|
40396
|
+
path: "/casos/:casoId/respuesta-equipo",
|
|
40397
|
+
auth: "board",
|
|
40398
|
+
capability: "api.routes.register",
|
|
40399
|
+
companyResolution: { from: "body", key: "companyId" }
|
|
40400
|
+
},
|
|
40393
40401
|
{
|
|
40394
40402
|
routeKey: "envio-nota",
|
|
40395
40403
|
method: "POST",
|
|
@@ -41057,7 +41065,7 @@ var FORMULA_POR_DEFECTO = { componentes: [{ serie: "ipc_general", peso: 100 }] }
|
|
|
41057
41065
|
var TOPE_AJUSTE_POR_DEFECTO = 15;
|
|
41058
41066
|
var LIMITES_TOPE_AJUSTE = [1, 100];
|
|
41059
41067
|
var LIMITES_PORCENTAJE = [-50, 100];
|
|
41060
|
-
function leerVolumenes(v, dias) {
|
|
41068
|
+
function leerVolumenes(v, dias, periodo) {
|
|
41061
41069
|
if (v === void 0 || v === null) return null;
|
|
41062
41070
|
if (!Array.isArray(v)) return { error: "volumenes tiene que ser una lista [{codigo, cantidad}]" };
|
|
41063
41071
|
const d = typeof dias === "number" && Number.isInteger(dias) && dias >= 7 && dias <= 999 ? dias : null;
|
|
@@ -41070,7 +41078,9 @@ function leerVolumenes(v, dias) {
|
|
|
41070
41078
|
if (!codigo || !Number.isFinite(cantidad2) || cantidad2 < 0) return { error: `volumenes[${i}]: c\xF3digo y cantidad mayor o igual a 0` };
|
|
41071
41079
|
porCodigo[codigo] = (porCodigo[codigo] ?? 0) + cantidad2;
|
|
41072
41080
|
}
|
|
41073
|
-
|
|
41081
|
+
const p = periodo && typeof periodo === "object" ? periodo : {};
|
|
41082
|
+
const fecha5 = (x) => typeof x === "string" && /^\d{4}-\d{2}-\d{2}$/.test(x) ? x : void 0;
|
|
41083
|
+
return { porCodigo, dias: d, ...fecha5(p.desde) ? { desde: fecha5(p.desde) } : {}, ...fecha5(p.hasta) ? { hasta: fecha5(p.hasta) } : {} };
|
|
41074
41084
|
}
|
|
41075
41085
|
var redondear2 = (n, dec = 2) => Math.round(n * 10 ** dec) / 10 ** dec;
|
|
41076
41086
|
var periodoValido = (p) => typeof p === "string" && /^\d{4}-(0[1-9]|1[0-2])$/.test(p);
|
|
@@ -41177,7 +41187,8 @@ function resumenAjuste(items, excluir, porcentaje3, porcentajeCalculado, tope, v
|
|
|
41177
41187
|
total_nuevo: redondear2(cargan.reduce((a, i) => a + (i.valor_nuevo ?? 0), 0)),
|
|
41178
41188
|
tope,
|
|
41179
41189
|
supera_tope: Math.abs(porcentaje3) > tope,
|
|
41180
|
-
|
|
41190
|
+
// Sin nada facturado de estas prácticas en el período no hay con qué estimar: null, no 0.
|
|
41191
|
+
impacto_mensual: volumenes && cargan.some((i) => (volumenes.porCodigo[i.codigo] ?? 0) > 0) ? redondear2(cargan.reduce((a, i) => a + ((i.valor_nuevo ?? 0) - (i.valor_actual ?? 0)) * (volumenes.porCodigo[i.codigo] ?? 0), 0) * 30 / volumenes.dias, 0) : null
|
|
41181
41192
|
};
|
|
41182
41193
|
}
|
|
41183
41194
|
function textoIndices(detalle, desde, hasta) {
|
|
@@ -42794,7 +42805,8 @@ var circularAranceles = {
|
|
|
42794
42805
|
}
|
|
42795
42806
|
]
|
|
42796
42807
|
};
|
|
42797
|
-
var RESPUESTAS_AJUSTE = ["aprobado", "aprobado_otro", "rechazado"];
|
|
42808
|
+
var RESPUESTAS_AJUSTE = ["aprobado", "aprobado_otro", "rechazado", "insistir", "contraoferta"];
|
|
42809
|
+
var RESPUESTAS_DEL_FINANCIADOR = ["aprobado", "otro", "rechazado"];
|
|
42798
42810
|
var CANALES_NOTA = ["correo", "portal_del_financiador", "nota", "otro"];
|
|
42799
42811
|
function calcularAjuste(caso, datos, contexto) {
|
|
42800
42812
|
const formula = contexto.formulaAjuste ?? FORMULA_POR_DEFECTO;
|
|
@@ -42828,7 +42840,7 @@ var ajusteConvenio = {
|
|
|
42828
42840
|
puesto: "Convenios y facturaci\xF3n",
|
|
42829
42841
|
descripcion: "Calcula el aumento de un convenio con los \xEDndices publicados (IPC de INDEC) y la f\xF3rmula de la instituci\xF3n, arma los valores nuevos de cada pr\xE1ctica y la nota al financiador; la direcci\xF3n revisa y aprueba, se registra la respuesta del financiador, se carga con el archivo que genera el plugin y el puesto comprueba releyendo cada valor.",
|
|
42830
42842
|
inicial: "abierto",
|
|
42831
|
-
estados: ["abierto", "calculado", "pendiente_aprobacion", "por_enviar", "enviado", "acordado", "aplicado", "verificado", "con_diferencias", "rechazado", "rechazado_financiador", "cancelado"],
|
|
42843
|
+
estados: ["abierto", "calculado", "pendiente_aprobacion", "por_enviar", "enviado", "respuesta_recibida", "acordado", "aplicado", "verificado", "con_diferencias", "rechazado", "rechazado_financiador", "cancelado"],
|
|
42832
42844
|
finalesOk: ["verificado"],
|
|
42833
42845
|
finalesNoOk: ["rechazado", "rechazado_financiador", "cancelado"],
|
|
42834
42846
|
minutosAbsorbidos: 40,
|
|
@@ -42929,15 +42941,39 @@ var ajusteConvenio = {
|
|
|
42929
42941
|
]
|
|
42930
42942
|
},
|
|
42931
42943
|
{
|
|
42932
|
-
via: "
|
|
42944
|
+
via: "informar_respuesta",
|
|
42933
42945
|
de: ["enviado"],
|
|
42934
|
-
|
|
42946
|
+
// Si aceptó lo propuesto no hay nada que decidir: queda el archivo. Otro porcentaje o un rechazo los decide la dirección.
|
|
42947
|
+
a: (_caso, datos) => datos.respuesta === "aprobado" ? "acordado" : "respuesta_recibida",
|
|
42935
42948
|
actor: ["humano"],
|
|
42936
|
-
descripcion: "
|
|
42949
|
+
descripcion: "El equipo registra lo que contest\xF3 el financiador: acept\xF3 lo propuesto, pidi\xF3 otro porcentaje (porcentaje) o no lo acept\xF3 (motivo).",
|
|
42950
|
+
validar: (_caso, datos) => {
|
|
42951
|
+
const r = datos.respuesta;
|
|
42952
|
+
if (!RESPUESTAS_DEL_FINANCIADOR.includes(r)) return [falla("respuesta_valida", `respuesta: ${RESPUESTAS_DEL_FINANCIADOR.join(", ")}`)];
|
|
42953
|
+
if (r === "otro") return [ok("respuesta_valida"), validarPorcentajeHumano(datos.porcentaje, "porcentaje_valido")];
|
|
42954
|
+
if (r === "rechazado") return [ok("respuesta_valida"), texto4(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "anotar qu\xE9 respondi\xF3 el financiador")];
|
|
42955
|
+
return [ok("respuesta_valida")];
|
|
42956
|
+
}
|
|
42957
|
+
},
|
|
42958
|
+
{
|
|
42959
|
+
via: "reenviar",
|
|
42960
|
+
de: ["enviado"],
|
|
42961
|
+
a: "enviado",
|
|
42962
|
+
actor: ["humano", "agente"],
|
|
42963
|
+
nivelRequerido: "ejecuta",
|
|
42964
|
+
descripcion: "Registrar que se volvi\xF3 a mandar la nota (el financiador no respondi\xF3 en el plazo).",
|
|
42965
|
+
validar: (_caso, datos) => [texto4(datos.destinatario) ? ok("destinatario_presente") : falla("destinatario_presente", "destinatario (a qui\xE9n se reenvi\xF3) es obligatorio")]
|
|
42966
|
+
},
|
|
42967
|
+
{
|
|
42968
|
+
via: "responder",
|
|
42969
|
+
de: ["enviado", "respuesta_recibida"],
|
|
42970
|
+
a: (_caso, datos) => datos.respuesta === "rechazado" ? "rechazado_financiador" : datos.respuesta === "insistir" || datos.respuesta === "contraoferta" ? "por_enviar" : "acordado",
|
|
42971
|
+
actor: ["humano"],
|
|
42972
|
+
descripcion: "La direcci\xF3n decide sobre la respuesta del financiador: aceptar lo propuesto, aceptar otro porcentaje (porcentaje), contraofertar (porcentaje: vuelve a salir una nota), insistir (se reenv\xEDa la nota) o darlo por rechazado (motivo).",
|
|
42937
42973
|
validar: (_caso, datos) => {
|
|
42938
42974
|
const r = datos.respuesta;
|
|
42939
42975
|
if (!RESPUESTAS_AJUSTE.includes(r)) return [falla("respuesta_valida", `respuesta: ${RESPUESTAS_AJUSTE.join(", ")}`)];
|
|
42940
|
-
if (r === "aprobado_otro") return [ok("respuesta_valida"), validarPorcentajeHumano(datos.porcentaje, "porcentaje_valido")];
|
|
42976
|
+
if (r === "aprobado_otro" || r === "contraoferta") return [ok("respuesta_valida"), validarPorcentajeHumano(datos.porcentaje, "porcentaje_valido")];
|
|
42941
42977
|
if (r === "rechazado") return [ok("respuesta_valida"), texto4(datos.motivo) ? ok("motivo_presente") : falla("motivo_presente", "anotar qu\xE9 respondi\xF3 el financiador")];
|
|
42942
42978
|
return [ok("respuesta_valida")];
|
|
42943
42979
|
}
|
|
@@ -42988,7 +43024,7 @@ var ajusteConvenio = {
|
|
|
42988
43024
|
},
|
|
42989
43025
|
{
|
|
42990
43026
|
via: "cancelar",
|
|
42991
|
-
de: ["abierto", "calculado", "pendiente_aprobacion", "por_enviar", "enviado", "acordado", "con_diferencias"],
|
|
43027
|
+
de: ["abierto", "calculado", "pendiente_aprobacion", "por_enviar", "enviado", "respuesta_recibida", "acordado", "con_diferencias"],
|
|
42992
43028
|
a: "cancelado",
|
|
42993
43029
|
actor: ["humano", "agente"],
|
|
42994
43030
|
nivelRequerido: "observa",
|
|
@@ -45396,7 +45432,8 @@ async function transicionar(ctx, input2) {
|
|
|
45396
45432
|
delete datos.excluir;
|
|
45397
45433
|
const c = calcularAjuste(caso, datos, contexto);
|
|
45398
45434
|
const act = leerActual(datos.actual);
|
|
45399
|
-
const vl = leerVolumenes(datos.volumenes, datos.dias_volumen);
|
|
45435
|
+
const vl = leerVolumenes(datos.volumenes, datos.dias_volumen, datos.periodo_volumen);
|
|
45436
|
+
delete datos.periodo_volumen;
|
|
45400
45437
|
const vol = vl && !("error" in vl) ? vl : null;
|
|
45401
45438
|
delete datos.dias_volumen;
|
|
45402
45439
|
if (c.ok && "practicas" in act) {
|
|
@@ -45420,26 +45457,32 @@ async function transicionar(ctx, input2) {
|
|
|
45420
45457
|
delete datos.indices;
|
|
45421
45458
|
delete datos.volumenes;
|
|
45422
45459
|
delete datos.dias_volumen;
|
|
45460
|
+
delete datos.periodo_volumen;
|
|
45423
45461
|
}
|
|
45424
45462
|
if (resultado.via === "ajustar") {
|
|
45425
45463
|
const excluir = Array.isArray(datos.excluir) ? datos.excluir.map(normalizarCodigo) : caso.datos.excluir ?? [];
|
|
45426
45464
|
datos.excluir = excluir;
|
|
45427
45465
|
recalcular(pedido ?? porcentajeAntes, excluir);
|
|
45428
45466
|
} else if (resultado.via !== "calcular") delete datos.excluir;
|
|
45429
|
-
if (resultado.via === "
|
|
45430
|
-
datos.
|
|
45467
|
+
if (resultado.via === "informar_respuesta") {
|
|
45468
|
+
datos.respuesta_financiador = { respuesta: datos.respuesta, porcentaje: pedido, detalle: str2(datos.motivo) ?? str2(datos.detalle) };
|
|
45469
|
+
if (datos.respuesta === "aprobado") datos.porcentaje_acordado = porcentajeAntes;
|
|
45470
|
+
}
|
|
45471
|
+
if (resultado.via === "responder" && (datos.respuesta === "aprobado_otro" || datos.respuesta === "contraoferta") && pedido !== null) {
|
|
45472
|
+
if (datos.respuesta === "aprobado_otro") datos.porcentaje_acordado = pedido;
|
|
45431
45473
|
recalcular(pedido, caso.datos.excluir);
|
|
45432
45474
|
}
|
|
45433
45475
|
if (resultado.via === "responder" && datos.respuesta === "aprobado") datos.porcentaje_acordado = porcentajeAntes;
|
|
45434
45476
|
if (resultado.a === "por_enviar") {
|
|
45435
45477
|
const op = await obtenerOpciones(ctx, input2.companyId, caso.workflow);
|
|
45436
|
-
const
|
|
45478
|
+
const porcentajeNota = typeof datos.porcentaje === "number" ? datos.porcentaje : porcentajeAntes;
|
|
45479
|
+
const r = datos.resumen ?? caso.datos.resumen;
|
|
45437
45480
|
datos.nota = armarNota(op.nota, {
|
|
45438
45481
|
institucion: str2(op.institucion) ?? str2(caso.datos.institucion) ?? "",
|
|
45439
45482
|
financiador: str2(caso.datos.financiador) ?? "",
|
|
45440
45483
|
convenio: str2(caso.datos.convenio) ?? "",
|
|
45441
|
-
porcentaje:
|
|
45442
|
-
indices: textoIndices(caso.datos.detalle ?? { fijo:
|
|
45484
|
+
porcentaje: porcentajeNota,
|
|
45485
|
+
indices: textoIndices(caso.datos.detalle ?? { fijo: porcentajeNota }, str2(caso.datos.ultimo_ajuste), str2(caso.datos.periodo_hasta)),
|
|
45443
45486
|
vigencia: str2(caso.datos.vigencia_desde) ?? "",
|
|
45444
45487
|
firma: str2(op.firma) ?? "",
|
|
45445
45488
|
practicas: r?.practicas ?? 0
|
|
@@ -46343,6 +46386,20 @@ async function registrarEnvioLiquidacion(ctx, input2) {
|
|
|
46343
46386
|
});
|
|
46344
46387
|
return transicionar(ctx, { companyId: input2.companyId, casoId: caso.id, via: "enviar", datos: { aviso_id: accion.id, canal, ...destinatario ? { destinatario } : {}, ...persona ? { registrado_por: persona } : {} }, actor: input2.actor });
|
|
46345
46388
|
}
|
|
46389
|
+
async function registrarRespuestaEquipo(ctx, input2) {
|
|
46390
|
+
const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
|
|
46391
|
+
if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
|
|
46392
|
+
if (caso.workflow !== "ajuste_convenio") throw new OpsError("workflow_invalido", "solo los ajustes de convenio esperan una respuesta del financiador");
|
|
46393
|
+
if (input2.actor.tipo !== "humano") throw new OpsError("actor_no_permitido", "la respuesta la registra una persona");
|
|
46394
|
+
const persona = str2(input2.persona);
|
|
46395
|
+
if (input2.reenvio) {
|
|
46396
|
+
const canal = typeof input2.reenvio.canal === "string" && CANALES_NOTA.includes(input2.reenvio.canal) ? input2.reenvio.canal : "otro";
|
|
46397
|
+
const accion = await registrarAccion(ctx, { companyId: input2.companyId, casoId: caso.id, tipo: "aviso", conector: canal === "correo" ? "mail" : "manual", objetivo: `Nota de ajuste reenviada a ${str2(caso.datos.financiador) ?? "el financiador"}`, resultado: "ok", evidencia: { canal, destinatario: input2.reenvio.destinatario, ...persona ? { registrado_por: persona } : {} }, actor: input2.actor });
|
|
46398
|
+
return transicionar(ctx, { companyId: input2.companyId, casoId: caso.id, via: "reenviar", datos: { aviso_id: accion.id, canal, destinatario: input2.reenvio.destinatario, ...persona ? { registrado_por: persona } : {} }, actor: input2.actor });
|
|
46399
|
+
}
|
|
46400
|
+
const n = typeof input2.porcentaje === "number" ? input2.porcentaje : typeof input2.porcentaje === "string" && input2.porcentaje.trim() ? Number(input2.porcentaje) : void 0;
|
|
46401
|
+
return transicionar(ctx, { companyId: input2.companyId, casoId: caso.id, via: "informar_respuesta", motivo: str2(input2.detalle), datos: { respuesta: input2.respuesta, ...n !== void 0 ? { porcentaje: n } : {}, ...str2(input2.detalle) ? { motivo: str2(input2.detalle) } : {}, ...persona ? { registrado_por: persona } : {} }, actor: input2.actor });
|
|
46402
|
+
}
|
|
46346
46403
|
async function registrarEnvioNota(ctx, input2) {
|
|
46347
46404
|
const caso = await leerCaso(ctx, input2.companyId, input2.casoId);
|
|
46348
46405
|
if (!caso) throw new OpsError("caso_inexistente", `no existe el caso ${input2.casoId} en esta instituci\xF3n`);
|
|
@@ -47398,6 +47455,11 @@ async function manejarApi(ctx, input2) {
|
|
|
47398
47455
|
const datosA = { ...body.porcentaje !== void 0 ? { porcentaje: body.porcentaje } : {}, ...Array.isArray(body.excluir) ? { excluir: body.excluir } : {} };
|
|
47399
47456
|
return respuesta(200, await transicionar(ctx, { companyId, casoId: input2.params.casoId ?? "", via: "ajustar", motivo: motivoConPersona(str3(body.motivo)), datos: conPersona({ ...datosA, ...str3(body.motivo) ? { motivo: str3(body.motivo) } : {} }), idempotencyKey: str3(body.idempotencyKey), actor }));
|
|
47400
47457
|
}
|
|
47458
|
+
case "respuesta-equipo": {
|
|
47459
|
+
if (actor.tipo !== "humano") return respuesta(403, { error: "actor_no_permitido", motivo: "la respuesta la registra una persona" });
|
|
47460
|
+
const reenvio = body.reenvio && typeof body.reenvio === "object" ? body.reenvio : null;
|
|
47461
|
+
return respuesta(200, await registrarRespuestaEquipo(ctx, { companyId, casoId: input2.params.casoId ?? "", respuesta: body.respuesta, porcentaje: body.porcentaje, detalle: str3(body.detalle), reenvio: reenvio ? { canal: reenvio.canal, destinatario: str3(reenvio.destinatario) } : null, persona: decididoPor, actor }));
|
|
47462
|
+
}
|
|
47401
47463
|
case "envio-nota": {
|
|
47402
47464
|
if (actor.tipo !== "humano") return respuesta(403, { error: "actor_no_permitido", motivo: "el env\xEDo lo registra una persona" });
|
|
47403
47465
|
return respuesta(200, await registrarEnvioNota(ctx, { companyId, casoId: input2.params.casoId ?? "", canal: body.canal, destinatario: str3(body.destinatario), persona: decididoPor, actor }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperclip-plugin-autopilot-ops",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Autopilot Care · operación autónoma gobernada para instituciones de salud: workflows determinísticos, validaciones, registro de negocio, políticas (observa/propone/ejecuta/corte) y tablero del director, como plugin de Paperclip.",
|
|
6
6
|
"files": [
|