@wondai/n8n-nodes-nucleo 0.5.5 → 0.6.0

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/README.md CHANGED
@@ -56,6 +56,11 @@ Exemplo de item configurável:
56
56
  O node não interpreta essas regras: ele encaminha o JSON sem alteração. A validação e o snapshot
57
57
  imutável do contrato aplicado pertencem à API do Núcleo (ADR-035).
58
58
 
59
+ A partir da v0.5.6, `Pedido Criar` falha antes da requisição quando `itens` está vazio ou quando
60
+ alguma linha não traz `produto_id`/`alias` e `quantidade` positiva. É uma proteção de integração:
61
+ o Núcleo continua sendo a autoridade final das regras, mas o workflow não envia pedido vazio por
62
+ erro de tool schema.
63
+
59
64
  **Disponibilidade na Rede** ("tem na outra loja?") é exposta como **tool** do AI Agent, mas com regra
60
65
  estrita no prompt: **usar SÓ quando o cliente pedir explicitamente outra unidade**. Passe o `produto_id`
61
66
  já resolvido (ou uma `consulta` curta). A loja que pergunta vem do **vínculo do token** (a IA não
@@ -86,6 +86,29 @@ function asArray(value) {
86
86
  }
87
87
  return [];
88
88
  }
89
+ function hasPedidoItemIdentifier(item) {
90
+ const produtoId = String(item.produto_id ?? "").trim();
91
+ const alias = String(item.alias ?? "").trim();
92
+ return Boolean(produtoId || alias);
93
+ }
94
+ function hasPositiveQuantidade(item) {
95
+ const quantidade = Number(item.quantidade);
96
+ return Number.isFinite(quantidade) && quantidade > 0;
97
+ }
98
+ function assertPedidoCriarItens(itens) {
99
+ if (!Array.isArray(itens) || itens.length === 0) {
100
+ throw new Error("Pedido Criar bloqueado: o campo itens precisa ser um array JSON não vazio com produto_id ou alias e quantidade. Resolva o catálogo e envie os itens estruturados antes de criar o pedido.");
101
+ }
102
+ const invalidIndex = itens.findIndex((item) => {
103
+ if (!item || typeof item !== "object" || Array.isArray(item))
104
+ return true;
105
+ const itemObject = item;
106
+ return !hasPedidoItemIdentifier(itemObject) || !hasPositiveQuantidade(itemObject);
107
+ });
108
+ if (invalidIndex >= 0) {
109
+ throw new Error(`Pedido Criar bloqueado: itens[${invalidIndex}] precisa ter produto_id ou alias e quantidade positiva.`);
110
+ }
111
+ }
89
112
  /** Lê um parâmetro JSON (string ou objeto) como objeto simples. Vazio → null. */
90
113
  function asObject(value) {
91
114
  if (value && typeof value === "object" && !Array.isArray(value))
@@ -158,6 +181,11 @@ class Nucleo {
158
181
  { name: "Conversa", value: "conversa" },
159
182
  { name: "Contexto", value: "contexto" },
160
183
  { name: "Telemetria", value: "telemetria" },
184
+ // --- odonto (ADR-036/038): resources próprios do vertical; server roteia pelo token ---
185
+ { name: "Paciente", value: "paciente" },
186
+ { name: "Procedimento", value: "procedimento" },
187
+ { name: "Agenda", value: "agenda" },
188
+ { name: "Agendamento", value: "agendamento" },
161
189
  ],
162
190
  default: "catalogo",
163
191
  },
@@ -299,6 +327,82 @@ class Nucleo {
299
327
  ],
300
328
  default: "enviar",
301
329
  },
