Snapshot: gti-scheduling-mcp (Z-PRO + Klingo adapters) e workflow n8n de agendamento funcionando de ponta a ponta

- gti-scheduling-mcp: MCP server com ZProAdapter e KlingoAdapter (agendar() completo pros dois)
- Workflow n8n 'Agendamento - GChat - Z-PRO': webhook -> AI Agent -> MCP -> Z-PRO, testado com sucesso real
- Documentacao: specs, plano, runbook de problemas reais e export do workflow

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-10 17:23:10 -03:00
co-authored by Claude Sonnet 5
commit 257afe98c0
48 changed files with 12465 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+237
View File
@@ -0,0 +1,237 @@
# Mapeamento da API Z-PRO — pasta "Agendamentos e Lembretes"
> Este documento contém **apenas** informações verificadas diretamente na
> coleção pública do Postman em
> https://www.postman.com/comunidade-zdg/z-pro/folder/ktt2uon/agendamentos-e-lembretes
> (coleção "Z-PRO v4.x.x.x", pasta "📅 Agendamentos e Lembretes").
> Nenhum valor de token, header sensível ou segredo real foi copiado.
>
> **Status: análise incompleta.** A integração real (`ZProAdapter`) **não foi
> implementada** nesta etapa porque partes essenciais do contrato não estão
> disponíveis na coleção. Veja a seção "O que não foi possível verificar".
## Itens existentes na pasta
A pasta contém 10 requests. Apenas os relacionados ao escopo atual
(consultar agendamentos / criar agendamento) foram analisados em detalhe;
os demais foram apenas identificados para não serem confundidos com o
escopo.
| Nome | Método | Path | No escopo atual? |
| ---------------------- | ------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| AppointmentCreate | POST | `/v2/api/external/{{ApiID}}/appointment/create` | ✅ sim |
| AppointmentList | GET | `/v2/api/external/{{ApiID}}/appointment/list` | ✅ sim (única consulta existente — ver ressalva abaixo) |
| AppointmentShow | GET | `/v2/api/external/{{ApiID}}/appointment/show/{{id}}` | fora do escopo pedido (não é "consultar horários/agendamentos disponíveis" em lista) |
| AppointmentUpdate | POST | `/v2/api/external/{{ApiID}}/appointment/update/{{id}}` | ❌ não (remarcação — explicitamente fora do escopo) |
| AppointmentDelete | POST | `/v2/api/external/{{ApiID}}/appointment/delete/{{id}}` | ❌ não (cancelamento — explicitamente fora do escopo) |
| ScheduleReminderCreate | POST | `/v2/api/external/{{ApiID}}/scheduleReminder/create` | ❌ não (lembretes — explicitamente fora do escopo) |
| ScheduleReminderList | GET | `/v2/api/external/{{ApiID}}/scheduleReminder/list` | ❌ não |
| ScheduleReminderToggle | POST | `/v2/api/external/{{ApiID}}/scheduleReminder/toggle/{{id}}` | ❌ não |
| ScheduleReminderUpdate | POST | `/v2/api/external/{{ApiID}}/scheduleReminder/update/{{id}}` | ❌ não |
| ScheduleReminderDelete | POST | `/v2/api/external/{{ApiID}}/scheduleReminder/delete/{{id}}` | ❌ não |
**Ressalva importante:** a pasta **não possui nenhum endpoint de "horários
disponíveis"** (busca de vagas livres/slots). O único endpoint de consulta
relacionado a agendamentos é `AppointmentList`, que **lista agendamentos já
existentes** (filtráveis por status `pending | confirmed | cancelled |
completed`), não horários livres para agendar. Ver detalhes na seção de
ambiguidades.
## Autenticação
- Tipo: **Bearer Token** (`auth.type = "bearer"`), definido tanto no nível
da coleção quanto em cada request individual.
- O token é enviado automaticamente pelo Postman no header
`Authorization: Bearer <token>`.
- Variável de coleção que armazena o token: `BearerToken` (valor de exemplo
na coleção: `your_token_here` — placeholder, não é um token real).
- Não há chave de API adicional em header (ex.: `X-API-Key`) documentada
nestes requests — apenas o Bearer token.
## Variáveis de coleção relevantes
| Variável | Uso verificado |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BaseUrl` | Host base da API (ex.: `https://{{BaseUrl}}/v2/api/external/...`) |
| `ApiID` | Segmento obrigatório da URL em **todos** os endpoints desta pasta: `/v2/api/external/{{ApiID}}/...`. Não há descrição do que ele representa (ex.: id da instância/empresa) além do nome da variável. |
| `BearerToken` | Token usado na autenticação Bearer |
| `SuperAdminToken` | Não utilizado em nenhum request desta pasta |
**Achado relevante para configuração:** o `ApiID` é parte obrigatória da
URL de todo endpoint. A configuração atual do projeto
(`src/config/config.types.ts`, `src/config/env.config.ts`) já prevê
`ZPRO_INSTANCE_ID` como **opcional**. Se `ApiID` corresponder a esse campo,
ele precisa passar a ser **obrigatório** — mas isso não deve ser alterado
sem confirmação, pois não há descrição na coleção que confirme a
equivalência semântica entre `ApiID` e "instância".
## AppointmentCreate (criar agendamento)
- Método: `POST`
- URL: `https://{{BaseUrl}}/v2/api/external/{{ApiID}}/appointment/create`
- Auth: Bearer (herdado da coleção)
- Headers explícitos no request: nenhum além dos automáticos do Postman
(Bearer + `Content-Type: application/json`, implícito pelo modo de body
`raw`/`json`)
- Query params: nenhum
- Corpo da requisição (JSON, `dataMode: raw`, exemplo salvo na coleção):
```json
{
"title": "Reunião de Vendas",
"description": "Reunião sobre proposta comercial",
"contactId": "{{contactId}}",
"contactName": "João Silva",
"contactPhone": "5511999999999",
"whatsappId": "{{whatsappId}}",
"startAt": "2026-04-15T14:00:00.000Z",
"endAt": "2026-04-15T15:00:00.000Z",
"status": "pending",
"notes": "Levar proposta impressa"
}
```
- Campos identificados no exemplo: `title`, `description`, `contactId`,
`contactName`, `contactPhone`, `whatsappId`, `startAt`, `endAt`,
`status`, `notes`.
- **Não há indicação, na coleção, de quais desses campos são obrigatórios
versus opcionais** (o Postman não declara isso fora do exemplo).
- Formato de sucesso, formato de erro e o campo que identifica o
agendamento criado (`id`, `_id`, `appointment.id`, etc.): **não
verificados** — não há nenhuma resposta de exemplo salva neste request na
coleção (`responses_order: []`).
## AppointmentList (única consulta existente no escopo)
- Método: `GET`
- URL: `https://{{BaseUrl}}/v2/api/external/{{ApiID}}/appointment/list`
- Auth: Bearer (herdado da coleção)
- Query params verificados:
| Parâmetro | Exemplo | Habilitado por padrão | Descrição na coleção |
| ----------- | -------------------------- | --------------------- | --------------------------------------------------- |
| `page` | `1` | sim | — |
| `limit` | `20` | sim | — |
| `status` | `pending` | sim | `pending \| confirmed \| cancelled \| completed` |
| `startFrom` | `2026-04-01T00:00:00.000Z` | **não** (disabled) | Filtrar agendamentos a partir desta data (ISO 8601) |
| `startTo` | `2026-04-30T23:59:59.000Z` | **não** (disabled) | Filtrar agendamentos até esta data (ISO 8601) |
| `search` | `""` | **não** (disabled) | Busca por título ou nome do contato |
- Corpo da requisição: nenhum (GET).
- Formato de sucesso (envelope, paginação, forma de cada item da lista) e
formato de erro: **não verificados** — sem resposta de exemplo salva.
## O que não foi possível verificar
1. **Formato de sucesso de qualquer endpoint** (envelope da resposta,
nomes de campos, se há paginação e como ela é representada).
2. **Formato de erro de qualquer endpoint** (estrutura do corpo de erro,
nomes de campos como `message`/`error`/`code`).
3. **Identificador retornado após a criação** do agendamento (nome do
campo, ex.: `id` vs `_id` vs `appointment.id`).
4. **Conceito de "horário disponível"**: a pasta não expõe nenhum endpoint
de disponibilidade/slots livres. `AppointmentList` lista agendamentos
já existentes (ocupados), não horários livres. Não é seguro tratar a
ausência de um agendamento como "horário disponível" — isso seria
inventar uma regra de negócio não documentada.
5. **Relação entre `slotId` (tipo genérico do projeto,
`CreateAppointmentInput.slotId`) e a API Z-PRO**: `AppointmentCreate`
não recebe um identificador de horário/slot — o agendamento é criado
diretamente por `startAt`/`endAt`. Não há, na coleção, nenhum conceito
de "slot" com identificador próprio.
6. **Campos obrigatórios vs. opcionais** no corpo de `AppointmentCreate`
(a coleção só mostra um exemplo, sem schema declarado).
7. **Semântica exata de `ApiID`** (é o identificador da instância? da
empresa? do WhatsApp conectado?) — não há descrição na coleção.
## O que é necessário para prosseguir com a integração real
Qualquer um dos itens abaixo destravaria a implementação:
- Um **exemplo de resposta salvo** (Postman "Save Response" / "Add
Example") para `AppointmentCreate` (sucesso e, se possível, erro) e para
`AppointmentList` (sucesso).
- Confirmação explícita de que **não existe** endpoint de horários/slots
disponíveis na API Z-PRO (para então documentar oficialmente que
`consultarHorarios()` não tem correspondência 1:1 e decidir, com o
responsável do produto, qual comportamento é aceitável — por exemplo,
reinterpretar o escopo, ou aguardar um endpoint futuro).
- Documentação (Swagger/OpenAPI, README da API, ou outra coleção) que
descreva o schema de resposta dos dois endpoints.
- Confirmação do significado e da obrigatoriedade de `ApiID`.
## Decisão tomada nesta etapa
Como os contratos de resposta não puderam ser verificados, o
`ZProAdapter` **não foi alterado** — permanece lançando `Not implemented`,
exatamente como estava antes desta análise, para não implementar por
suposição. `npm run build`, `npm run lint` e `npm test` continuam
passando sem alterações de código.
## Atualização — resposta real de sucesso capturada (teste manual controlado)
Em 09/08/2026, foi executado um teste manual único e controlado de
`POST /v2/api/external/{ApiID}/appointment/create`, contra uma instância
real do Z-PRO, com dados fictícios (`TESTE-MCP-2026-04-20-001`). O
agendamento foi criado de fato (confirmado visualmente na interface do
Z-PRO) e a resposta HTTP foi capturada:
- **Content-Type**: `application/json; charset=utf-8`
- **Corpo bruto** (dados fictícios, sem nenhuma credencial):
```json
{
"success": true,
"data": {
"source": "manual",
"checkInStatus": "none",
"id": 2,
"title": "TESTE-MCP-2026-04-20-001",
"description": null,
"contactId": null,
"contactName": "Paciente Teste MCP",
"contactPhone": "5511900000000",
"whatsappId": null,
"tenantId": 4,
"startAt": "2026-04-20T14:00:00.000Z",
"endAt": "2026-04-20T14:30:00.000Z",
"status": "pending",
"notes": null,
"updatedAt": "2026-08-09T20:32:06.426Z",
"createdAt": "2026-08-09T20:32:06.426Z",
"bookingPageId": null,
"bookingEventTypeId": null,
"bookingProfessionalId": null,
"userId": null,
"location": null,
"meetingLink": null,
"formAnswers": null,
"cancelToken": null,
"googleEventId": null,
"checkInSentAt": null
}
}
```
**O que isso confirma:**
- Envelope de sucesso: `{ success: boolean, data: {...} }`.
- Identificador do agendamento criado: `data.id`, **numérico** (não
`_id`, não `appointment.id`).
- `data.status` reflete o mesmo valor enviado na requisição (`"pending"`)
— **não temos, ainda, um exemplo real de `"confirmed"` ou
`"cancelled"` retornados pela API**; o mapeamento desses dois valores
no `ZProAdapter` é feito por analogia com o valor documentado no
request (`AppointmentList`'s filtro de `status`), não por observação
direta.
- `data.startAt`/`data.endAt` são ecoados exatamente como enviados.
- `data.contactId` continua `null` quando não enviado no request (não
inventa um contato).
**O que continua não verificado:** o formato de erro (4xx/5xx) do
endpoint (esse teste só cobriu o caminho de sucesso), e os valores reais
de `data.status` para `"confirmed"`/`"cancelled"`. O `ZProAdapter`
continua tratando **qualquer HTTP 2xx** como sucesso mesmo que o corpo
não bata com este envelope exato (ver `zpro.mapper.ts`), como proteção
contra duplicidade caso a API mude de formato em algum caso não coberto
por este teste único.