330
+ {
331
+ displayName: "Operação",
332
+ name: "operation",
333
+ type: "options",
334
+ noDataExpression: true,
335
+ displayOptions: { show: { resource: ["paciente"] } },
336
+ options: [
337
+ {
338
+ name: "Buscar",
339
+ value: "buscar",
340
+ action: "Buscar paciente por telefone",
341
+ description: "Lookup do paciente (cliente + dados clínicos) + agendamentos recentes",
342
+ },
343
+ ],
344
+ default: "buscar",
345
+ },
346
+ {
347
+ displayName: "Operação",
348
+ name: "operation",
349
+ type: "options",
350
+ noDataExpression: true,
351
+ displayOptions: { show: { resource: ["procedimento"] } },
352
+ options: [
353
+ {
354
+ name: "Resolver",
355
+ value: "resolver",
356
+ action: "Resolver procedimentos por nome",
357
+ description: "Busca vários procedimentos numa chamada, tolera erro de digitação e apelidos e devolve id, duração e preço (status achou/ambiguo/nao_encontrado)",
358
+ },
359
+ ],
360
+ default: "resolver",
361
+ },
362
+ {
363
+ displayName: "Operação",
364
+ name: "operation",
365
+ type: "options",
366
+ noDataExpression: true,
367
+ displayOptions: { show: { resource: ["agenda"] } },
368
+ options: [
369
+ {
370
+ name: "Consultar",
371
+ value: "consultar",
372
+ action: "Consultar horários livres numa data",
373
+ description: "Horários livres por profissional numa data (horário publicado − ocupado). Sem config do dia ⇒ unknown honesto",
374
+ },
375
+ ],
376
+ default: "consultar",
377
+ },
378
+ {
379
+ displayName: "Operação",
380
+ name: "operation",
381
+ type: "options",
382
+ noDataExpression: true,
383
+ displayOptions: { show: { resource: ["agendamento"] } },
384
+ options: [
385
+ {
386
+ name: "Criar",
387
+ value: "criar",
388
+ action: "Criar agendamento",
389
+ description: "Marca um horário (idempotente). Overlap do profissional ⇒ 409",
390
+ },
391
+ {
392
+ name: "Alterar",
393
+ value: "alterar",
394
+ action: "Alterar agendamento",
395
+ description: "Remarca / troca procedimento ou profissional / observação / status (idempotente). Transição inválida ⇒ 409",
396
+ },
397
+ {
398
+ name: "Cancelar",
399
+ value: "cancelar",
400
+ action: "Cancelar agendamento",
401
+ description: "Cancela o agendamento (idempotente). Libera o horário",
402
+ },
403
+ ],
404
+ default: "criar",
405
+ },
302
406
  // ----------------------------------------------------------------- cliente:buscar
303
407
  {
304
408
  displayName: "Telefone",
@@ -831,6 +935,211 @@ class Nucleo {
831
935
  description: "Opcional. UUID do pedido vinculado (validado contra o tenant).",
832
936
  displayOptions: { show: { resource: ["telemetria"], operation: ["enviar"] } },
833
937
  },
938
+ // ===================================================================== ODONTO (ADR-036/038)
939
+ // ----------------------------------------------------------------- paciente:buscar
940
+ {
941
+ displayName: "Telefone",
942
+ name: "pacTelefone",
943
+ type: "string",
944
+ default: "",
945
+ required: true,
946
+ placeholder: "+5511999999999",
947
+ description: "Telefone do paciente em E.164 (só dígitos + DDI). Devolve o paciente (cliente + dados clínicos) e os agendamentos recentes.",
948
+ displayOptions: { show: { resource: ["paciente"], operation: ["buscar"] } },
949
+ },
950
+ // ----------------------------------------------------------------- procedimento:resolver
951
+ {
952
+ displayName: "Consultas",
953
+ name: "procConsultas",
954
+ type: "string",
955
+ typeOptions: { rows: 3 },
956
+ default: "",
957
+ required: true,
958
+ placeholder: "limpeza, clareamento, canal",
959
+ description: "Procedimentos a buscar: um por linha ou separados por vírgula (máx 10). Tolera erro de digitação, falta de acento e apelidos. Devolve status (achou/ambiguo/nao_encontrado) + candidatos com id, duração e preço.",
960
+ displayOptions: { show: { resource: ["procedimento"], operation: ["resolver"] } },
961
+ },
962
+ // ----------------------------------------------------------------- agenda:consultar
963
+ {
964
+ displayName: "Data",
965
+ name: "agdData",
966
+ type: "string",
967
+ default: "",
968
+ required: true,
969
+ placeholder: "2026-07-15",
970
+ description: "UMA data YYYY-MM-DD. Devolve horários livres por profissional (horário publicado − ocupado). Sem config do dia ⇒ status unknown (não invente horário).",
971
+ displayOptions: { show: { resource: ["agenda"], operation: ["consultar"] } },
972
+ },
973
+ {
974
+ displayName: "Procedimento (ID, opcional)",
975
+ name: "agdProcedimentoId",
976
+ type: "string",
977
+ default: "",
978
+ description: "UUID do procedimento (do Procedimento Resolver). Define a duração dos slots; vazio = 30 min.",
979
+ displayOptions: { show: { resource: ["agenda"], operation: ["consultar"] } },
980
+ },
981
+ {
982
+ displayName: "Profissional (ID, opcional)",
983
+ name: "agdProfissionalId",
984
+ type: "string",
985
+ default: "",
986
+ description: "UUID do profissional. Vazio = todos os profissionais ativos.",
987
+ displayOptions: { show: { resource: ["agenda"], operation: ["consultar"] } },
988
+ },
989
+ {
990
+ displayName: "Passo (min, opcional)",
991
+ name: "agdPassoMin",
992
+ type: "number",
993
+ default: 30,
994
+ description: "Granularidade dos horários oferecidos, em minutos (1..240). Default 30.",
995
+ displayOptions: { show: { resource: ["agenda"], operation: ["consultar"] } },
996
+ },
997
+ // ----------------------------------------------------------------- agendamento:criar
998
+ {
999
+ displayName: "Telefone",
1000
+ name: "agcTelefone",
1001
+ type: "string",
1002
+ default: "",
1003
+ required: true,
1004
+ placeholder: "+5511999999999",
1005
+ description: "Telefone do paciente (E.164). Resolve o paciente existente ou cria um novo.",
1006
+ displayOptions: { show: { resource: ["agendamento"], operation: ["criar"] } },
1007
+ },
1008
+ {
1009
+ displayName: "Nome do Paciente",
1010
+ name: "agcNomePaciente",
1011
+ type: "string",
1012
+ default: "",
1013
+ description: "Opcional. Preenche paciente novo ou existente ainda sem nome. Não sobrescreve nome já cadastrado.",
1014
+ displayOptions: { show: { resource: ["agendamento"], operation: ["criar"] } },
1015
+ },
1016
+ {
1017
+ displayName: "Profissional (ID)",
1018
+ name: "agcProfissionalId",
1019
+ type: "string",
1020
+ default: "",
1021
+ required: true,
1022
+ description: "UUID do profissional (obrigatório). Horário ocupado desse profissional ⇒ 409 (indisponível).",
1023
+ displayOptions: { show: { resource: ["agendamento"], operation: ["criar"] } },
1024
+ },
1025
+ {
1026
+ displayName: "Procedimento (ID, opcional)",
1027
+ name: "agcProcedimentoId",
1028
+ type: "string",
1029
+ default: "",
1030
+ description: "UUID do procedimento (do Procedimento Resolver). Define a duração quando 'Fim' não é enviado.",
1031
+ displayOptions: { show: { resource: ["agendamento"], operation: ["criar"] } },
1032
+ },
1033
+ {
1034
+ displayName: "Início",
1035
+ name: "agcInicio",
1036
+ type: "string",
1037
+ default: "",
1038
+ required: true,
1039
+ placeholder: "2026-07-15T14:00:00-03:00",
1040
+ description: "Instante ISO 8601 do início do agendamento.",
1041
+ displayOptions: { show: { resource: ["agendamento"], operation: ["criar"] } },
1042
+ },
1043
+ {
1044
+ displayName: "Fim (opcional)",
1045
+ name: "agcFim",
1046
+ type: "string",
1047
+ default: "",
1048
+ placeholder: "2026-07-15T14:30:00-03:00",
1049
+ description: "Instante ISO 8601 do fim. Vazio = início + duração do procedimento (ou 30 min).",
1050
+ displayOptions: { show: { resource: ["agendamento"], operation: ["criar"] } },
1051
+ },
1052
+ {
1053
+ displayName: "Observações",
1054
+ name: "agcObservacoes",
1055
+ type: "string",
1056
+ default: "",
1057
+ displayOptions: { show: { resource: ["agendamento"], operation: ["criar"] } },
1058
+ },
1059
+ // ----------------------------------------------------------------- agendamento:alterar / cancelar
1060
+ {
1061
+ displayName: "Agendamento ID",
1062
+ name: "agAgendamentoId",
1063
+ type: "string",
1064
+ default: "",
1065
+ required: true,
1066
+ description: "UUID do agendamento (do tenant). Agendamento de outro tenant → 404.",
1067
+ displayOptions: { show: { resource: ["agendamento"], operation: ["alterar", "cancelar"] } },
1068
+ },
1069
+ {
1070
+ displayName: "Início (remarcar)",
1071
+ name: "agaInicio",
1072
+ type: "string",
1073
+ default: "",
1074
+ placeholder: "2026-07-16T09:00:00-03:00",
1075
+ description: "ISO 8601. Vazio = não remarca.",
1076
+ displayOptions: { show: { resource: ["agendamento"], operation: ["alterar"] } },
1077
+ },
1078
+ {
1079
+ displayName: "Fim (remarcar)",
1080
+ name: "agaFim",
1081
+ type: "string",
1082
+ default: "",
1083
+ description: "ISO 8601. Vazio = mantém/recalcula pelo início.",
1084
+ displayOptions: { show: { resource: ["agendamento"], operation: ["alterar"] } },
1085
+ },
1086
+ {
1087
+ displayName: "Procedimento (ID, alterar)",
1088
+ name: "agaProcedimentoId",
1089
+ type: "string",
1090
+ default: "",
1091
+ description: "UUID do novo procedimento. Vazio = não altera. Use 'null' para desvincular.",
1092
+ displayOptions: { show: { resource: ["agendamento"], operation: ["alterar"] } },
1093
+ },
1094
+ {
1095
+ displayName: "Profissional (ID, alterar)",
1096
+ name: "agaProfissionalId",
1097
+ type: "string",
1098
+ default: "",
1099
+ description: "UUID do novo profissional. Vazio = não altera. Overlap ⇒ 409.",
1100
+ displayOptions: { show: { resource: ["agendamento"], operation: ["alterar"] } },
1101
+ },
1102
+ {
1103
+ displayName: "Observações (alterar)",
1104
+ name: "agaObservacoes",
1105
+ type: "string",
1106
+ default: "",
1107
+ description: "Vazio = não altera.",
1108
+ displayOptions: { show: { resource: ["agendamento"], operation: ["alterar"] } },
1109
+ },
1110
+ {
1111
+ displayName: "Status",
1112
+ name: "agaStatus",
1113
+ type: "options",
1114
+ options: [
1115
+ { name: "(não alterar)", value: "" },
1116
+ { name: "Confirmado", value: "confirmado" },
1117
+ { name: "Realizado", value: "realizado" },
1118
+ { name: "Faltou", value: "faltou" },
1119
+ { name: "Cancelado", value: "cancelado" },
1120
+ ],
1121
+ default: "",
1122
+ description: "Transição de status (a API recusa transição inválida com 409). Válidas: agendado→confirmado/cancelado; confirmado→realizado/faltou/cancelado.",
1123
+ displayOptions: { show: { resource: ["agendamento"], operation: ["alterar"] } },
1124
+ },
1125
+ {
1126
+ displayName: "Motivo",
1127
+ name: "agxMotivo",
1128
+ type: "string",
1129
+ default: "",
1130
+ description: "Motivo do cancelamento (opcional).",
1131
+ displayOptions: { show: { resource: ["agendamento"], operation: ["cancelar"] } },
1132
+ },
1133
+ {
1134
+ displayName: "Idempotency Key",
1135
+ name: "agIdempotencyKey",
1136
+ type: "string",
1137
+ default: "",
1138
+ description: "Criar: vazio = gerada automaticamente. Alterar/cancelar: use a MESMA chave (ex.: messageID do WhatsApp) num retry para não reaplicar a operação.",
1139
+ displayOptions: {
1140
+ show: { resource: ["agendamento"], operation: ["criar", "alterar", "cancelar"] },
1141
+ },
1142
+ },
834
1143
  ],
835
1144
  };
836
1145
  }
@@ -884,6 +1193,7 @@ class Nucleo {
884
1193
  const telefone = this.getNodeParameter("telefone", i).trim();
885
1194
  const nomeCliente = this.getNodeParameter("nomeCliente", i, "").trim();
886
1195
  const itens = asArray(this.getNodeParameter("itens", i));
1196
+ assertPedidoCriarItens(itens);
887
1197
  const tipoEntrega = this.getNodeParameter("tipoEntrega", i);
888
1198
  const observacoes = this.getNodeParameter("observacoes", i, "").trim();
889
1199
  const agendadoPara = this.getNodeParameter("agendadoPara", i, "").trim();
@@ -1081,6 +1391,96 @@ class Nucleo {
1081
1391
  path = "/api/v1/agent/telemetria";
1082
1392
  bodyObj = b;
1083
1393
  }
1394
+ else if (resource === "paciente" && operation === "buscar") {
1395
+ const telefone = this.getNodeParameter("pacTelefone", i).trim();
1396
+ method = "POST";
1397
+ path = "/api/v1/agent/paciente";
1398
+ bodyObj = { telefone };
1399
+ }
1400
+ else if (resource === "procedimento" && operation === "resolver") {
1401
+ const consultas = parseConsultas(this.getNodeParameter("procConsultas", i));
1402
+ method = "POST";
1403
+ path = "/api/v1/agent/procedimento/resolver";
1404
+ bodyObj = { consultas };
1405
+ }
1406
+ else if (resource === "agenda" && operation === "consultar") {
1407
+ const data = this.getNodeParameter("agdData", i).trim();
1408
+ const procedimentoId = this.getNodeParameter("agdProcedimentoId", i, "").trim();
1409
+ const profissionalId = this.getNodeParameter("agdProfissionalId", i, "").trim();
1410
+ const passoMin = Number(this.getNodeParameter("agdPassoMin", i, 30));
1411
+ const b = { data };
1412
+ if (procedimentoId)
1413
+ b.procedimento_id = procedimentoId;
1414
+ if (profissionalId)
1415
+ b.profissional_id = profissionalId;
1416
+ if (Number.isFinite(passoMin) && passoMin > 0)
1417
+ b.passo_min = passoMin;
1418
+ method = "POST";
1419
+ path = "/api/v1/agent/agenda/consultar";
1420
+ bodyObj = b;
1421
+ }
1422
+ else if (resource === "agendamento" && operation === "criar") {
1423
+ const telefone = this.getNodeParameter("agcTelefone", i).trim();
1424
+ const nomePaciente = this.getNodeParameter("agcNomePaciente", i, "").trim();
1425
+ const profissionalId = this.getNodeParameter("agcProfissionalId", i).trim();
1426
+ const procedimentoId = this.getNodeParameter("agcProcedimentoId", i, "").trim();
1427
+ const inicio = this.getNodeParameter("agcInicio", i).trim();
1428
+ const fim = this.getNodeParameter("agcFim", i, "").trim();
1429
+ const observacoes = this.getNodeParameter("agcObservacoes", i, "").trim();
1430
+ // Escrita de agenda EXIGE Idempotency-Key no servidor (400 se vazia) — gera se não vier.
1431
+ idempotencyKey =
1432
+ this.getNodeParameter("agIdempotencyKey", i, "").trim() || (0, node_crypto_1.randomUUID)();
1433
+ const b = { telefone, profissional_id: profissionalId, inicio };
1434
+ if (nomePaciente)
1435
+ b.nome_paciente = nomePaciente;
1436
+ if (procedimentoId)
1437
+ b.procedimento_id = procedimentoId;
1438
+ if (fim)
1439
+ b.fim = fim;
1440
+ if (observacoes)
1441
+ b.observacoes = observacoes;
1442
+ method = "POST";
1443
+ path = "/api/v1/agent/agendamento";
1444
+ bodyObj = b;
1445
+ }
1446
+ else if (resource === "agendamento" && operation === "alterar") {
1447
+ const agendamentoId = this.getNodeParameter("agAgendamentoId", i).trim();
1448
+ const inicio = this.getNodeParameter("agaInicio", i, "").trim();
1449
+ const fim = this.getNodeParameter("agaFim", i, "").trim();
1450
+ const procedimentoId = this.getNodeParameter("agaProcedimentoId", i, "").trim();
1451
+ const profissionalId = this.getNodeParameter("agaProfissionalId", i, "").trim();
1452
+ const observacoes = this.getNodeParameter("agaObservacoes", i, "").trim();
1453
+ const status = this.getNodeParameter("agaStatus", i, "").trim();
1454
+ const b = {};
1455
+ if (inicio)
1456
+ b.inicio = inicio;
1457
+ if (fim)
1458
+ b.fim = fim;
1459
+ // "null" (string) desvincula o procedimento; vazio = não mexe no campo.
1460
+ if (procedimentoId) {
1461
+ b.procedimento_id = procedimentoId.toLowerCase() === "null" ? null : procedimentoId;
1462
+ }
1463
+ if (profissionalId)
1464
+ b.profissional_id = profissionalId;
1465
+ if (observacoes)
1466
+ b.observacoes = observacoes;
1467
+ if (status)
1468
+ b.status = status;
1469
+ idempotencyKey =
1470
+ this.getNodeParameter("agIdempotencyKey", i, "").trim() || (0, node_crypto_1.randomUUID)();
1471
+ method = "PATCH";
1472
+ path = `/api/v1/agent/agendamento/${encodeURIComponent(agendamentoId)}`;
1473
+ bodyObj = b;
1474
+ }
1475
+ else if (resource === "agendamento" && operation === "cancelar") {
1476
+ const agendamentoId = this.getNodeParameter("agAgendamentoId", i).trim();
1477
+ const motivo = this.getNodeParameter("agxMotivo", i, "").trim();
1478
+ idempotencyKey =
1479
+ this.getNodeParameter("agIdempotencyKey", i, "").trim() || (0, node_crypto_1.randomUUID)();
1480
+ method = "POST";
1481
+ path = `/api/v1/agent/agendamento/${encodeURIComponent(agendamentoId)}/cancelar`;
1482
+ bodyObj = motivo ? { motivo } : {};
1483
+ }
1084
1484
  else {
1085
1485
  throw new n8n_workflow_1.NodeApiError(this.getNode(), {
1086
1486
  message: `Operação não suportada: ${resource}.${operation}`,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wondai/n8n-nodes-nucleo",
3
- "version": "0.5.5",
4
- "description": "Node n8n para o Núcleo Wondai — atendimento de IA (gate liga/desliga, cliente, catálogo fuzzy, disponibilidade em rede, pedidos, contexto operacional, telemetria) com assinatura HMAC v1. Tenant vem do token (a parede, ADR-013).",
3
+ "version": "0.6.0",
4
+ "description": "Node n8n para o Núcleo Wondai — atendimento de IA multi-vertical (gate liga/desliga, cliente/paciente, catálogo fuzzy, pedidos [padaria], procedimento/agenda/agendamento [odonto], contexto operacional, telemetria) com assinatura HMAC v1. Tenant e vertical vêm do token (a parede, ADR-013/038).",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
7
7
  "n8n",
@@ -9,6 +9,7 @@
9
9
  "nucleo",
10
10
  "crm",
11
11
  "padaria",
12
+ "odonto",
12
13
  "ai-tool"
13
14
  ],
14
15
  "license": "MIT",