# CRM, Processos e Automação — especificação de conceitos e implementação para o novo ERP de indústria

> **Público:** o time que está construindo o novo ERP de indústria.
> **Origem:** atividade #479827. Consolidado em set/2026, a partir de leitura direta do código do Panoverse.
> **Premissa:** este documento é a **única fonte** que o time terá. Ele é autocontido: descreve o conceito, o porquê da decisão, **e o modelo de dados real, campo a campo**, para que a implementação possa ser espelhada sem acesso ao repositório.

---

## 1. Como usar este documento

### Para quem é

Para quem vai **projetar e construir** o CRM, o workflow interno e a automação do novo ERP. Não é documentação de manutenção do sistema atual: é a transferência do que aprendemos construindo e operando esse sistema por vários anos.

### O que ele assume

- Que você tem um ERP com pedidos, produtos, notas fiscais e pessoas — os quatro domínios aqui descritos se apoiam nesses conceitos, mas não os definem.
- Que você **não** vai copiar código. Os nomes reais de classe, tabela e coluna estão aqui para servir de referência de modelagem e de vocabulário comum, não para transplante.
- Que a stack de destino é livre. Nada aqui depende de linguagem, ORM ou banco específico, exceto onde explicitamente marcado (busca vetorial, travas de banco).

### Como está organizado

| Seção | O que traz | Quando ler |
|---|---|---|
| 2. Sumário executivo | O problema e os quatro blocos | Primeiro contato |
| 3. Mapa de domínios | O que dá para adotar isolado | Antes de decidir escopo |
| 4. Fundamentos transversais | Multi-tenancy, auditoria, arquivos, permissão, notificação, campos customizados | **Antes de modelar qualquer coisa** |
| 5. Atendimento | Modelo completo, estados, canais, contenções, métricas | Ao construir o CRM |
| 6. Processo | Motor de workflow completo | Ao construir o workflow interno |
| 7. Centro de custo | Árvore organizacional e visibilidade | Junto com a seção 6 |
| 8. Motor de fluxo | Versionamento, compilação, execução | Ao construir a automação |
| 9. Catálogo de blocos | Os 28 blocos, um a um | Referência durante a seção 8 |
| 10. Agente de IA | Integrações, operações, variáveis, RAG | Ao construir o agente |
| 11. Fluxos ponta a ponta | 8 walkthroughs completos | Para entender como as peças se encaixam |
| 12. Contratos de extensão | Assinaturas dos pontos de extensão | Ao implementar |
| 13. Integrações externas | Com o que o sistema fala | Planejamento de integração |
| 14. Decisões de arquitetura | O porquê de cada escolha | **Leitura obrigatória** |
| 15. Armadilhas | O que já custou incidente | **Leitura obrigatória** |
| 16. Copiar × adaptar × não replicar | Classificação item a item | Ao definir escopo |
| 17. Roteiro de implementação | Fases com critério de pronto | Planejamento |
| 18–20. Glossário e apêndices | Vocabulário, esquema, enumerações | Consulta |

### Se você só puder ler três seções

**4** (fundamentos), **14** (decisões) e **15** (armadilhas). São as três que não se descobrem lendo código nem especificação de tela.

### Convenções

- `NomeDeClasse` — classe real no sistema atual.
- `schema.nome_tabela` — tabela real. Onde não há prefixo de schema, a tabela está no schema padrão (`public`).
- `coluna_real` — coluna real.
- Blocos marcados com **⚠** são armadilhas com incidente documentado.
- Blocos marcados com **✅** são decisões que recomendamos copiar sem mudança.
- Blocos marcados com **🚫** são coisas que **não** recomendamos replicar.

---

## 2. Sumário executivo

O CRM do sistema atual resolve um problema que todo ERP com venda ao consumidor acaba tendo: **a empresa recebe demandas por muitos canais diferentes e precisa tratá-las com o mesmo padrão de trabalho**. Um cliente pergunta por e-mail, outro pelo WhatsApp, outro pela caixa de perguntas de um marketplace, outro num comentário de rede social. Todos esperam resposta, alguns geram trabalho interno, e a empresa precisa saber quanto tempo levou, quem atendeu e o que foi dito.

Ao longo de vários anos esse módulo se decantou em **quatro blocos conceituais**, independentes o bastante para serem adotados separadamente:

- **Atendimento** — a conversa com alguém de fora: por onde entrou, quem responde, qual o prazo, quando termina. Hoje: 39 canais fixos mais canais criados por cadastro, 5 estados de negócio, prazo por mensagem e trilha de auditoria automática.
- **Processo** — o trabalho interno que uma demanda gera. Um motor genérico de workflow com desenho configurável e instâncias tramitadas, usado hoje por **8 módulos diferentes** do ERP sem nenhuma adaptação.
- **Centro de custo** — a árvore organizacional que responde "para qual setor isso vai" e "quem pode ver isso", com visibilidade herdada para os setores descendentes.
- **Automação e IA** — um motor de fluxo visual com 28 blocos, versionado, que serve tanto para automação determinística quanto para agente de IA com ferramentas e base de conhecimento vetorial.

Três coisas tornam esse conjunto valioso o suficiente para justificar a cópia:

1. **O workflow é configurável por cadastro.** Criar um tipo novo de demanda — não conformidade, aprovação de compra, manutenção de máquina — não exige desenvolvimento.
2. **A automação é desenhada por quem conhece a regra.** O motor de fluxo tira do caminho o ciclo "pessoa de negócio explica → desenvolvedor implementa → release".
3. **O mesmo motor serve automação e IA.** Os blocos que automatizam são as mesmas ferramentas que o agente de IA pode chamar.

E uma coisa torna este documento valioso além da especificação: **as seções 14 e 15**. Decisões e armadilhas são o que não se recupera lendo o código de chegada — cada armadilha listada custou incidente real em produção.

---

## 3. Mapa de domínios e dependências

```
                    ┌──────────────────────┐
                    │     ATENDIMENTO      │  conversa com quem está fora
                    │  canal · ciclo · SLA │
                    └──────────┬───────────┘
                               │ opcional, nos dois sentidos
                    ┌──────────┴───────────┐
                    │       PROCESSO       │  trabalho interno
                    │  desenho × instância │
                    └──────────┬───────────┘
                               │ OBRIGATÓRIO
                    ┌──────────┴───────────┐
                    │   CENTRO DE CUSTO    │  para onde vai, quem vê
                    └──────────────────────┘

    ┌──────────────────────────────────────────────────┐
    │      AUTOMAÇÃO E IA — motor de fluxo visual      │
    │  atua sobre Atendimento, sobre Processo, ou só   │
    │  como endpoint HTTP independente                 │
    └──────────────────────────────────────────────────┘

    ┌──────────────────────────────────────────────────┐
    │  FUNDAMENTOS TRANSVERSAIS (seção 4)              │
    │  multi-tenancy · auditoria · arquivos ·          │
    │  permissão · notificação · campos customizados   │
    └──────────────────────────────────────────────────┘
```

### Dependências reais

| Dependência | Natureza | Comentário |
|---|---|---|
| Processo → Centro de custo | **Obrigatória** | Todo processo nasce endereçado a um setor, e a abertura falha sem ele. Sem essa dimensão o motor de workflow não roteia nada |
| Atendimento → Processo | **Opcional** | Um atendimento pode se resolver na conversa. A ponte existe para quando não se resolve |
| Processo → Atendimento | **Opcional** | Só quando se quer dar retorno ao cliente sobre o andamento interno |
| Automação → Atendimento / Processo | **Opcional dos dois lados** | O motor de fluxo tem um terceiro modo — endpoint HTTP — que não toca nenhum dos dois |
| Atendimento → Centro de custo | **Fraca** | Usado para rotear fila e escopo de textos padrão; o atendimento pode ser roteado por usuário direto |
| Todos → Fundamentos | **Obrigatória** | Multi-tenancy, permissão e arquivos não são opcionais em nenhum dos quatro |

### Combinações que fazem sentido adotar

| Escopo | O que entrega | Fases do roteiro (seção 17) |
|---|---|---|
| Centro de custo + Processo | Workflow interno configurável. Aprovação de compra, não conformidade, manutenção, liberação de crédito | 1 |
| + Eventos | O workflow passa a agir no ERP, não só registrar | 1–2 |
| + Motor de fluxo | Automação desenhada, sem release | 1–3 |
| + Atendimento | CRM completo com SLA | 1–5 |
| + Agente de IA | Resposta automática | 1–8 |
| Só motor de fluxo | Camada de automação e endpoints, sem CRM nem workflow | 3 |

**O que não funciona:** adotar processo sem centro de custo. Sobra um workflow que não sabe para quem mandar a tarefa.

---

## 4. Fundamentos transversais

Seis mecanismos atravessam todos os domínios. **Modelar qualquer um dos quatro blocos antes de decidir estes seis produz retrabalho.**

### 4.1 Multi-tenancy: empresa, loja e marketplace

Três eixos **ortogonais**, frequentemente confundidos por serem todos "de quem é a venda".

| Conceito | Classe / tabela | Papel |
|---|---|---|
| **Empresa** | `Admin\Model\Empresa` → `admin.ad_empresa` | O **tenant**. Unidade jurídica/organizacional. É o eixo de isolamento de dados. Auto-relacional (matriz/filial, e também fornecedores) |
| **Loja** | `Unico\Model\Loja` → `un_loja` | O **canal de venda**: a loja própria do e-commerce, ou a conta/vitrine em um marketplace. **Não é multi-tenant** — não tem `emp_id` |
| **Marketplace** | `Unico\Model\Marketplace` → `un_marketplace` | O **conector/integração**: o "driver" do canal. Várias lojas podem usar o mesmo conector |
| Laço Empresa↔Loja | `Unico\Model\LojaEmpresa` → `un_loja_empresa` | N:N, PK composta (`loj_id`, `emp_id`). É por aqui que o filtro decide quais lojas o tenant enxerga |

Exemplos reais de `Loja`: `LOJA_PANORAMA = 1`, `LOJA_ML = 3`, `LOJA_MAGAZINE_LUIZA = 7`, `LOJA_AMERICANAS = 12`, `LOJA_LEROY_MERLIN = 42`.
Exemplos de `Marketplace`: `CODIGO_MARKETPLACE_PANORAMA_LOJA = 1`, `MERCADO_LIVRE = 3`, `VIA_VAREJO = 13`, `GOOGLE_SHOPPING = 16`, `FACEBOOK = 17`, `ELASTICSEARCH = 18`.

#### Como o isolamento funciona — três peças

1. **Classe base** — `Admin\Model\EmpresaBase` dá a relação `emp_id → Empresa` e implementa a interface marcadora `Admin\Model\EmpresaFilter`. No `@PrePersist` **preenche a empresa sozinha** com a da sessão, se ninguém setou. Entidades que não podem estender a base implementam a interface diretamente.
2. **Filtro de leitura** — `Admin\Repository\EmpresaFilter` é um filtro de ORM registrado como `"empresa_filter"`: para toda entidade que implementa a interface marcadora, injeta `alias.emp_id = <id da sessão>` em **todas** as consultas. Retorna vazio (sem filtro) quando a entidade não é marcada ou quando não há empresa na sessão.
3. **Controle** — `Admin\Controller\BaseController`: `getEmpresa()` lê a empresa da sessão e devolve uma referência lazy, com cache por requisição; `disableFilterEmpresa()` / `enableFilterEmpresa()` / `isEnableFilterEmpresa()` ligam e desligam o filtro.

Há um **segundo filtro do mesmo desenho** — `"transportadora_filter"` — para o portal de transportadoras.

Conceitos de escopo auxiliares: `getEmpresaBaseID()` / `getEmpresaBase()` / `isEmpresaBase()` (a empresa "matriz", definida em configuração), `getClientPlatform()` (o identificador do cliente da plataforma, um nível **acima** da empresa) e `getLojaBaseID()`.

Configuração por tenant vive em entidades `Admin\Model\EmpresaConfiguracao*` — S3, e-mail, estoque, SAC, Correios.

> **✅ Copiar:** a combinação "interface marcadora + filtro automático de leitura + preenchimento automático na escrita" é o que torna o isolamento difícil de furar por esquecimento. Cada consulta nova nasce filtrada.

> **⚠ Armadilha:** o desligamento do filtro (`disableFilterEmpresa()`) é usado em vários pontos legítimos — grids de relacionamento, catálogo compartilhado, relatórios globais. **Cada chamada é um furo potencial de isolamento** e precisa de justificativa escrita. No produto novo, torne o escopo explícito e limitado: um bloco `semFiltroDeTenant(function(){ ... })` em vez de um toggle global que pode ficar ligado.

### 4.2 Trilha de auditoria por callback de ciclo de vida

O padrão mais replicável do sistema: **o histórico não é responsabilidade de quem altera o registro** — é produzido pelo próprio ato de gravar.

No atendimento (`CRM\Model\Atendimento`), os callbacks são:

- `@PrePersist` / `@PreUpdate` → `newLog()` monta um `CRM\Model\AtendimentoLog` com situação, autor e observação correntes; `newLogAlteracaoPedido()` e `newLogAlteracaoProdutoDerivacao()` registram vínculo/desvínculo comparando o *changeset* do ORM.
- `@PostPersist` / `@PostUpdate` → `postPersitUpdate()` persiste o log e tudo o que ficou acumulado em `$itemsToPersist`.

Duas mecânicas que acompanham o padrão:

- **Deduplicação** — `addHashObservacoes(md5(id . situacao))`, um array estático por requisição: o mesmo par atendimento+situação só gera uma linha, mesmo com vários flushes no mesmo ciclo.
- **Carimbo de autoria** — o chamador preenche campos **transientes** (`setUsuarioLog()`, `setObservacaoLog()`, `setDataArrayFormularioLog()`) antes do flush. Preencher `dataArrayFormularioLog` muda o tipo do log para "formulário".

> **✅ Copiar o princípio:** com registro manual, os caminhos que esquecem são justamente os automáticos — jobs, integrações, bots — que são os que mais precisam de rastro, porque ninguém viu acontecer.

> **🚫 Não replicar a forma:** campos transientes na entidade para passar autor e texto são frágeis e não óbvios. Use um **contexto de operação** (quem, por quê, de onde) propagado na transação, lido pelo interceptador de gravação.

### 4.3 Arquivos: documento eletrônico e mídia

**Entidade:** `Documento\Model\DocumentoEletronico` → `documento.dc_documento_eletronico` (estende `EmpresaBase`, índice em `doc_file_md5`)

| Coluna | Tipo | Significado |
|---|---|---|
| `doc_id` | int | PK |
| `tip_id` | FK → `TipoDocumento` | Classificação funcional: relatório, nota, anexo de processo |
| `doc_documento` | text | **A chave**. Conforme `doc_tipo`: o conteúdo textual, a *key* do objeto em S3, ou uma URL |
| `midia_id` | FK → `Midia` | Quando o binário está no banco |
| `doc_identificador` | string | Nome original do arquivo |
| `log_id` | FK → `LogIntegracao` | Rastreio da integração que gerou |
| `doc_data_hora` | datetime | |
| `doc_tipo` | smallint | Ver abaixo |
| `doc_file_md5` | string | Hash para deduplicação |
| `doc_bucket` | string(100) | Bucket S3 **gravado no registro** — não derivado em runtime |

Modos de armazenamento (`DocumentoEletronico::TIPO_*`):

- `TIPO_TEXTO = 1` — conteúdo inline na própria coluna
- `TIPO_ARQUIVO = 2` — binário no banco (`un_midia.midia_file`)
- `TIPO_REPOSITORIO = 3` — objeto em S3 (**o caso normal hoje**)
- `TIPO_URL = 4` — referência externa

Ciclo (`Documento\Controller\DocumentoEletronicoController`):

- **Upload** — `newDocumentoEletronicoByFileUpload()` monta a chave `prefixo/AAAA-MM-DD/arquivo`, calcula MD5 e **deduplica**: se já existe documento com o mesmo hash, reaproveita o registro em vez de subir de novo.
- **Leitura** — o bucket é resolvido em cascata: bucket do próprio documento → configuração da empresa → configuração da loja.
- **Entrega** — `getLink()` devolve a **URL pública** do objeto. O upload usa ACL `public-read`.
- **URL assinada** — existe em **um único ponto**: a entrega de arquivo ao serviço externo de processamento de RAG, com expiração de 1 hora.

**Entidade irmã:** `Unico\Model\Midia` → `un_midia` — imagens e mídia, podendo guardar o binário (`midia_file`) e/ou o caminho S3 (`midia_raw_s3_path`), com mime type e md5.

> **🚫 Não replicar:** upload com ACL `public-read` e acesso por URL pública. Anexo de atendimento e de processo contém documento de cliente, laudo, nota fiscal e conversa. **Todo acesso a arquivo deve ser autenticado e por URL assinada de vida curta**, como já se faz no caminho do RAG. Este é o ponto de segurança mais evidente a corrigir na cópia.

> **✅ Copiar:** a deduplicação por hash no upload. Anexos repetidos são comuns (mesmo PDF reenviado em vários atendimentos) e o ganho é imediato.

### 4.4 Permissão: RBAC por rota

**Não há ACL por registro nem grupo de usuários.** O modelo é: usuário → N perfis → N rotas.

```
Usuario ──N:N (un_usuario_perfil)──► Perfil ──N:N (un_perfil_router)──► Router
```

| Classe | Tabela | Papel |
|---|---|---|
| `Unico\Model\Usuario` | `un_usuario` | Tipos: `TIPO_MASTER = 1`, `TIPO_ADMINISTRADOR = 2`, `TIPO_SITE = 3`, `TIPO_WEB_SERVICE = 4` |
| `Unico\Model\Perfil` | `un_perfil` | Estende `EmpresaBase` — perfis são **por tenant**. Tipos: `TIPO_NORMAL = 1`, `TIPO_PERFIL_FORNECEDOR = 2`, `TIPO_PERFIL_TRANSPORTADORA = 3`, `TIPO_PERFIL_TECNICO = 4` |
| `Faderim\Framework\Model\Router` | `faderim.fd_router` | `router_name` (PK, string ≤50) é **o identificador da permissão** |
| `Unico\Model\UsuarioPerfil` | `un_usuario_perfil` | Laço N:N |
| `Unico\Model\PerfilRouter` | `un_perfil_router` | Laço N:N |

**O conceito central:** cada ação e cada tela do sistema é uma rota, e **a rota é a permissão**. A granularidade é por operação — `vd_pedido_list`, `vd_pedido_view`, `vd_pedido_edit`, `vd_pedido_add`, `vd_pedido_delete`, `vd_pedido_export`. Não existe "permissão de pedido"; existem seis.

**Decisão** (`BaseController::validaPrivilegioUsuario($routerName)`):

1. usuário inativo na sessão → nega;
2. `TIPO_MASTER` → libera tudo (bypass total);
3. senão, consulta se **algum** dos perfis do usuário contém aquela rota, com cache de 300 segundos.

**Onde é aplicado:**

- *Enforcement* central no front controller do admin, em toda requisição, com allowlist de rotas públicas. Sem acesso: registra erro e **destrói a sessão**.
- A interface se auto-filtra: itens de menu e botões de grid consultam a mesma função e somem quando não há permissão.

**Dimensão adicional, ortogonal ao RBAC:** no motor de processos, além de ter a rota, o usuário precisa estar vinculado ao **centro de custo** do trâmite para agir sobre ele (seção 7.4). É o mais próximo de permissão por registro que existe no sistema.

> **✅ Copiar:** permissão por operação, não por entidade. É o que permite dar "ver pedido" sem dar "editar pedido" sem inventar um segundo sistema.

> **⚠ Ponto de atenção:** o bypass total do usuário master não tem trilha diferenciada. No produto novo, um perfil de superusuário deve gerar registro de auditoria distinto, não silêncio.

### 4.5 Notificação

Catálogo em dois níveis, fila persistida e job que drena por canal.

#### Modelo

| Classe | Tabela | Papel |
|---|---|---|
| `Unico\Model\TipoNotificacao` | `un_tipo_notificacao` | **Domínio/assunto**. ~55 constantes fixas, semeadas por migration. Sem multi-tenancy |
| `Unico\Model\TipoNotificacaoEvento` | `un_tipo_notificacao_evento` | **Gatilho concreto**: canal + template. Estende `EmpresaBase` |
| `Unico\Model\TipoNotificacaoEventoDestinatario` | `un_tipo_notificacao_evento_destinatario` | Lista fixa de destinatários (`dest_destinatario`) |
| `Unico\Model\Notificacao` | `un_notificacao` | **Item de fila já renderizado**. Estende `EmpresaBase` |
| `Unico\Model\NotificacaoAdmin` | `un_notificacao_admin` | Notificação **interna** (sino do admin) |
| `Unico\Model\NotificacaoAnexo` | — | Liga notificação a `DocumentoEletronico` ou `Midia` |

**Colunas de `un_tipo_notificacao_evento`:** `ev_nome`, `ev_codigo`, `ev_template_tipo` (= o canal), `ev_template` (corpo, template de string), `ev_template_titulo` (título), `ev_reply_to_padrao`, `ev_ativo`, `ev_config` (json), `ev_tipo` (interna/externa), `ev_prioridade`, `ev_agendar` + `ev_dados_agendamento` (json), FK `tip_id` e `loj_id`.

**Colunas de `un_notificacao`:** `not_tipo` (canal), `not_situacao`, `not_destinatario`, `not_titulo`, `not_mensagem`, `not_origem`, `not_reply_to`, `not_params` (json), `not_data_hora_envio`, `not_erro`, FK `tip_id` e `loj_id`.

#### Canais

`Notificacao::TIPO_*`:

- `TIPO_EMAIL = 1` — **implementado** (SMTP, configuração por empresa)
- `TIPO_SMS = 2` — **constante existe, envio não implementado**
- `TIPO_MARKETPLACE = 3` — não passa pela fila; vai por controller próprio (mensagem dentro do marketplace)
- `TIPO_PUSH = 4` — **implementado** (serviço de push; detecta token de app e comuta o provedor)
- `TIPO_WHATSAPP = 6` — **implementado**, por *template name*; registra atendimento de SAC junto
- `TIPO_PUSH_APP = 7` — alcançado indiretamente, por heurística sobre o formato do token
- `TIPO_META_MESSENGER = 8` — constante existe, sem envio

Situação: `SITUACAO_FILA = 1`, `SITUACAO_ENVIADO = 2`, `SITUACAO_ERRO = 3`.
Tipo do evento: `NOTIFICACAO_EXTERNA = 1`, `NOTIFICACAO_INTERNA = 2`.

#### Fluxo

1. O chamador monta um `Unico\Controller\NotificacaoController` com o evento, o `dataTemplate` (os modelos que alimentam o template) e opcionalmente destinatário, pedido, anexos, prioridade, origem, reply-to.
2. `notificar()` bifurca por `ev_tipo`: **interna** cria `NotificacaoAdmin` (com deduplicação por token não lido e agendamento opcional); **externa** segue adiante.
3. **Renderização** — os templates ficam **no banco** (`ev_template`, `ev_template_titulo`), não em arquivo, e são renderizados por um motor de template a partir de string, com o `dataTemplate` como contexto. Renderiza **uma vez** e reaproveita para todos os destinatários.
4. Persiste **uma linha de `un_notificacao` por destinatário**, em `SITUACAO_FILA`, já com o conteúdo renderizado.
5. **Envio assíncrono** — um job varre a fila e envia por canal. Há jobs separados para push e para notificações agendadas.

#### Como um evento se amarra a um ponto do sistema

Dois padrões coexistem, e vale escolher **um** no produto novo:

- **Codificado por domínio** — o controller do ponto busca o `TipoNotificacao` por constante, filtra os eventos ativos e dispara. Simples, mas o ponto de disparo vive no código.
- **Configurável** — no motor de processos, a linha de `processo.pro_fluxo_tipo_tramite` aponta um evento (`ev_id`) **ou** um grupo de envio (`gre_id`), mais o destino (`flutip_destino_notificacao`). O disparo é cadastro.

> **✅ Copiar:** template no banco renderizado uma vez para N destinatários, e fila persistida com estado por destinatário. É o que torna "por que o cliente não recebeu?" uma pergunta respondível.

#### Grupo de envio: política de canais, não de pessoas

**Entidade:** `Unico\Model\GrupoEnvio` → `un_grupo_envio` (só `gre_id` + `gre_nome`)
**Laço:** `Unico\Model\GrupoEnvioTipoNotificacaoEvento` → `un_grupo_envio_tipo_notificacao_evento` (N:N com evento)

Um grupo de envio agrupa N eventos, tipicamente **um por canal** para a mesma mensagem. O motor escolhe qual usar em runtime, por ordem de prioridade e disponibilidade de contato: `[WhatsApp, E-mail, SMS]`, ou o canal de marketplace quando o pedido é de um marketplace que aceita abertura de atendimento.

> **Cuidado com o nome:** não é um grupo de destinatários. Não existe, no motor de processos, lista estática de pessoas — os destinatários são sempre derivados do contexto (seção 6.7).

### 4.6 Campos customizados: características

Mecanismo de extensão de formulário **sem alterar esquema**, reaproveitado em seis lugares do sistema.

| Classe | Tabela | Papel |
|---|---|---|
| `Unico\Model\Produto\GrupoCaracteristica` | `produto.pr_grupo_caracteristica` | O grupo tipado. É o grupo, não a característica, que se pluga num ponto do sistema |
| `Unico\Model\Produto\Caracteristica` | `produto.pr_caracteristica` | A definição do campo: nome, tipo de valor, obrigatoriedade, ordem, ajuda, default, ícone, `car_config` (json) |
| `Unico\Model\Processo\ProcessoTramiteCaracteristica` | `processo.pro_processo_tramite_caracteristica` | O **valor**, no contexto do trâmite |

Tipos de grupo (`GrupoCaracteristica::TIPO_*`):

- `TIPO_PRODUTO = 1`
- `TIPO_PROCESSO = 2`
- `TIPO_PERSONALIZACAO = 3`
- `TIPO_COMPOSICAO_PRODUTO = 4`
- `TIPO_AVALIACAO_PRODUTO = 5`

Tipos de valor (`Caracteristica::TIPO_*`):

- `TIPO_LISTA = 1` — opções em `car_lista_opcoes` (json)
- `TIPO_LISTA_MULTIPLA = 2`
- `TIPO_TEXTO = 3`
- `TIPO_BOOLEANO = 4`
- `TIPO_NUMERO = 5`
- `TIPO_GRID_MULTIPLO = 6`
- `TIPO_ARQUIVO = 7`

**O valor é EAV com serialização em texto:** `pro_processo_tramite_caracteristica` tem PK composta (`protra_id`, `car_id`) e uma única coluna `procar_valor` (text, nullable). Listas múltiplas são gravadas como JSON nessa coluna.

**Como se pluga numa etapa de workflow:** a linha de `processo.pro_fluxo_tipo_tramite` aponta um `grupo_id`; o formulário daquela etapa renderiza exatamente as características daquele grupo. Trocar os campos de uma etapa é trocar o grupo apontado.

O mesmo mecanismo aparece em `produto.pr_produto_caracteristica`, `produto.pr_categoria_grupo_caracteristica`, composição de produto, avaliação de produto e nos mapeamentos de atributo para marketplaces.

> **✅ Copiar:** o conceito de grupo tipado plugável por ponto de uso. É o que evita que "adicionar um campo na etapa de laudo" vire migration.

> **⚠ Ponto de atenção:** valor único em coluna `text` significa **sem tipagem e sem consulta eficiente**. Filtrar processos por valor de característica hoje é varredura. Se o produto novo precisa disso, considere coluna por tipo ou JSON indexado.

### 4.7 Convenções de modelagem que se repetem

Quatro padrões aparecem em todos os domínios e vale decidi-los de uma vez:

**Configuração em JSON lida por *value object*.** Várias entidades têm uma coluna `*_configuracao` do tipo JSON, e um objeto sem tabela que a interpreta com getters nomeados. Exemplos: `OrigemAtendimentoConfiguracao.orconf_configuracao` → `OrigemAtendimentoConfiguracaoModel`; `TextoPadrao.tex_configuracao` → `TextoPadraoConfiguracao`; `FluxoGrupo.flu_configuracao` → `FluxoGrupoConfiguracaoModel`; `FluxoConstrutorParametro.fcp_configuracao` → cinco objetos de `Configuracao/`. *Benefício*: acrescentar opção de configuração não é migration. *Custo*: nada valida o JSON, e a evolução de formato é por conta do código.

**`codigo_integracao`.** Várias entidades cadastráveis têm uma coluna de código externo (`flu_codigo_integracao`, `tiptra_codigo_integracao`). É o que permite um sistema de fora referenciar "o fluxo de devolução" sem conhecer o id interno. **Copiar** — integrações sempre precisam disso, e improvisar depois é caro.

**Flags de comportamento no orquestrador.** `ProcessoController` tem uma dúzia de setters (`setRealizaTramiteInicial()`, `setForcePessoaAbertura()`, `setNotificaOrigemProcesso()`…) que mudam o que a abertura faz. *Benefício*: um ponto de entrada para 8 módulos. *Custo*: a combinação de flags não é validada, e existem combinações que não fazem sentido. No produto novo, prefira **casos de uso nomeados** ("abrir processo a partir de atendimento", "abrir processo automático por evento") sobre um construtor com doze interruptores.

**Chave de negócio composta e legível.** Número de processo e código de centro de custo não são o id do banco. São compostos, legíveis e pronunciáveis por telefone (seções 6.6 e 7.3). **Copiar o princípio**, com as ressalvas de cada seção.

---

## 5. Domínio: Atendimento

### 5.1 Problema de negócio

A empresa precisa responder pessoas de fora. Essas pessoas chegam por canais tecnicamente muito diferentes — e-mail, mensageria, APIs de marketplace, formulários do site — mas **o trabalho de responder é o mesmo trabalho**. Sem uma unidade comum, cada canal vira um sistema separado, com sua própria fila, seu próprio prazo e sua própria caixa de entrada que alguém esquece de olhar.

### 5.2 Modelo de dados

#### `Atendimento` — a unidade de trabalho

`CRM\Model\Atendimento` → `crm.crm_atendimento` (estende `EmpresaBase`)

Índices: `ate_situacao`, `ate_telefone`, `ate_origem`, `ate_data_hora`.

| Coluna | Tipo | Nulo | Significado |
|---|---|---|---|
| `ate_id` | int | não | PK |
| `ate_numero_protocolo` | varchar(120) | sim | **Campo multiuso**: protocolo de um marketplace, protocolo de devolução, ou identificador de conta no messenger. Ver ressalva abaixo |
| `ate_data_hora` | datetime tz | não | **Reescrita na reabertura** — não é a data de criação original |
| `ate_email` | varchar(255) | sim | E-mail do cliente |
| `ate_email_origem` | varchar(255) | sim | A **caixa** que capturou a mensagem. Distinto do anterior |
| `ate_nome` | varchar(255) | sim | Nome informado |
| `ate_telefone` | varchar(50) | sim | |
| `ate_numero_pedido` | varchar(50) | sim | Número textual, antes de resolver o vínculo |
| `ate_situacao` | smallint | sim | Ver 5.4 |
| `ate_motivo_cancelamento` | smallint | sim | Ver 5.4 |
| `ate_modalidade` | smallint | sim | `1 = DUVIDA_CLIENTE`, `2 = PEDIDO` |
| `ate_origem` | bigint | sim | **O canal.** Não é FK. Ver 5.3 |
| `ate_nova_mensagem` | boolean | sim | Flag de não lido |
| `usu_id_cliente` | FK → `Usuario` | sim | Quem abriu, quando é usuário do site |
| `ped_id` | FK → `Pedido` | sim | Vínculo com o pedido |
| `usu_id_responsavel` | FK → `Usuario` | sim | Quem atende |
| `ate_ultima_mensagem_id` | FK → `AtendimentoMensagem` | sim | Denormalização para a listagem |
| `ate_log_email_copia` | json | sim | Log das cópias enviadas |
| `proder_id` | FK → `ProdutoDerivacao` | sim | O SKU em questão |
| `mot_id` | FK → `MotivoAtendimento` | sim | Classificação do assunto |
| `loj_id` | FK → `Loja` | sim | **Necessário** porque atendimento sem pedido não teria como saber a loja |
| `ate_situacao_marketplace` | smallint | sim | Estado da devolução: `1` enviada, `2` aguardando, `3` recebida, `4` finalizada |
| `ate_revisado` | boolean | sim | Alimenta a fila de revisão |
| `ate_data_hora_sessao` | datetime tz | sim | Janela de sessão dos bots de resposta |
| `mkp_id` | FK → `Marketplace` | sim | Quando o canal é de marketplace |
| `ate_id_pai` | FK → `Atendimento` | sim | **Autorrelação.** Um fluxo pode abrir atendimento de negociação a partir de outro. `ON DELETE SET NULL` |
| `ate_panobot_parametros` | json | sim | Estado da conversa com a IA. Ver 5.8 |
| `ate_identificador_plataforma_externa` | varchar(255) | sim | Chave do atendimento no canal externo |
| `ate_data_plataforma_externa` | json | sim | Payload bruto do canal |

> **🚫 Não replicar:** `ate_numero_protocolo` como campo multiuso. Três significados diferentes na mesma coluna, distinguidos só pela origem. No produto novo, use uma tabela de identificadores externos (`atendimento_id`, `sistema`, `chave`) ou colunas nomeadas.

#### `AtendimentoMensagem` — o turno da conversa

`CRM\Model\AtendimentoMensagem` → `crm.crm_atendimento_mensagem`

| Coluna | Tipo | Nulo | Significado |
|---|---|---|---|
| `atemsg_id` | bigint | não | PK |
| `ate_id` | FK → `Atendimento` | não | |
| `atemsg_data_hora` | datetime tz | não | |
| `atemsg_data_hora_limite` | datetime tz | sim | **O prazo (SLA).** Calculado no persist. Ver 5.5 |
| `atemsg_data_hora_visualizada` | datetime tz | sim | |
| `atemsg_observacao` | text | **sim** | **O texto da mensagem.** Nulo quando a mensagem é só anexo |
| `not_id` | FK → `Notificacao` | sim | Quando a mensagem saiu por notificação |
| `usu_id` | FK → `Usuario` | sim | Autor interno. Nulo = mensagem do cliente |
| `atemsg_situacao` | smallint | não | `1 = NOVA`, `2 = LIDA`, `3 = REMOVIDA` |
| `atemsg_tipo` | smallint | não | `1 = PADRAO`, `2 = EVENTO` |
| `protra_id` | FK → `ProcessoTramite` | sim | **A ponte de volta do processo.** Ver 6.8 |
| `atemsg_assunto` | text | sim | |
| `atemsg_respondido` | boolean | não | Default false |
| `atemsg_revisado` | boolean | sim | Entra no indicador de revisões |
| `atemsg_params` | json | sim | Metadados da resposta (ex.: solução escolhida em marketplace) |
| `atemsg_manual` | boolean | não | Distingue digitada de gerada |
| `atemsg_identificador_plataforma_externa` | varchar(255) | sim | Id da mensagem no canal |
| `atemsg_id_mensagem_pai` | FK → `AtendimentoMensagem` | sim | Encadeamento de respostas |

Callback relevante: o `@PostPersist` da mensagem **enfileira o bot** (seção 10.3).

#### Demais entidades do núcleo

| Classe | Tabela | Conteúdo |
|---|---|---|
| `AtendimentoMensagemAnexo` | `crm.crm_atendimento_mensagem_anexo` | Liga mensagem a `DocumentoEletronico`. Validação de extensão e tamanho vem da configuração do canal |
| `AtendimentoLog` | `crm.crm_atendimento_log` | `atelog_situacao`, `atelog_observacao`, `atelog_data_array` (json), `atelog_tipo` (`1` padrão, `2` formulário, `3` alteração de pedido), `usu_id`, `atelog_data_hora` |
| `AtendimentoProduto` | `crm.crm_atendimento_produto` | `ate_id` + `proder_id`. Quais SKUs a conversa trata |
| `AtendimentoProdutoAssistencia` | `crm.crm_atendimento_produto_assistencia` | `atepro_id` + outro `proder_id` + `ateproass_quantidade`. Desdobramento por peça: permite pedir três peças de reposição diferentes dentro do mesmo produto |
| `MotivoAtendimento` | `crm.crm_motivo_atendimento` | `mot_descricao`, `mot_ativo` |
| `AtendimentoFluxoConstrutorSessao` | `crm.crm_atendimento_fluxo_construtor_sessao` | Liga atendimento à sessão do motor de fluxo. Ver 10.4 |

### 5.3 O canal de entrada

O canal é um `bigint` em `ate_origem`. **Não é FK** — e vem de duas fontes ao mesmo tempo.

#### As 39 origens fixas

Constantes de `CRM\Model\AtendimentoOrigemInterface`. Os identificadores 27 e 39 estão vagos por descontinuação.

**Site e loja própria**
- `ORIGEM_SITE = 1`
- `ORIGEM_SITE_ASSISTENCIA = 5`
- `ORIGEM_COMENTARIO = 11`

**E-mail (caixas monitoradas)**
- `ORIGEM_EMAIL = 2`
- `ORIGEM_EMAIL_LOGISTICA = 7`
- `ORIGEM_EMAIL_LOGISTICA_TRANSPORTADORA = 8`
- `ORIGEM_EMAIL_TROCA = 15`
- `ORIGEM_EMAIL_RASTREIO = 16`
- `ORIGEM_JURIDICO = 23`
- `ORIGEM_CHARGEBACK = 9`

**Mensageria direta**
- `ORIGEM_WHATSAPP = 22`
- `ORIGEM_META_MESSENGER = 25`
- `ORIGEM_META_INSTAGRAM = 26`

**Comentários de rede social**
- `ORIGEM_META_COMENTARIO_INSTAGRAM = 40`
- `ORIGEM_META_COMENTARIO_FACEBOOK = 41`

**Marketplaces** — o padrão é um trio pedido / produto / reclamação por marketplace
- Mercado Livre: `3` produto, `4` pedido, `6` reclamação, `37` reclamação em mediação
- Magazine Luiza: `10` pedido, `24` produto, `31` conversa
- Shopee: `28` pergunta, `29` pedido, `30` produto
- TikTok Shop: `32` pedido, `33` produto, `34` geral
- Kabum: `18` pedido, `19` oferta
- Avulsos: `12` Colombo, `17` Leroy Merlin, `21` Via Varejo, `35` Amazon manual, `36` MadeiraMadeira
- Arquivados (não recebem mais): `13` e `14` SkyHub/B2W

**Pós-venda transversal**
- `ORIGEM_DEVOLUCAO_MARKETPLACE = 20`
- `ORIGEM_POS_VENDA_PEDIDOS = 38`

#### As origens criadas por cadastro

`Atendimento::getOrigemList()` soma às constantes o resultado de uma consulta em `crm.crm_origem_atendimento_configuracao`: **toda linha com `orconf_nome` preenchido vira uma origem nova**, disponível na interface, sem release.

O identificador de uma origem criada é gerado a partir do **timestamp Unix em segundos**, incrementando em caso de colisão — é isso que garante que não colida com a faixa fixa 1–41.

> **⚠ Armadilha:** `getOrigemList()` guarda o resultado em cache estático de requisição. Depois de criar uma origem, é obrigatório chamar `Atendimento::clearOrigemListCache()` — senão ela só aparece na requisição seguinte.

> **🚫 Não replicar:** o identificador por timestamp. Ele existe para conviver com uma faixa fixa herdada. Num produto novo não há faixa reservada a evitar: use identificador natural e faça **todo** canal ser registro (seção 14.3).

#### Configuração do canal

`CRM\Model\OrigemAtendimentoConfiguracao` → `crm.crm_origem_atendimento_configuracao`

| Coluna | Tipo | Significado |
|---|---|---|
| `ate_origem` | bigint | **PK.** É o próprio identificador da origem, não um id sequencial |
| `orconf_nome` | varchar(255) | Preenchido só nas origens criadas por cadastro — é o que as faz aparecer na lista |
| `orconf_configuracao` | json | **Todo o comportamento do canal** |
| `mkp_id` | FK → `Marketplace` | Quando a origem representa um canal de marketplace |

O JSON é lido pelo *value object* `CRM\Model\OrigemAtendimentoConfiguracaoModel` (sem tabela). Chaves de comportamento:

| Getter | O que controla |
|---|---|
| `getDiasLimiteAtendimento()` | Dias do SLA |
| `getHorasLimiteAtendimento()` | Horas do SLA |
| `getCondiderarApenasDiasUteis()` | Se a contagem é em dias úteis *(o typo está no fonte)* |
| `getDiasLimiteVinculoAutomaticoPedido()` | Janela para casar automaticamente com um pedido |
| `getUtilizaAnexos()` | Se o canal aceita anexo |
| `getTamanhoMaximoAnexo()` | Teto de tamanho |
| `getExtensoesAnexos()` | A lista de extensões |
| `getTipoPermissaoExtensoesAnexos()` | `TIPO_EXTENSAO_PERMITIDA = 1` ou `TIPO_EXTENSAO_NAO_PERMITIDA = 2` — **a mesma lista serve de allowlist ou denylist** conforme este campo |
| `isSelectedExtension()` | Consulta de uma extensão |
| `getQuantidadeMinimaCaracteres()` / `getQuantidadeMaximaCaracteres()` | Limites de tamanho da mensagem |
| `getRespotaEmailComCopia()` | Se responde com cópia *(typo no fonte)* |
| `getPermiteCadastroManual()` | Se o operador pode abrir atendimento nessa origem |
| `getTipoIntegracao()` e derivados | Integração de IA. Ver seção 10 |

#### Quem atende e de onde vêm os e-mails

`CRM\Model\OrigemAtendimentoUsuario` → `crm.crm_origem_atendimento_usuario`

Amarra a origem a um **usuário** ou a um **centro de custo** — é o roteamento da fila. Dois unique constraints parciais e independentes:

```sql
uq_origem_atendimento_usuario      UNIQUE (ate_origem, usu_id)     WHERE usu_id IS NOT NULL
uq_origem_atendimento_centro_custo UNIQUE (ate_origem, cencus_id)  WHERE cencus_id IS NOT NULL
```

`CRM\Model\OrigemAtendimentoCaixaEntradaEmail` → `crm.crm_origem_atendimento_caixa_entrada_email` (`caixa_email` é **unique**) — diz qual endereço alimenta qual origem. É o que faz uma mensagem que chega em `logistica@` virar `ORIGEM_EMAIL_LOGISTICA` em vez de `ORIGEM_EMAIL`.

### 5.4 Máquina de estados

#### Os estados

| Constante | Valor | Rótulo | Natureza |
|---|---|---|---|
| `SITUACAO_ABERTO` | 1 | Aberto | Negócio — na fila, sem dono |
| `SITUACAO_EM_ANDAMENTO` | 2 | Em Andamento | Negócio — alguém assumiu |
| `SITUACAO_FINALIZADO` | 3 | Finalizado | Negócio — terminal |
| `SITUACAO_IGNORADO` | 4 | Ignorado/Cancelado | Negócio — terminal, exige motivo |
| `SITUACAO_TRAMITADO` | 5 | Trâmite | **Transitório** — ver abaixo |
| `SITUACAO_LOCK` | 6 | *(sem rótulo)* | **Infraestrutura** — ver abaixo |
| `SITUACAO_PENDENTE_VERIFICACAO` | 7 | Pendente Verificação | Negócio — terminal com ressalva |

`getSituacaoList()` devolve apenas 1, 2, 3, 4 e 7. `getSituacaoLogList()` acrescenta o 5. **O 6 não aparece em lista nenhuma.**

**`SITUACAO_LOCK` é uma reserva de fila, não um estado.** `AtendimentoRepository::buscaProximoAtendimento()` executa:

```sql
UPDATE crm.crm_atendimento
   SET ate_situacao = 6      -- LOCK
 WHERE ate_id = :atendimento
   AND ate_situacao = 1      -- ABERTO
```

Se o update afeta zero linhas, outro operador pegou primeiro e o método se rechama. É um *claim* atômico que dura milissegundos.

**`SITUACAO_TRAMITADO` existe entre dois flushes.** `AtendimentoController::tramite()` grava TRAMITADO **apenas para produzir a linha de histórico** com o texto da transferência, e no flush seguinte volta para EM_ANDAMENTO já com o novo responsável.

> **🚫 Não replicar:** estado de negócio servindo de trava de concorrência. Reserva de fila é infraestrutura — mantenha fora do enum, numa coluna própria (`reservado_por`, `reservado_ate`) ou numa trava de banco. E não use um estado transitório só para gerar log: o log deve poder registrar uma transferência sem que o registro passe por um estado falso.

#### Motivos de cancelamento

- `MOTIVO_CANCELAMENTO_DUPLICADO = 1`
- `MOTIVO_CANCELAMENTO_INVALIDO = 2`
- `MOTIVO_CANCELAMENTO_EMAIL_SPAM = 3`
- `MOTIVO_CANCELAMENTO_AGRADECIMENTO_CLIENTE = 4`

#### As transições, com gatilho, guarda e efeito

Todas concentradas em `CRM\Controller\AtendimentoController`.

| Método | De → Para | Gatilho | Guarda | Efeito colateral |
|---|---|---|---|---|
| `newAtendimentoSite()` | — → `ABERTO` | Formulário do site, captura de e-mail, API | Passa por `validarSpam()` antes | Cria mensagem `NOVA`; dispara e-mail de confirmação |
| `newAtendimentoComentarioSite()` | — → `ABERTO` | Comentário de produto | idem | idem |
| `newAtendimentoMensageiroSite()` / `...SitePedido()` | — → `ABERTO` | Mensageiro do site | idem | idem |
| `init()` | `ABERTO`/`FINALIZADO`/`IGNORADO`/`LOCK` → `EM_ANDAMENTO` | Operador abre a tela | Lança exceção se não houver usuário na sessão | Grava o responsável |
| `tramite()` | `*` → `TRAMITADO` → `EM_ANDAMENTO` | Transferência para outro usuário | — | Duas gravações; a primeira só gera log |
| `liberar()` | `EM_ANDAMENTO` → `ABERTO` | Operador devolve à fila | **Exige `permiteResponsavelNulo = true`** | Zera o responsável |
| `finalizar()` | `*` → `FINALIZADO` | Encerramento | — | **Limpa o motivo de cancelamento** |
| `pendenteVerificacao()` | `*` → `PENDENTE_VERIFICACAO` | Encerramento com ressalva | — | Limpa o motivo de cancelamento |
| `reabrir()` | `FINALIZADO` → `ABERTO` | Nova mensagem do cliente | — | Zera o responsável, **reescreve `ate_data_hora`**, atualiza a última mensagem |
| *(sem método)* | `*` → `IGNORADO` | `AtendimentoFormController` | Exige motivo de cancelamento | — |

#### A trava de regressão de estado

`Atendimento::preUpdate()` contém uma regra que não aparece em nenhuma interface: **um atendimento `EM_ANDAMENTO` não pode voltar para `ABERTO`**. Se o *changeset* mostra essa transição, o callback a desfaz e ainda restaura o responsável anterior.

A única forma de liberar de fato é setar `permiteResponsavelNulo = true` antes — exatamente o que `liberar()` faz.

O motivo histórico é real: vários caminhos automáticos estavam devolvendo à fila atendimentos que já tinham dono, e a fila ficava com trabalho duplicado.

> **⚠ Armadilha:** a trava **falha silenciosamente**. Código que tenta devolver um atendimento à fila sem a flag não recebe erro — a gravação acontece e o valor volta ao anterior. Se replicar a regra, faça-a **lançar**, não desfazer em silêncio.

### 5.5 O prazo é da mensagem, não do atendimento

Cada mensagem recebe um vencimento calculado no momento em que é gravada, a partir da configuração do canal. É isso que permite medir a resposta turno a turno numa conversa longa, em vez de medir só o tempo total até o encerramento.

**Com dias úteis desligado:** soma `dias` como intervalo corrido e depois `horas` como intervalo corrido.

**Com dias úteis ligado:**

1. soma `dias` como **dias úteis**;
2. soma `horas` como intervalo corrido;
3. **se o resultado cair em dia não útil, empurra para o próximo dia útil.**

O passo 3 é o que costuma faltar em implementações ingênuas: sem ele, uma mensagem recebida sexta às 17h com prazo de 4 horas venceria sábado de madrugada.

### 5.6 Extensão: plugar um canal conversacional

`AtendimentoOrigemInterface` **não é um contrato de plugin** — é um contêiner de constantes. O contrato real são três peças:

**1. Processador de webhook** — estende `CRM\Model\Webhook\AbstractWebhookAtendimentoBaseProcessor`, cujo `processar()` é `final` (Template Method). O canal implementa quatro métodos:

```php
abstract protected function getPayloadMensagens(array $payload): array;
abstract protected function getIdentificadorRemetente(array $mensagemPayload): string;
abstract protected function getLockPrefix(): string;
abstract protected function resolverStrategy(array $mensagemPayload): CrmOrigemFormatterStrategyInterface;
```

Ganchos opcionais: `prepararAtendimentoFormatter()`, `prepararMensagemFormatter()`, `prepararMensagem()`, `prepararAnexoFormatter()`, `isRemocao()`, `processarRemocao()`, `postPersistNewAtendimento()`, `postPersistNewMessage()`, `getAsyncUrl()`.

**2. Estratégia de formatação e resposta** — implementa `CRM\Model\Formatter\Strategy\CrmOrigemFormatterStrategyInterface`:

```php
public function getOrigem(): int;
public function createAtendimentoFormatter(): AtendimentoCrmBaseFormatterResponse;
public function createMensagemFormatter(): AtendimentoMensagemCrmBaseFormatterResponse;
public function createAnexoFormatter(): ?AtendimentoMensagemAnexoCrmBaseFormatterResponse;
public function reply(AtendimentoMensagem $mensagem, LojaConfiguracao $lojaConfiguracao, array $anexos = []): EventResponse;
```

**3. Um `case` em `CrmOrigemFormatterFactory::forOrigem()`** — o mapa origem → estratégia. Origem sem entrada lança exceção.

Mais um controller de webhook na borda, que valida e delega.

#### O que o Template Method resolve de graça

Não reescrever por canal:

- **transação por mensagem**, com rollback e rethrow;
- **trava de concorrência** — `SELECT pg_advisory_xact_lock(hashtext(?))` com chave `prefixo:identificador:loja`, tomada dentro da transação e liberada com ela. É o que impede que duas entregas simultâneas do mesmo webhook criem dois atendimentos para o mesmo cliente;
- **cache do atendimento dentro do lote** — várias mensagens do mesmo remetente caem no mesmo atendimento;
- **liberação de responsável inativo** — se o dono do atendimento está desativado, o atendimento é solto;
- **notificação assíncrona da interface** ao fim de cada mensagem.

Canais implementados hoje: WhatsApp, Meta Messenger, Instagram Direct, comentários de Facebook e de Instagram.

> **Fronteira importante:** o CRM tem a camada de *atendimento* do WhatsApp. O **transporte** da API vive noutro módulo (`Unico\Controller\WhatsApp\*`, `Unico\Model\WhatsApp\*`). O Meta é inteiramente interno ao CRM. **Não são simétricos** — é dívida de organização, não desenho.

### 5.7 Contenções

Três mecanismos com propósitos completamente diferentes e nomes que os confundem.

| Mecanismo | Entidade / tabela | Problema que resolve |
|---|---|---|
| **Filtro de entrada** | `SpamAtendimento` → `crm.crm_spam_atendimento` (`spam_email`, `spam_termo`, `spam_origem`) | Caixas públicas recebem marketing e automação de terceiros. Sem filtro, cada um vira unidade de trabalho para alguém fechar à mão |
| **Denylist de confirmação** | `AtendimentoRespostaAutomaticaBloqueio` → `crm.crm_atendimento_resposta_automatica_bloqueio` (só `resblq_email`) | Quando o remetente é um robô, a confirmação de abertura gera nova mensagem, que abre outro atendimento, que gera outra confirmação |
| **Restrição de texto livre** | `UsuarioBloqueioMensagem` → `crm.crm_usuario_bloqueio_mensagem` (`usu_id`, `msg_livres_bloqueio_msg`) | Operador novo ou terceirizado respondendo o que quer em nome da marca |

**Filtro de entrada** — cada linha cadastra **ou** um e-mail **ou** um termo, opcionalmente restrito a uma origem. `AtendimentoController::validarSpam()` roda antes da abertura: converte o HTML em texto puro e consulta as duas listas. Batendo qualquer uma, o atendimento simplesmente não é criado.

**Denylist de confirmação** — `sendNewAtendimentoEmail()` carrega a lista e, se o e-mail do atendimento estiver nela, não envia. O mesmo método tem uma segunda guarda: atendimento **sem e-mail de contato não envia nada**, para evitar o fallback silencioso para a caixa da loja.

**Restrição de texto livre** — apesar do nome, **não bloqueia cliente**. `AtendimentoFormController::defineBloqueioMensagemUsuario()` lê a linha do usuário logado e decide entre liberar o campo de mensagem ou travá-lo para uso exclusivo do catálogo.

> **🚫 Não replicar os nomes.** "Bloqueio de mensagem do usuário" que controla o operador interno é a pior ambiguidade do módulo. Nomeie pelo que faz: `RestricaoTextoLivreOperador`, `DenylistConfirmacaoAutomatica`, `FiltroEntradaSpam`.

#### A quarta contenção: silenciar o bot

Não é entidade. É uma flag dentro de `ate_panobot_parametros`, manipulada por `CRM\Model\AtendimentoPanobotParametros`:

```php
public function setReplyBlocked(bool $replyBlocked, bool $reabrirAtendimento = true);
public function getReplyBlocked(): ?bool;
public function saveConfig();
```

**Problema que resolve:** quando a conversa passa para um humano, o bot precisa calar a boca **naquele atendimento**, sem que se desligue a IA da origem inteira. É reversível, e a mudança é registrada no histórico.

Também expõe `getParametrosLastRequest(bool $consideraAtendimentosRelacionados = true)` — os parâmetros da última chamada à IA. O flag faz a busca subir para atendimentos relacionados do mesmo cliente, para o bot não perder contexto quando a conversa se fragmenta.

### 5.8 Catálogo de respostas: texto padrão

`CRM\Model\TextoPadrao` → `crm.crm_texto_padrao`

| Coluna | Tipo | Significado |
|---|---|---|
| `tex_id` | int | PK |
| `tex_tipo` | smallint | Ver abaixo |
| `tex_titulo` | varchar(120) | |
| `tex_texto` | text | **Template** (motor de template com variáveis) |
| `tex_descricao` | text | Usada pela IA para escolher o texto |
| `tex_configuracao` | json | Lido por `TextoPadraoConfiguracao` |
| `usu_id` | FK → `Usuario` | Preenchido no tipo Usuário |
| `cencus_id` | FK → `CentroCusto` | Preenchido no tipo Centro de Custo |

**Cinco tipos, dois públicos:**

Uso humano — o operador escolhe na tela:
- `TIPO_GLOBAL = 1`
- `TIPO_USUARIO = 2`
- `TIPO_CENTRO_CUSTO = 3`

Uso da IA — são as respostas que o agente pode dar:
- `TIPO_PANOBOT_IA_ATENDIMENTO = 4`
- `TIPO_PANOBOT_IA_PROCESSO_TRAMITE = 5`

`getTipoList()` recebe dois flags (`$ignorePanoBotIA`, `$ignoreUseSelect`) justamente porque a mesma tabela alimenta dois seletores que nunca devem se misturar.

> **Copiar adaptando:** o conceito de catálogo com variáveis é excelente. Mas **separe as duas tabelas**. Público, ciclo de vida, quem edita e critério de seleção são diferentes, e conviver na mesma estrutura produziu seletores com flags para não se misturarem.

#### `TextoPadraoConfiguracao` — o que o texto faz além de ser texto

*Value object* sobre `tex_configuracao`:

- `TIPO_RESPOSTA_MENSAGEM = 1` × `TIPO_RESPOSTA_PARAMETROS = 2` — se o texto vira mensagem para o cliente ou um JSON de parâmetros consumido por quem chamou
- `TIPO_RENDER_RESPOSTA_PARAMETRO_TEXTO = 1` × `TIPO_RENDER_RESPOSTA_PARAMETRO_JSON = 2` — como cada parâmetro é serializado
- `getRealizaAberturaProcesso()`, `getFluxo()`, `getFluxoAtualSituacao()`, `getDescricaoAbertura()`, `getPrioridade()` — **um texto padrão pode abrir um processo ao ser usado**
- `getProcessoIncluirAtendimentoMensagem()` — se a mensagem entra no processo
- `getReplyBlocked()` — o texto pode silenciar o bot ao ser respondido

#### Sugestão contextual ao operador

`AtendimentoController::loadListTextoSituacao()` não devolve o catálogo inteiro. Filtra pelas condições em `tex_configuracao.opcoes`:

- `situacoesPedido` — só sugere se o pedido estiver numa dessas situações
- `emAtraso` — restringe a pedidos atrasados
- `canalVenda` — restringe por marketplace/loja do pedido
- `pedidoNaoEncontrado` — textos para atendimento sem pedido vinculado

Quando situação **e** canal estão configurados, ambos precisam bater. **Se o filtro não sobra nada, devolve a lista completa** em vez de uma tela vazia.

#### Resolução do template

Mecanismo em `ProcessTextoPadraoResponse::getResponse()`, e é o ponto mais replicável do catálogo:

1. `getRequiredVariables()` varre o texto procurando **quais variáveis do catálogo aparecem ali** (busca de substring);
2. **só essas** são carregadas do ERP;
3. o texto é renderizado pelo motor de template;
4. o que sobrar de `{variavel}` simples é resolvido por substituição direta, como rede de segurança.

> **✅ Copiar:** o custo de montar uma resposta é proporcional ao que o texto **realmente cita**, não ao tamanho do catálogo de variáveis. Variável não citada = zero consulta. Reimplementar sem essa granularidade degrada a performance de forma difícil de recuperar depois.

O catálogo completo de variáveis está na seção 10.6.

#### Mensagens de WhatsApp

`CRM\Model\MensagemPadraoWhatsApp` **não é entidade** — é um DTO serializável usado para montar a lista de modelos da tela:

- `TIPO_TEXTO_PADRAO = 1` — texto do catálogo
- `TIPO_TEMPLATE = 2` — template aprovado na plataforma, **único formato aceito fora da janela de 24 horas**

### 5.9 FAQ

| Classe | Tabela | Conteúdo |
|---|---|---|
| `FaqGrupo` | `crm.crm_faq_grupo` | `faqgru_nome`, `loj_id` **obrigatório**, autorrelação `faqgru_id_pai` (árvore de categorias por loja) |
| `FaqDuvida` | `crm.crm_faq_duvida` | `faqduv_pergunta`, `faqduv_resposta`, `faqgru_id` obrigatório |

O FAQ é **conteúdo público da loja**, não catálogo de resposta do operador. São coisas separadas de propósito, e a confusão entre as duas é comum.

### 5.10 Solicitação de troca e reembolso

`CRM\Model\SolicitacaoTrocaReembolso` → `crm.crm_solicitacao_troca_reembolso`

| Coluna | Tipo | Significado |
|---|---|---|
| `str_id` | int | PK |
| `str_status` | smallint | Ver abaixo |
| `ped_id` | FK → `Pedido` | **Obrigatório** — a solicitação sempre nasce de um pedido |
| `str_current_step` | int | Passo do assistente no site |
| `cupom_id` | FK → `Cupom` | Quando o reembolso vira voucher |
| `str_opc_coleta` | int | `1 = POSTAGEM_CORREIOS`, `2 = TRANSPORTADORA` |
| `str_forma_reemb` | int | `1 = PIX`, `2 = TRANSFERENCIA_BANCARIA` |

Os oito status (`STATUS_*`): `1 INICIADA`, `2 EM_ANALISE`, `3 APROVADA`, `4 EM_ANDAMENTO`, `5 ITENS_RECEBIDOS`, `6 ITENS_RECEBIDOS_PARCIALMENTE`, `7 NEGADA`, `8 FINALIZADA`.

Satélites:

| Classe | Tabela | Conteúdo |
|---|---|---|
| `SolicitacaoTrocaReembolsoHistorico` | `crm.crm_solicitacao_troca_reembolso_historico` | `strh_status`, `usu_id`, `strh_data_hora`. **Repete as mesmas oito constantes**, de propósito: o histórico não deve mudar de significado quando o enum principal mudar |
| `SolicitacaoTrocaReembolsoPedidoItem` | `crm.crm_solicitacao_troca_reembolso_pedido_item` | `pedit_id`, `strpi_tipo_sol`, `strpi_recebido`. **O tipo é por item** (`TIPO_SOLICITACAO_*`): `1` troca, `2` reembolso, `3` reembolso por voucher, `4` assistência |
| `SolicitacaoTrocaReembolsoProdutoDerivacao` | `crm.crm_solicitacao_troca_reembolso_produto_derivacao` | Os SKUs escolhidos para a troca |

**O conceito a copiar:** o tipo de solicitação é **por item**, não pela solicitação. Uma mesma devolução pode pedir troca de um item e reembolso de outro. E `strpi_recebido` por item é o que separa "itens recebidos" de "recebidos parcialmente".

> **Copiar adaptando:** a duplicação do enum no histórico tem intenção correta (histórico imutável), mas se resolve melhor **guardando o rótulo** junto do valor, não duplicando constantes que vão divergir.

Existe apenas grid no admin; **não há form controller**. A solicitação é criada pelo cliente no site e o admin acompanha e decide.

### 5.11 Métricas

Cinco telas, todas servidas pelo mesmo repositório.

#### Como os números saem

Tudo é montado sobre uma **tabela temporária** criada com os filtros da tela e destruída ao fim da requisição. Ela é um join já filtrado de `crm_atendimento` com `crm_atendimento_log`, e é sobre ela que as métricas rodam.

**Duas medidas de tempo, que medem coisas opostas:**

**Atraso** (`performance_atendimento`) — para cada mensagem com prazo preenchido:

```
Σ ( data do log FINALIZADO  −  atemsg_data_hora_limite )
```

contando apenas a finalização que **não tem outro EM_ANDAMENTO depois dela** (o encerramento definitivo). Valor negativo = resposta dentro do prazo.

**Custo de atendimento** (`custo_atendimento`) — para cada log de EM_ANDAMENTO:

```
Σ ( primeira finalização do mesmo usuário  −  momento em que assumiu )
```

É o esforço gasto, independente de prazo.

Um operador pode ter atraso zero e custo altíssimo, ou o contrário. Ter as duas separadas é o que permite conversar sobre produtividade sem confundir com qualidade de serviço.

**Indicadores de apoio por usuário:** finalizados, cancelados, pedidos tocados, pedidos que acabaram cancelados depois do atendimento, média de mensagens por atendimento, e quantidade de mensagens revisadas.

> **🚫 Não replicar:** existe uma exceção **codificada em SQL** — quando a loja da sessão é uma loja específica, as duas colunas de tempo retornam `'00:00:00'` fixo. Regra de negócio de um tenant dentro de consulta compartilhada. Se um tenant não deve ver a métrica, isso é configuração, não `if` em SQL.

---

## 6. Domínio: Processo

### 6.1 Problema de negócio

Nem toda demanda se resolve numa conversa. "O produto chegou quebrado" vira coleta, laudo, decisão e reembolso — passos executados por setores diferentes, em ordem, com prazo e com rastro. Sem um motor genérico, cada tipo de demanda vira uma tela nova com uma máquina de estados própria, e a empresa acumula dezenas de mini-workflows incompatíveis.

**A evidência de que a generalização valeu:** hoje **8 módulos** consomem o mesmo motor sem nenhuma adaptação (seção 6.9).

### 6.2 O conceito central: o processo é a projeção do último trâmite

Esta é a ideia mais importante do domínio, e a que mais se perde numa cópia apressada.

O processo **não guarda seu próprio estado**. Ele tem quatro colunas — `flu_id_atual`, `tiptra_id_atual`, `cencus_id_atual`, `protra_id_atual` — e todas as quatro são **reescritas a cada trâmite** com os valores daquele trâmite, num único método:

```php
// ProcessoController::persisteProcessoTramite()
$processo->setProcessoTramiteAtual($tramite);
$processo->setFluxoAtual($tramite->getFluxo());
$processo->setTipoTramite($tramite->getTipoTramite());
$processo->setCentroCusto($tramite->getCentroCusto());
```

**O trâmite é o log de eventos; o processo é a view materializada da última linha.**

Consequências:

- o histórico é completo por construção, sem tabela de auditoria separada;
- consultar "onde está esse processo" é leitura simples, sem varrer o log;
- reconstruir o estado a partir do log é sempre possível, o que torna correções de dados auditáveis;
- e, crucialmente, **um processo pode mudar de fluxo no meio do caminho sem perder identidade**, porque o fluxo é atributo do trâmite, não do processo.

> **✅ Copiar.** Com estado próprio, log e realidade divergem — e divergem sempre, porque algum caminho atualiza um e esquece o outro. Com projeção, a divergência é impossível: só existe um lugar que escreve.

### 6.3 O desenho: fluxo

#### `Fluxo`

`Unico\Model\Processo\Fluxo` → `processo.pro_fluxo` (estende `EmpresaBase`)

| Coluna | Tipo | Nulo | Significado |
|---|---|---|---|
| `flu_id` | int | não | PK |
| `flu_nome` | varchar(200) | não | |
| `flu_descricao` | varchar(200) | não | |
| `flu_previsao_dia` | int | sim | Prazo do fluxo — dias |
| `flu_previsao_hora` | int | sim | Prazo — horas |
| `flu_previsao_minuto` | int | sim | Prazo — minutos |
| `flu_ativo` | boolean | não | Default true |
| `flu_configuracao` | json | sim | |
| `flu_codigo_integracao` | int | sim | Chave para sistemas externos referenciarem o fluxo |
| `flugru_id` | FK → `FluxoGrupo` | sim | |

**Cálculo do prazo** (`ProcessoController::defineDataPrevisao()`), executado **uma vez, na abertura**: os **dias são somados como dias úteis**; horas e minutos são somados como intervalo corrido. O resultado vai para `pro_data_hora_prevista`.

> **⚠ Limitação conhecida:** o prazo **não é recalculado** quando o processo muda de fluxo por transferência (seção 6.5). Um processo transferido para um fluxo com prazo maior mantém a previsão do fluxo original. Corrigir na cópia.

#### `FluxoGrupo`

→ `processo.pro_fluxo_grupo` (`flugru_nome`, `flu_configuracao` json)

Agrupa fluxos e é onde se configura a automação por IA do trâmite. As constantes moram aqui:

- `PANOBOT_IA_TIPO_INTEGRACAO_DIALOGFLOW_CX = 1`
- `PANOBOT_IA_TIPO_INTEGRACAO_CONSTRUTOR = 2`

> **⚠ Armadilha real:** o CRM tem constantes com o **mesmo nome** e **valores diferentes** (`1 = ES`, `2 = CX`, `3 = Construtor`). Dois módulos, mesma ideia, numeração incompatível. Não é intercambiável. Este é o exemplo mais caro de "nomes que enganam" (seção 15.13).

O JSON é lido por `FluxoGrupoConfiguracaoModel` (sem tabela), que reusa dois traits definidos **no módulo de CRM** — dependência invertida que vale evitar na cópia.

#### `FluxoAssunto`

→ `processo.pro_fluxo_assunto` (`flu_id`, `fluass_nome`, `fluass_ativo`)

Subclassificação do fluxo. Um fluxo "Devolução" pode ter assuntos "produto avariado", "arrependimento", "erro de envio". **O assunto é escolhido no trâmite, não no processo.**

#### `FluxoTipoTramite` — o coração do desenho

→ `processo.pro_fluxo_tipo_tramite`

É a tabela que responde "neste fluxo, quais etapas são possíveis e o que cada uma faz". Quase toda a configuração de comportamento do workflow mora aqui.

| Coluna | Tipo | Significado |
|---|---|---|
| `flu_id` | FK → `Fluxo` | |
| `tiptra_id` | FK → `TipoTramite` | A etapa |
| `flu_id_transferencia` | FK → `Fluxo` | Se preenchido, concluir esta etapa **joga o processo em outro fluxo** |
| `flutip_fluxo_transferencia_obrigatorio` | boolean | |
| `flutip_destino_notificacao` | smallint | `0` sem necessidade, `1` cliente, `2` transportadora, `3` fornecedor, `4` origem do processo |
| `flutip_quantidade_max_tramtie` | smallint | Teto de repetições da etapa *(o typo `tramtie` está no fonte)* |
| `flutip_obrigatoriedade_documentos` | boolean | Exige anexo para avançar |
| `flutip_automatizado_panobot` | boolean | A etapa é executada pela IA, não por humano |
| `ev_id` | FK → `TipoNotificacaoEvento` | Evento de notificação único |
| `gre_id` | FK → `GrupoEnvio` | Grupo de envio (política de canais) — alternativa ao anterior |
| `grupo_id` | FK → `GrupoCaracteristica` | **Os campos customizados da etapa** (seção 4.6) |
| `flu_configuracao` | json | |

Constantes de modo de notificação: `TIPO_NOTIFICACAO = 1`, `TIPO_GRUPO_ENVIO = 2`.

> **⚠ Atenção à nomenclatura:** `gre_id` e `grupo_id` parecem irmãos e não são. O primeiro é grupo de **eventos de notificação**; o segundo é grupo de **campos customizados**, e aponta para uma tabela de outro domínio (produto).

#### `FluxoCentroCusto`

→ `processo.pro_fluxo_centro_custo` (`flu_id`, `cencus_id`, `flucen_ordem`)

A **trilha organizacional**: por quais setores a demanda passa e em que ordem. O primeiro da lista é quem recebe o processo na abertura e na transferência.

### 6.4 A instância: processo e trâmite

#### `Processo`

`Unico\Model\Processo\Processo` → `processo.pro_processo` (estende `EmpresaBase`, com índices)

| Coluna | Tipo | Nulo | Significado |
|---|---|---|---|
| `pro_id` | int | não | PK |
| `pro_numero` | varchar(50) | não | **O identificador de negócio.** Ver 6.6 |
| `pro_prioridade` | smallint | não | `1` baixa, `2` normal, `3` alta, `4` urgente |
| `pro_data_hora_abertura` | datetime tz | não | |
| `pro_data_hora_prevista` | datetime tz | sim | Calculada na abertura |
| `pro_descricao_abertura` | text | sim | |
| `pro_origem` | smallint | sim | `1 = ORIGEM_MANUAL`, `2 = ORIGEM_AUTOMATICO` |
| `pro_numero_protocolo` | varchar(120) | sim | Protocolo externo |
| **`flu_id_atual`** | FK → `Fluxo` | **não** | **Projeção do último trâmite** |
| **`tiptra_id_atual`** | FK → `TipoTramite` | **não** | **Projeção do último trâmite** |
| **`cencus_id_atual`** | FK → `CentroCusto` | **não** | **Projeção do último trâmite** |
| **`protra_id_atual`** | FK → `ProcessoTramite` | sim | **Projeção do último trâmite** |
| `pes_id_abertura` | FK → `Pessoa` | não | Quem registrou |
| `pes_id_solicitante` | FK → `Pessoa` | não | Quem pediu. **Distinto do anterior** |
| `ped_id` | FK → `Pedido` | sim | Vínculos opcionais com o domínio que originou |
| `nf_id` | FK → `NotaFiscal` | sim | |
| `oc_id` | FK → `OrdemCompra` | sim | |
| `tra_id` | FK → `Transportadora` | sim | |

**Validações de abertura** (`ProcessoController::insereProcesso()`), todas lançando exceção: fluxo, pessoa de abertura, solicitante, centro de custo e prioridade são obrigatórios.

> **🚫 Não replicar:** se o tipo de trâmite não for informado, o motor adota **o primeiro `FluxoTipoTramite` cadastrado no fluxo**. É uma dependência de ordem de cadastro que não aparece em interface nenhuma. Torne a etapa inicial **explícita** no cadastro do fluxo.

#### `ProcessoTramite`

→ `processo.pro_processo_tramite`

| Coluna | Tipo | Nulo | Significado |
|---|---|---|---|
| `protra_id` | int | não | PK |
| `pro_id` | FK → `Processo` | não | |
| `tiptra_id` | FK → `TipoTramite` | não | A etapa executada |
| `cencus_id` | FK → `CentroCusto` | sim | Para onde a demanda foi |
| `flu_id` | FK → `Fluxo` | não | **Guardado na linha** — o fluxo pode mudar ao longo do processo |
| `protra_data_hora` | datetime tz | não | |
| `protra_mensagem` | text | sim | O texto do trâmite |
| `fluass_id` | FK → `FluxoAssunto` | sim | |
| `pes_id_operacao` | FK → `Pessoa` | **não** | Quem executou |
| `pes_id_destino` | FK → `Pessoa` | sim | **Nulo = endereçado ao setor, não a uma pessoa** |
| `protra_tempo_gasto_minuto` | int | sim | Apontamento de esforço |
| `protra_nova_mensagem` | boolean | sim | Flag de não lido |
| `protra_assunto_notificacao` | text | sim | |
| `mot_id` | FK → `MotivoTramite` | sim | |
| `protra_panobot_execucoes` | smallint | sim | Controle da automação por IA da etapa |
| `protra_panobot_data_hora_envio` | datetime tz | sim | |
| `protra_panobot_situacao` | smallint | sim | Usa as mesmas constantes da fila de bot (seção 10.3) |
| `tralog_id` | FK → `PedidoTransporteLog` | sim | Quando o trâmite nasceu de evento de rastreio |

**`pes_id_destino` nulo é significativo:** "qualquer um do setor pode pegar". Com pessoa preenchida, é dela — e tomar para si algo endereçado a outra pessoa exige ser **responsável** pelo setor (seção 7.4).

#### Satélites do trâmite

| Classe | Tabela | Conteúdo |
|---|---|---|
| `ProcessoTramiteCaracteristica` | `processo.pro_processo_tramite_caracteristica` | **PK composta** (`protra_id`, `car_id`), valor único em `procar_valor` (text). Os campos customizados da etapa |
| `ProcessoTramiteDocumentoEletronico` | `processo.pro_processo_tramite_documento_eletronico` | `doc_id`, `nf_id`, `prodoc_tipo`: `1` anexo comum, `2` DANFE, `3` XML da NF-e, `4` DANFE e XML |
| `ProcessoTramiteNotificacao` | `processo.pro_processo_tramite_notificacao` | `not_id` + `protra_id`. Quais notificações aquele trâmite disparou |

### 6.5 Como o processo anda

| Método de `ProcessoController` | O que faz |
|---|---|
| `insereProcesso()` | Valida, numera, calcula previsão, persiste e executa o trâmite inicial |
| `initTramite()` | O primeiro trâmite, herdando fluxo, tipo e centro de custo do processo |
| `persisteProcessoTramite()` | Grava o trâmite e **reescreve as quatro colunas `_atual`** |
| `receberProcesso()` | Alguém do setor assume. **Valida privilégio antes** |
| `liberar()` | Devolve o trâmite ao setor |
| `doRealizaTramiteTransferencia()` | Move o processo para outro fluxo |
| `notificarEventoTramite()` | Dispara as notificações da etapa |
| `notificarOrigemProcesso()` | Responde no atendimento que originou o processo |
| `newInstanceByEventoProcesso()` | Abre processo a partir de um evento, sem tela |
| `verificaProcessoExistenteAberto()` | Guarda de duplicidade (estática) |

**Flags de comportamento** que o chamador liga antes de abrir, e que mudam bastante o resultado:

- `setRealizaTramiteInicial()` — se falso, o processo nasce **sem nenhum trâmite**
- `setTramiteInicialApenasCentroCusto()` — o trâmite inicial não endereça pessoa, só setor
- `setForcePessoaAbertura()` — a pessoa de operação do trâmite é a de abertura, não o usuário da sessão
- `setNotificaOrigemProcesso()` / `setNotificaEventoTramite()`
- `setNotaFiscalFilterEvento()`, `setOrdemCompraFilterEvento()`, `addProdutoFilterEvento()` — restringem quais eventos casam
- `setRealizaTramiteTransferencia()` — ver abaixo

> **🚫 Não replicar a forma.** Doze interruptores num construtor produzem combinações que ninguém validou. Prefira **casos de uso nomeados**: `abrirProcessoDeAtendimento()`, `abrirProcessoAutomaticoPorEvento()`, `abrirProcessoManual()`.

#### Transferência entre fluxos

Se o `FluxoTipoTramite` da etapa concluída tem `flu_id_transferencia`, `doRealizaTramiteTransferencia()` abre **um novo trâmite no fluxo de destino, dentro do mesmo processo**, herdando mensagem e pessoa de destino, e adotando o primeiro centro de custo e o primeiro assunto do fluxo novo.

É o que permite um processo de "Devolução" virar um processo de "Reembolso Financeiro" sem perder o número nem o histórico. **O processo é um só; o desenho que ele percorre é que muda.**

**A recursão é cortada explicitamente:** o controller filho recebe `setRealizaTramiteTransferencia(false)`. Sem esse corte, dois fluxos que se apontam mutuamente produziriam recursão infinita na primeira conclusão.

### 6.6 Numeração

O número **não** é o `pro_id`. É uma string de 22 caracteres:

```
AAAA + sequencia(6, zero à esquerda) + unique(12, zero à esquerda)
```

- **`AAAA`** — ano corrente
- **`sequencia`** — contador por empresa e por ano, de `processo.pro_sequencia_processo`
- **`unique`** — o identificador da plataforma cliente que abriu o processo. No portal da transportadora não existe plataforma, e o valor vira um hash de CNPJ + timestamp

`SequenciaProcesso` → `processo.pro_sequencia_processo` (`emp_id`, `seqpro_sequencia`, `seqpro_ano`):

```sql
UNIQUE INDEX (emp_id, seqpro_ano)
```

`nextSequenciaProcesso()` abre transação, faz `SELECT ... ORDER BY seqpro_sequencia DESC LIMIT 1 FOR UPDATE`, incrementa ou insere, e **confirma a transação antes de o processo ser gravado**.

Duas consequências que precisam estar documentadas no produto de destino:

- **⚠ Buracos na numeração são esperados.** Se a gravação do processo falhar, o número já foi consumido. Quem não souber disso vai abrir chamado.
- **⚠ Na primeira abertura do ano o `FOR UPDATE` não trava nada** — não existe linha para bloquear. Duas aberturas simultâneas em 1º de janeiro tentam o mesmo `INSERT`; quem perde recebe violação do unique index. O erro é o comportamento correto, mas é um erro, não um retry.

> **Copiar adaptando:** manter ano + sequência por empresa. **🚫 Não replicar** o sufixo de plataforma: ele carrega um dado de origem técnica que ninguém consulta e obriga um caminho alternativo quando a plataforma não existe.

### 6.7 Tipos de trâmite, motivos e notificação

#### `TipoTramite` e as quatro categorias

→ `processo.pro_tipo_tramite` (estende `EmpresaBase`): `tiptra_tipo`, `tiptra_nome`, `tiptra_descricao`, `tiptra_codigo_integracao`

O tipo de trâmite é **cadastrado pelo usuário** — os nomes variam por empresa. Mas cada um precisa se declarar como uma de quatro categorias semânticas:

- `TIPO_ABERTURA = 1`
- `TIPO_PROCESSANDO = 2`
- `TIPO_CONCLUIDO = 3`
- `TIPO_CANCELAMENTO = 4`

**Essa classificação é o único ponto em que o motor entende o significado de uma etapa.** Ele a usa para responder uma pergunta só: "este processo ainda está vivo?". Por exemplo, o disparador de conclusão por nota entregue recusa tramitar quando o tipo atual já é `CONCLUIDO` ou `CANCELAMENTO`.

> **✅ Copiar exatamente.** É o truque que permite workflows arbitrários sem que o motor precise conhecê-los: nomes configuráveis, semântica mínima fixa.

#### `MotivoTramite` e a combinação

- `MotivoTramite` → `processo.pro_motivo_tramite` (`mot_descricao`)
- `TipoTramiteMotivoTramite` → **`processo.pro_tipo_motivo_tramite`** (`tipmot_id`, `mot_id`, `tiptra_id`)

A tabela de ligação restringe **quais motivos são oferecidos em cada etapa**. Uma etapa de cancelamento oferece "cliente desistiu" e "produto sem estoque"; uma de conclusão oferece outros. É validação de domínio por etapa, feita em dado e não em código.

> **⚠ Duas pegadinhas:** a tabela **não** segue o nome da classe (`pro_tipo_motivo_tramite`, não `pro_tipo_tramite_motivo_tramite`), e a entidade **não tem repositório próprio** — reusa o do motivo.

#### Notificação na tramitação

`ProcessoNotificacaoController::notificar()`, disparado automaticamente:

1. acha o `FluxoTipoTramite` do par (fluxo, tipo de trâmite);
2. resolve o evento de notificação — direto (`ev_id`) ou pelo grupo de envio (`gre_id`);
3. **resolve os destinatários pelo destino configurado:**

| Destino | Quem recebe |
|---|---|
| `DESTINO_NOTIFICACAO_SEM_NECESSIDADE = 0` | Ninguém |
| `DESTINO_NOTIFICACAO_CLIENTE = 1` | A pessoa do pedido |
| `DESTINO_NOTIFICACAO_TRANSPORTADORA = 2` | Contatos da transportadora do processo **e suas filiais** |
| `DESTINO_NOTIFICACAO_FORNECEDOR = 3` | A empresa de cada SKU dos itens do pedido |
| `DESTINO_NOTIFICACAO_ORIGEM_PROCESSO = 4` | A pessoa solicitante |

4. cria uma notificação por contato e **registra o vínculo em `ProcessoTramiteNotificacao`**.

Os contatos vêm de `PessoaContato`, filtrados por tipo: e-mail para canais de texto, telefone quando o canal é WhatsApp.

**Fallback por prioridade de canal** quando há grupo de envio e o destino é cliente: se o pedido é de um marketplace que aceita abertura de atendimento, usa o evento de canal marketplace; senão percorre `[WhatsApp, E-mail, SMS]` e pega **o primeiro canal que tenha evento configurado no grupo e contato válido para a pessoa**.

> **✅ Copiar:** destinatário derivado do contexto, nunca de lista estática. Lista estática de pessoas num workflow envelhece em semanas — a pessoa muda de área e ninguém atualiza o cadastro.

Há também um controller de requisições da tela (`ProcessoNotificacaoRequestController`) para o envio **manual** pelo operador: lista os eventos disponíveis para o trâmite, preenche contatos ao trocar o destino, e filtra eventos de canal marketplace quando o destino não é cliente.

### 6.8 Eventos: a ponte com o resto do ERP

Evento é a resposta para "quando o processo chegar nesta etapa, faça alguma coisa lá fora".

#### `EventoProcesso` — a definição

→ `processo.pro_evento_processo`

| Coluna | Tipo | Significado |
|---|---|---|
| `evpro_id` | int | PK |
| `evpro_codigo` | smallint | Qual ação. Ver lista |
| `evpro_nome` | varchar(255) | |
| `evpro_tipo` | smallint | `1 = TIPO_NOVO_PROCESSO`, `2 = TIPO_VALIDACAO_TRAMITE` |
| `flu_id` | FK → `Fluxo` | **O gatilho**: em que ponto do desenho o evento vale |
| `fluass_id` | FK → `FluxoAssunto` | |
| `tiptra_id` | FK → `TipoTramite` | |
| `cencus_id` | FK → `CentroCusto` | |
| `evpro_id_pre_requisito` | FK → `EventoProcesso` | **Autorrelação** — encadeia eventos |
| `evpro_parametro` | json | Lido pelos *value objects* de `Model/Processo/EventoProcesso/` |
| `evpro_ordem` | smallint | |
| `evepro_template` | text | |
| `evepro_ativo` | boolean | |

Os 14 códigos:

- `CODIGO_DIVERGENCIA_ORDEM_COMPRA = 1`
- `CODIGO_DIVERGENCIA_CTE = 2`
- `CODIGO_BOLETO_DIVERGENTE = 3`
- `CODIGO_PEDIDOS_EM_ANALISE = 4`
- `CODIGO_RENOVACAO_CERTIFICADO = 5`
- `CODIGO_EMISSAO_NFE = 6`
- `CODIGO_ORDEM_COMPRA_ATRASADA = 7`
- `CODIGO_ALTERACAO_PRAZO_ENTREGA = 8`
- `CODIGO_ALTERAR_SITUACAO_PEDIDO = 9`
- `CODIGO_MOVIMENTACAO_FINANCEIRA = 10`
- `CODIGO_PEDIDO_MARKETPLACE = 11`
- `CODIGO_GESTAO_PAGAMENTO = 12`
- `CODIGO_PEDIDO_ERRO_INTEGRACAO = 13`
- `CODIGO_CANCELAMENTO_OC = 14`

Os parâmetros de cada código são tipados por um *value object*: `EventoProcessoAlteracaoPrazo`, `EventoProcessoAlterarSituacaoPedido`, `EventoProcessoCancelamentoOc`, `EventoProcessoEmissaoNFe`, `EventoProcessoMovimentacaoFinanceira`, `EventoProcessoPedidoMarketplace`.

#### `ProcessoTramiteEventoProcesso` — a execução

→ `processo.pro_processo_tramite_evento_processo`

| Coluna | Tipo | Significado |
|---|---|---|
| `event_id` | int | PK |
| `protra_id` | FK → `ProcessoTramite` | O trâmite que disparou |
| `evpro_id` | FK → `EventoProcesso` | A definição |
| `event_situacao` | smallint | Ver abaixo |
| `event_data_hora_situacao` | datetime tz | |
| `event_log` | text | Mensagem de erro quando falha |
| `event_params` | json | Parâmetros **e o contador de tentativas** |
| `nf_id` | FK → `NotaFiscal` | Resultado produzido |
| `mov_id` | FK → `Movimentacao` | Resultado produzido |

Situações: `1 = FILA`, `2 = AGUARDANDO_TERCEIRO`, `3 = FINALIZADO`, `4 = ERRO`, `5 = IGNORADO`.

> **✅ `IGNORADO` separado de `ERRO` é decisão importante.** "Ignorado" é o executor dizendo "as condições não se aplicam a este caso", e é o desfecho **mais comum** nos eventos condicionais. Sem esse estado, todo evento que não se aplica pareceria falha e a fila pareceria quebrada o tempo todo.

#### O consumidor

```sql
SELECT event_id FROM processo.pro_processo_tramite_evento_processo
 WHERE event_situacao = 1                                  -- FILA
    OR (event_situacao = 4                                  -- ERRO
        AND coalesce((event_params->>'errors')::smallint, 2) < 2)
 LIMIT 250
```

**Duas tentativas por evento**, com o contador dentro do próprio JSON de parâmetros.

Executores registrados — **apenas 5 dos 14 códigos têm ação automatizada**: emissão de nota fiscal, alteração de prazo de entrega, alteração de situação do pedido, movimentação financeira e cancelamento de ordem de compra. Os demais são de tipo "novo processo": o efeito deles é abrir um processo, não executar uma ação.

> **⚠ Armadilha séria:** o job roda cada evento em transação própria, mas no `catch` marca a situação como `ERRO` e executa **`die`**. O primeiro evento que falha **para a fila inteira** daquela execução. Um evento problemático segura todos os que estão atrás dele até esgotar as duas tentativas. Falha em um item de fila deve isolá-lo, nunca parar o consumo.

#### Disparadores existentes

Servem de exemplo do padrão, cada um a partir de um domínio diferente:

- `TramitaProcessoNotaEntregueController` — nota fiscal entregue conclui ou abre processo
- `ProcessoTramiteBotFilaController` — fila de automação por IA do trâmite
- `ProcessoTramiteDocumentoEletronicoController` — anexos, com upload e download
- `ProcessoTramiteEventoProcessoController` — criação da linha de execução

> **✅ Copiar o princípio:** o motor de workflow **não chama o ERP diretamente**. Ele enfileira uma intenção, e um consumidor separado executa. Isso torna o efeito colateral observável, reexecutável e isolado da transação do trâmite. Ação externa na mesma transação significa que uma emissão de nota que falha derruba o trâmite que o usuário acabou de fazer.

### 6.9 Quem usa o motor

| Módulo | Arquivos | Pontos de entrada principais |
|---|---|---|
| CRM | 18 | `AtendimentoFormController`, `SacApiController`, `PanoBotIaProcessoController` |
| Faturamento | 15 | `ProcessoNotaFiscalController`, `ConcluirProcessoNotaFiscalController`, `RecebimentoDevolucaoFormController` |
| Vendas | 14 | `AnaliseRiscoBaseController`, `ChecarPedidosEmAnaliseJob`, `CobrancaFornecedorFormController` |
| Transportadora | 7 | `TramiteProcessoTransporteBaseFormController`, `NotasFiscaisEnviarComprovanteFormController` |
| Hub | 4 | `PedidoProcessoAutomaticoController` |
| Admin | 3 | `EmailProcessing\SacStrategy`, `RenovarCertificadoJobController` |
| Financeiro | 2 | `AjustePendenciasAuditoriaGatewayPagamentoController` |
| Marketplaces | 1 | `PedidoMarketplaceValidador` |

Todos entram pelo mesmo lugar: instanciam o controller, preenchem o processo, ligam as flags e mandam abrir. **Nenhum manipula trâmites diretamente.**

---

## 7. Domínio: Centro de custo

### 7.1 Problema de negócio

Trabalho precisa ir para algum lugar, e nem todo mundo pode ver tudo. A alternativa ingênua — endereçar sempre a uma pessoa — quebra assim que alguém entra de férias, muda de área ou sai da empresa.

### 7.2 Modelo

`Unico\Model\CentroCusto` → **`un_centro_custo`** (schema padrão, **não** `processo.*`)

| Coluna | Tipo | Significado |
|---|---|---|
| `cencus_id` | int | PK |
| `cencus_codigo` | varchar(50) | Código hierárquico `001.002.003` |
| `cencus_nome` | varchar(250) | |
| `cencus_ativo` | boolean | Default true |
| `cencus_id_pai` | FK → `CentroCusto` | **Autorrelação** — a árvore |
| `emp_id` | FK → `Empresa` | |

```sql
uq_centro_custo_codigo_empresa UNIQUE (emp_id, cencus_codigo)
```

`Unico\Model\CentroCustoUsuario` → `un_centro_custo_usuario` (`cenusu_id`, `cencus_id`, `usu_id`, `cenusu_responsavel`)

A flag `cenusu_responsavel` distingue quem **participa** do setor de quem **responde** por ele.

> **Nota de organização:** apesar de ser usado quase exclusivamente pelo motor de processos, o centro de custo fica fora do schema `processo.*`. Faz sentido — a dimensão organizacional é maior que o workflow — mas confunde quem procura.

### 7.3 O código hierárquico

`CentroCustoRepository::getNextCodigo($pai)`:

- **sem pai** — pega o maior código entre as raízes, incrementa, preenche com zeros até 3 dígitos (`001`, `002`, …);
- **com pai, primeiro filho** — concatena o código do pai com `001` (`002` → `002.001`);
- **com pai, demais filhos** — pega o maior código irmão, incrementa **o último nível** e recompõe (`002.003` → `002.004`).

O código é composicional e legível: `002.004.001` se lê imediatamente como "primeiro filho do quarto filho do segundo setor raiz".

`ProcessaCodigoCentroCusto` é apenas o endpoint de 23 linhas que o formulário chama para preencher o campo — a lógica inteira está no repositório.

> **⚠ Ponto de atenção:** não há transação nem trava. Duas criações simultâneas sob o mesmo pai calculam o mesmo código, e a colisão é barrada pelo unique constraint — uma das duas **falha com erro** em vez de receber o próximo código. É aceitável na prática (criar setor é raro), mas se replicar, resolva no cálculo e não na restrição.

### 7.4 Visibilidade herdada

`CentroCustoUsuarioRepository::validaPrivilegioUsuarioCentroCusto()` é a regra de autorização do módulo:

```sql
SELECT count(1) FROM un_centro_custo_usuario
 WHERE usu_id = :usuario
   [ AND cenusu_responsavel = true ]          -- quando exigido
   AND cencus_id IN (
        WITH RECURSIVE centro_custo (cencus_id, cencus_id_pai) AS (
             SELECT cencus_id, cencus_id_pai FROM un_centro_custo
              WHERE cencus_id = :centro_custo
             UNION ALL
             SELECT f.cencus_id, f.cencus_id_pai
               FROM un_centro_custo f
               JOIN centro_custo ON f.cencus_id = centro_custo.cencus_id_pai)
        SELECT cencus_id FROM centro_custo)
```

A consulta **sobe** do setor da tarefa até a raiz e verifica se o usuário está vinculado a **qualquer ancestral**.

**Ou seja: o privilégio é herdado para baixo.** Quem está vinculado ao setor pai enxerga e recebe os processos de todos os setores filhos. É isso que permite montar a hierarquia real da empresa e conceder acesso num nível só — o gerente industrial é vinculado ao nó "Indústria" e passa a ver produção, manutenção e qualidade sem cadastro adicional. Um setor novo criado abaixo **já nasce visível** para quem precisa.

`ProcessoController::receberProcesso()` aplica a regra com uma nuance:

- trâmite **sem pessoa de destino** → basta estar no setor (ou em um ancestral);
- trâmite **com pessoa de destino** → exige `cenusu_responsavel = true`.

> **✅ Copiar.** É a peça que faz o workflow funcionar numa empresa real, onde as pessoas mudam de lugar e os setores não.

### 7.5 Variante do portal externo

`CentroCustoUsuarioPortalFormController` e `CentroCustoUsuarioPortalGridController` estendem os controllers normais trocando apenas a view e injetando a empresa do login. Mesma entidade, exposta no portal externo com escopo restrito.

---

## 8. Motor de fluxo visual

### 8.1 Problema de negócio

Boa parte das perguntas que chegam tem resposta mecânica: onde está meu pedido, qual o prazo, como faço a troca. Responder isso à mão é caro e lento. Mas automatizar via código significa uma release a cada mudança de regra, e quem conhece a regra não é quem programa.

### 8.2 Conceito e os quatro escopos

A pessoa de negócio desenha um grafo de blocos numa tela e o backend executa esse grafo. O mesmo motor tem duas faces:

- **automação determinística** — condição, desvio, junção, espera, chamada HTTP, ação no ERP. O desenhista decide cada passo;
- **agente de IA** — um bloco que conversa com um modelo de linguagem, e outros blocos pendurados nele como **ferramentas**. O modelo decide o que chamar.

Todo fluxo tem um tipo e todo bloco declara seu escopo:

| Escopo (constante) | Valor | Tipo de fluxo correspondente |
|---|---|---|
| `NodeInterface::USE_GLOBAL` | `'GLOBAL'` | Disponível em todos |
| `NodeInterface::USE_CRM_SAC` | `'CRM_SAC'` | `FluxoConstrutor::USE_CRM_SAC = 1` |
| `NodeInterface::USE_UNICO_PROCESSO` | `'UNICO_PROCESSO'` | `FluxoConstrutor::USE_UNICO_PROCESSO = 2` |
| `NodeInterface::USE_API` | `'USE_API'` | `FluxoConstrutor::USE_API = 3` |

Os escopos são string; os tipos de fluxo são inteiro. São listas paralelas — e `USE_API` é o único cujo nome de constante não segue o padrão dos outros três.

**Por que importa:** um bloco que responde um atendimento não faz sentido num fluxo de processo, e oferecê-lo ali só produz fluxos que falham em runtime. Filtrar por escopo no catálogo é mais barato que validar depois.

### 8.3 Modelo de dados

#### `FluxoConstrutor` — o fluxo

→ `un_fluxo_construtor`

| Coluna | Tipo | Significado |
|---|---|---|
| `fc_id` | int | PK |
| `fc_tipo` | smallint | `1` CRM/SAC, `2` processo, `3` API |
| `fc_codigo` | varchar(255) | **Único por tipo** (`uq_un_fluxo_construtor_tipo_codigo`). É o que a URL da API usa |
| `fc_nome` | varchar(255) | |
| `fc_descricao` | text | |
| `fc_debug` | boolean | Liga a gravação dos passos de execução |
| `fc_ttl_sesao` | int | Minutos de vida da sessão. Default `DEFAULT_TTL_SESSAO = 30` *(o typo `sesao` está na coluna)* |
| `fc_configuracao` | json | Ex.: `CONFIG_KEY_API_METODOS = 'api_metodos_permitidos'` |

Constantes de tempo: `MIN_EXECUTION_TIMEOUT_SECONDS = 600`, `MAX_EXECUTION_TIMEOUT_SECONDS = 900`, `EXECUTION_TIMEOUT_POR_NODE_SECONDS = 10`.

#### `FluxoConstrutorVersao` — o desenho versionado

→ `un_fluxo_construtor_versao` (único por `fc_id` + `fcv_codigo_versao`)

| Coluna | Tipo | Significado |
|---|---|---|
| `fcv_id` | int | PK |
| `fc_id` | FK → `FluxoConstrutor` | |
| `fcv_codigo_versao` | varchar(255) | `CODIGO_DRAFT = 'draft'` é **reservado** |
| `fcv_configuracao` | json | **O grafo**: `{nodes: [...], connections: [...]}` |
| `fcv_hash` | varchar(255) | sha256 do JSON. **Gravado apenas no rascunho** |
| `fcv_descricao_versao` | text | |
| `fcv_data_criacao` | datetime tz | |
| `usu_id` | FK → `Usuario` | Quem publicou |

O timeout de execução fica **dentro do JSON de configuração**, sob `CONFIG_KEY_EXECUTION_TIMEOUT = 'executionTimeoutInSeconds'`.

#### `FluxoConstrutorParametro` — o que não cabe no desenho

→ `un_fluxo_construtor_parametro` (`fcp_id`, `fcp_codigo`, `fc_id` nullable, `fcp_tipo`, `fcp_configuracao` json)

Credenciais, provedores e variáveis globais ficam **fora do grafo** para não vazarem no JSON da versão.

Tipos:

- `TIPO_CREDENCIAIS_HTTP_REQUEST = 1`
- `TIPO_PROVIDER_IA = 2`
- `TIPO_CREDENCIAIS_PROVIDER_IA = 3`
- `TIPO_GLOBAL_VARIABLE = 4`
- `TIPO_PROVIDER_IA_MODEL = 5` — **não persiste**; existe só para listar modelos dinamicamente
- `TIPO_GLOBAL_FUNCTION = 6`
- `TIPO_PROVIDER_IA_MODEL_EMBEDDING = 7` — **não persiste**; idem, para modelos de embedding

Escopos: `ESCOPO_POR_FLUXO = 'por_fluxo'` (amarrado a um `fc_id`) e `ESCOPO_COMPARTILHADA = 'compartilhada'`.

Cada tipo tem um *value object* que lê `fcp_configuracao`: `...CredenciaisHttpRequest`, `...CredenciaisProviderIA`, `...ProviderIA`, `...GlobalVariable`, `...FuncaoGlobal`.

> **✅ Copiar:** credenciais fora do documento versionado. O JSON do fluxo é copiado a cada publicação, exportado, logado em dumps de execução e lido por quem edita. Credencial ali dentro vaza por seis caminhos diferentes.

#### `FluxoConstrutorSessao` — a memória da conversa

→ `un_fluxo_construtor_sessao`

| Coluna | Tipo | Significado |
|---|---|---|
| `fcs_id` | int | PK |
| `fcs_session_id` | varchar(255) **unique** | Identificador externo da sessão |
| `fcv_id` | FK → `FluxoConstrutorVersao` | |
| `fcs_variables` | json | As variáveis do fluxo, **sobrevivem entre mensagens** |
| `fcs_metadata` | json | Estado de execução (await, merge) |
| `fcs_pending_nodes` | json | **Onde a execução parou.** É o que permite retomar |
| `fcs_updated_at` | datetime imutável | Base do TTL |
| `fcs_tipo` | smallint | `1 = PRODUCAO`, `2 = TESTE`, `3 = ERRO` |
| `fcs_fcv_hash` | varchar(255) | O hash da versão quando a sessão nasceu |

`fcs_fcv_hash` é o **detector de sessão obsoleta**: se o desenho mudou desde que a conversa começou, o hash não bate e a sessão não deve ser retomada sobre um grafo diferente.

Duas tabelas especializam a sessão por domínio, cada uma no schema do seu módulo:

- `CRM\Model\FluxoConstrutor\AtendimentoFluxoConstrutorSessao` → `crm.crm_atendimento_fluxo_construtor_sessao`
- `Unico\Model\Processo\FluxoConstrutor\ProcessoFluxoConstrutorSessao` → `processo.pro_processo_fluxo_construtor_sessao`

O trait `CRM\Model\FluxoConstrutor\TraitConfiguracaoFluxoConstrutorModel` (**não é entidade**) é a ponte config → fluxo: dá a qualquer *value object* de configuração os getters `fluxoConstrutor` / `fluxoConstrutorVersao`.

#### `FluxoConstrutorExecucao` — o registro de uma corrida

→ `un_fluxo_construtor_execucao` (`fce_id`, `fcs_id`, `doc_id`, `fcs_updated_at`)

Só quatro colunas: o dump de debug **não fica em coluna**, vai para um `DocumentoEletronico`. É o que permite guardar passos de execução grandes sem inchar a tabela — e eles ficam grandes: dumps reais chegaram a dezenas de megabytes.

### 8.4 Publicação e compilação

Este é o ponto mais mal compreendido do motor.

#### O que acontece ao salvar

Em transação:

1. persiste o `FluxoConstrutor`;
2. joga a configuração no **rascunho** (um por fluxo, obtido ou criado);
3. **compila o rascunho** e grava na versão o timeout calculado;
4. o callback de gravação da versão recalcula `fcv_hash` — **só se for rascunho**;
5. se o usuário pediu "salvar versão utilizável", **copia o JSON do rascunho** para uma linha nova com o código informado.

**Publicar é copiar.** Não existe transformação entre rascunho e versão publicada: são o mesmo JSON em duas linhas.

#### `FluxoConstrutorVersaoCompiled` não é uma versão compilada armazenada

**A classe não tem tabela e não é entidade.** É um objeto em memória — id, nome, array de blocos, array de conexões — produzido por `FluxoConstrutorCompilerBO::compile()` e guardado num **cache estático por requisição**.

`Executor::getCompiled()` chama exatamente o mesmo compilador. Ou seja: **a execução lê o JSON cru e recompila a cada corrida.** Nada compilado é persistido.

O que "compilar" significa, em ordem:

1. `validateConfiguracao()` — exige as chaves `nodes` e `connections`
2. `compileNodes()` — injeta `tipoFluxoConstrutor` em cada bloco e instancia via registro (`$class::beanModel($definicao)`)
3. `compileConnections()` — instancia as conexões via registro
4. `validateFluxo()` — as cinco asserções abaixo
5. `orderConnections()` — ordena as conexões por bloco de origem

**As cinco asserções:**

| Asserção | O que garante |
|---|---|
| `assertStartNodeExists()` | Existe um bloco inicial |
| `assertMoreThanStartNode()` | Existe algo além dele |
| `assertSingleConnectionRules()` | Bloco marcado `isSingleConnection()` não aceita mais de uma conexão de entrada |
| `assertNodeLimits()` | Respeita o `limit` da definição do bloco |
| `assertToolConnectionCompatibility()` | **Conexão de ferramenta só pode sair de bloco que declara essa capacidade** |

A última é a mais importante: uma conexão de ferramenta incompatível **ficaria inerte em runtime**, sem erro. Barrar no save transforma um erro silencioso num erro visível.

> **✅ Copiar:** validar no save. Um erro de desenho descoberto em produção, numa conversa com cliente, custa muito mais caro que um erro descoberto na tela do desenhista.

#### O timeout é derivado do desenho

`calcularExecutionTimeout()` soma:

- `EXECUTION_TIMEOUT_POR_NODE_SECONDS = 10` por bloco;
- mais o timeout configurado de cada bloco de chamada HTTP;
- mais os segundos de cada bloco de espera;
- mais `HttpRequestNode::MAX_TIMEOUT = 240` para cada bloco de IA.

Resultado limitado entre 600 e 900 segundos. Fluxo grande não estoura o teto; fluxo pequeno ganha o piso.

> **✅ Copiar:** remove do usuário uma decisão que ele não tem como tomar bem, e o número acompanha o desenho automaticamente quando ele cresce.

### 8.5 Execução

#### O loop

`Executor::execute()` mantém uma fila de blocos pendentes e consome do início:

1. seta contexto e último resultado no bloco;
2. se o bloco aceita conexões extras, monta o conjunto de ferramentas ligadas a ele;
3. executa e guarda o resultado no contexto;
4. registra o passo de debug;
5. empilha os próximos a partir das conexões, casando o **nome da porta de saída** com `fromOutput` da conexão;
6. se o metadata marcou **await**, guarda os pendentes na sessão e **sai do loop**.

Estado de runtime: `ExecutionContext` (variáveis, último bloco, último resultado, última exceção), `ExecutionMetadata` (await, pendentes), `RunFlowState`, `MergeState` (quem já chegou na junção), `PendingNode`.

#### Tratamento de erro

Três modos por bloco (`NodeInterface`):

- `TYPE_EXCEPTION_PATH = 'PATH'` — segue pelas conexões de exceção do bloco
- `TYPE_EXCEPTION_IGNORE = 'IGNORE'` — registra e continua pelo caminho normal, com resultado nulo
- `TYPE_EXCEPTION_STOP = 'STOP'` — interrompe (**default**)

`ExceptionStartNode` é a entrada do tratamento global: quando nenhuma conexão de exceção local atende, a execução salta para ele, com os dados da exceção disponíveis.

Exceções que estendem `AbstractNodeFatalException` — hoje só `ExecutaFluxoNodeRecursaoInfinitaFatalException` — passam por `checkFatalExceptions()` e **não respeitam o modo configurado**.

No caminho normal (`run()`), falha fatal marca a sessão como `TIPO_ERRO` e grava a execução antes de relançar, **mas só se não for subsessão**.

#### Modo teste

`Executor::test()`:

1. abre transação, executa, **sempre reverte**;
2. abre uma segunda transação e grava **apenas** sessão e execução;
3. se houve exceção, marca a sessão como erro e relança **depois** de persistir.

Falha e sucesso seguem o mesmo caminho de persistência, para que a tela mostre os passos nos dois casos.

> **⚠ Armadilha:** o rollback protege o banco, **não o mundo externo**. Chamada HTTP, resposta enviada ao cliente e requisição ao modelo de IA já aconteceram quando o rollback roda. Alguns blocos verificam `isTestMode()` e não executam — mas não todos. Um modo de teste honesto precisa de **interruptor explícito de efeitos externos por bloco**, não de rollback de transação.

#### Dados de entrada

`Executor/InputData/`: `ExecutionInputData` (base) e três variantes que dão ao fluxo o contexto do domínio — `ExecutionInputDataAtendimento`, `ExecutionInputDataProcesso`, `ExecutionInputDataApi`. A resolução é por registro.

`Executor/Service/` tem dois serviços de apoio consultados pelos blocos: informações de NF-e e informações de rastreio.

O que `ExecutionInputDataAtendimento` expõe está detalhado em 10.7.

### 8.6 Fluxo exposto como endpoint HTTP

O terceiro modo do motor, e o que menos depende dos outros domínios.

`FluxoConstrutorApiController` recebe `GET`, `POST`, `PUT` e `DELETE` e executa:

| Passo | Falha → resposta |
|---|---|
| Extrai o código do fluxo da URL | `400` "Código do fluxo não informado" |
| Busca o fluxo por (`fc_codigo`, tipo = API) | `404` "Fluxo não encontrado" |
| Confere o método contra `api_metodos_permitidos` | `405` "Método não permitido" |
| Resolve a versão — `?versao=` na query, senão a última publicada | `503` "Nenhuma versão publicada disponível" |
| Recusa versão rascunho explicitamente pedida | `400` "Versão draft não pode ser usada" |
| Executa o fluxo com o corpo e os cabeçalhos | — |
| Se um bloco de resposta produziu saída, devolve-a | senão, `200 {"success": true}` |

> **✅ Copiar:** permitir fixar a versão por query string é excelente para teste e para migração gradual de consumidores, **desde que** a versão rascunho seja recusada — o que aqui é feito.

---

## 9. Catálogo de blocos

28 blocos: 27 no diretório principal, 1 em `Node/Tool/`. Todos registrados em `NodeRegistry::registerAll()`, que é a **fonte única** — o registro define o escopo, e o mesmo registro alimenta o compilador e a paleta do canvas.

### 9.1 Convenções

**Nomes de porta de saída canônicos:** `next`, `true`, `false`, `default`, `output`. São apenas nomes; `next` e `output` são semanticamente idênticos (fluxo linear) e a diferença é histórica. Vale unificar na cópia.

**Pausa (`await`)** — um único mecanismo, acionado por três blocos: resposta ao atendimento (quando configurado), aguardar evento (sempre), e executar subfluxo (propagando o await do filho).

**Renderização** — quase todo parâmetro do tipo expressão é renderizado como template no momento da execução, contra o contexto (variáveis, atendimento, saídas de blocos anteriores). Formatos: booleano `"1"`, data `d/m/Y`, data-hora `d/m/Y H:i:s`.

**Flags declaradas mas não usadas:** nenhum bloco atual usa `supportsExceptionOutput` nem `singleConnection`. `dynamicOutputs` só no bloco de desvio múltiplo; conexão extra só no bloco de IA. Tratar como reservado na cópia.

### 9.2 Controle de fluxo — escopo GLOBAL

| Bloco / tipo | O que faz | Parâmetros | Saídas | Efeito externo | Pausa | Limite |
|---|---|---|---|---|---|---|
| **StartNode** `start` | Ponto de entrada. Só sinaliza início | — | `next` | não | não | **1 por fluxo** |
| **ExceptionStartNode** `exception_start` | Entrada do tratamento de erro global. Expõe `nodeException`, `exception`, `message`, `code`, `file`, `line` | — | `next` | não | não | **1 por fluxo** |
| **IfNode** `if` | Avalia uma expressão booleana e bifurca. Expressão que falha ou não resolve booleano cai em `false` | `expr` (expressão, obrigatório) | **`true`**, **`false`** | não | não | — |
| **SwitchNode** `switch` | Lista ordenada de casos; a **primeira** expressão verdadeira define a saída | `cases` (tabela: `output` = nome da porta, `expr` = expressão) | `default` + **uma porta por caso** | não | não | — |
| **MergeNode** `merge` | Junção: acumula as saídas dos ramos e só libera após receber `expected` entradas. Ao receber nova passagem depois de resolvido, **reinicia a contagem** | `expected` (2..10, default 2) | `output` | não | **não** — apenas não propaga enquanto faltam entradas | — |
| **SleepNode** `sleep` | Espera N segundos **de forma bloqueante** | `seconds` (0..**60**) | `output` | não (mas segura o processo) | não | **10 por fluxo** |
| **DefineVariavelNode** `set_variable` | Cria ou atualiza variável do contexto | `key`, `value` (expressão), `overwrite`, `persistInAtendimento`, `restringirAoFluxo` | `output` | **sim, condicional** — com `persistInAtendimento` grava no banco | não | — |
| **ExecutaFluxoNode** `executa_fluxo` | Chama outro fluxo como sub-rotina | `executaUltimaVersaoPublicada`, `fluxoConstrutor`, `fluxoConstrutorVersao`, `visualizarExecucao` | `output` | **sim** — cria/atualiza sessão do subfluxo | **sim, propaga** o await do filho | — |

**Sobre o `SwitchNode`:** valida nomes de caso na compilação — nome vazio ou duplicado lança exceção. As portas são recalculadas no editor por callback ao salvar.

**Sobre o `DefineVariavelNode`:** três semânticas distintas de escopo, e vale entendê-las antes de copiar.

- `persistInAtendimento` — a variável vai para os parâmetros do **atendimento**, sobrevive a qualquer sessão e a qualquer execução;
- sem essa flag — vive na **sessão**, sobrevive entre mensagens da mesma conversa até o TTL;
- `restringirAoFluxo` — variável **local**: não sobe ao fluxo pai nem desce para subfluxos.

**Sobre o `ExecutaFluxoNode`:** tem uma **pilha estática anti-recursão**. Repetir uma versão já na pilha lança exceção fatal, que **não** é capturável pelo tratamento de erro configurado. É a proteção contra dois fluxos que se chamam mutuamente.

> **⚠ Armadilha:** o resultado do subfluxo embute os passos de debug do filho **recursivamente**. Dumps de execução reais chegaram a dezenas de megabytes com dados pessoais replicados em três estruturas. Custo de armazenamento e de privacidade. Se copiar, guarde referência à execução do filho, não o conteúdo inteiro.

### 9.3 Integração externa — escopo GLOBAL

| Bloco / tipo | O que faz | Parâmetros | Saídas | Efeito externo | Pausa |
|---|---|---|---|---|---|
| **HttpRequestNode** `http_request` | Chamada HTTP a API externa | `method` (GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS), `url`, `headers` (tabela), `query` (tabela), `typeBody` (`json`/`formUrlencoded`), `body`, `credenciais` (referência a parâmetro), `timeout` (1..**240**, default **30**) | `output` → `status`, `headers`, `body`, `error`, `message` | **sim** | não |
| **ProviderAIRequestNode** `provider_ai_request` | Chat completion com suporte a chamada de ferramenta | `provider`, `credenciais`, `model`, `messages` (tabela: `role` + `content` + `image_urls`), `temperature`, `top_p`, `max_tokens`, `presence_penalty`, `frequency_penalty`, `response_format` | `output` → texto final + resposta original | **sim** | não |
| **MovimentacaoFinanceiraNode** `movimentacao_financeira` | Gera lançamento financeiro, opcionalmente recorrente e opcionalmente já quitado | `tipo_conta`, `empresa_destino`, `descricao`, `plano_contas`, `valor_original`, `data_vencimento`, `valor_desconto`, `valor_juros`, `conta_bancaria`, `repetir`, `repetir_tipo`, `ocorrencia_vezes`, `pago`, `pagamentos` (tabela com 8 colunas), `centro_custo`, `pessoa_origem`, `pedido_pagamento`, `pedido_venda`, `chave_nf`, `observacao` | `next` → `movimentacao_id` | **sim — grava no banco**, situação inicial pendente | não |

**Sobre o bloco de IA:** aceita **conexão extra do tipo ferramenta, com limite 2**. Tem `MAX_TOOL_ITERATIONS = 10` — ultrapassar lança "Agente IA excedeu o limite de iterações de tool calling"; ferramenta desconhecida lança "Tool não encontrada".

> **⚠ Armadilha:** o bloco de chamada HTTP marca sucesso para **qualquer** status HTTP — 4xx e 5xx não acionam o tratamento de erro. Pode ser intencional (o fluxo inspeciona o status na saída), mas precisa estar documentado e, de preferência, ser opção configurável ("falhar em status ≥ 400").

### 9.4 Atendimento — escopo CRM_SAC

| Bloco / tipo | O que faz | Parâmetros | Saídas | Efeito externo | Pausa |
|---|---|---|---|---|---|
| **RespondeAtendimentoNode** `response` | Envia mensagem ao cliente; opcionalmente pausa esperando a resposta | `message`, `await`, `params` (JSON de metadados) | `next` → `message`, `awaiting` | **sim — mensagem real ao cliente**. Com `await=false` pode **finalizar o atendimento**. Não envia em modo teste | **sim quando `await=true`** |
| **AguardaEventoNode** `aguarda_evento` | Pausa até que **outro** atendimento dispare evento para este, sem mandar mensagem | — | `next` → `awaiting` | **sim** — altera a situação do atendimento para tirá-lo da fila do bot | **sim, sempre** |
| **DisparaEventoNode** `dispara_evento` | Notifica outro atendimento do mesmo pedido que a negociação terminou, liberando-o | `atendimentoAlvo`, `mensagem`, `params` | `next` → ids do atendimento alvo, da mensagem e da fila | **sim** — cria mensagem e enfileira o bot do alvo | não |
| **AbrirAtendimentoNode** `abrir_atendimento` | Cria atendimento **filho** do atual | `lojaId`, `origem`, `mensagemAbertura`, `pedidoId`, `nomeSolicitante`, `numeroProtocolo`, `produtoDerivacaoId` | `next` → `atendimento_id` | **sim** | não |
| **BloqueiaAtendimentoNode** `bloqueia_atendimento` | Impede o bot de responder (trava para humano) | — | `output` → `success` | **sim** | não |
| **DesbloqueiaAtendimentoNode** `desbloqueia_atendimento` | Inverso do anterior | — | `output` → `success` | **sim** | não |
| **DefinePedidoAtendimentoNode** `define_pedido_atendimento` | Vincula pedido ao atendimento | `pedidoId` (obrigatório) | `next` → `pedido_id` | **sim**. Pedido não encontrado → resultado nulo, **não lança** | não |
| **DefineProdutoAtendimentoNode** `define_produto_atendimento` | Vincula SKU ao atendimento | `pedidoId` (rótulo "SKU Produto" — **o nome da constante é enganoso**) | `next` → `produto_derivacao_id` | **sim** | não |
| **OcorrenciaRastreioAtendimentoNode** `ocorrencia_rastreio` | Registra ocorrência de rastreio para a NF-e informada | `transportadora`, `situacao`, `chaveNotaFiscal`, `observacao` | `next` → `situacao`, `observacao` | **sim** — chama a API de transporte | não |
| **CotacaoFreteNode** `cotacao_frete` | Cota frete para um ou mais SKUs | `cepDestino`, `sku`, `quantidade`, `valorUnitario` | `next` → `cotacao` (array) | **sim** — integração de transportadoras | não |
| **AcaoAtendimentoMarketplaceNode** `acao_atendimento_marketplace` | Executa ação de atendimento no marketplace (reembolso, disputa, revisão de devolução, envio de rastreio) | `action` (**carrega o sub-formulário dinamicamente** conforme a ação) | `next` → `success`, `response` | **sim** — API do marketplace. Resposta falsa em modo teste | não |
| **AcaoPosVendaMarketplaceNode** `acao_pos_venda_marketplace` | Mesma mecânica, para o pós-venda do pedido (confirmação de entrega, contestação, aprovar/rejeitar reembolso) | `action` (idem) | `next` → `success`, `response` | **sim** | não |
| **FluxoProcessoAtendimentoNode** `fluxo_processo_atendimento` | Abre processo novo **ou** tramita existente, a partir do atendimento | Ver 9.6 | `next` → `id`, `new`, `processoTramites` | **sim** | não |

**Sobre `CotacaoFreteNode`:** as listas são **strings separadas por `;`** e precisam ter o mesmo número de itens que a lista de SKUs, senão lança. Valor unitário vazio faz buscar o preço pelo canal.

> **⚠ Armadilha documentada:** este bloco **trunca valores com vírgula decimal** — justamente o formato ensinado no próprio texto de ajuda do campo. Conferir na cópia.

> **⚠ Inconsistência real no código:** `DefineProdutoAtendimentoNode` **declara** o esquema de saída do bloco de pedido, mas **retorna** o de produto. Funciona, mas o autocomplete do editor mostra os campos errados.

### 9.5 API — escopo USE_API

| Bloco / tipo | O que faz | Parâmetros | Saídas | Efeito externo | Pausa |
|---|---|---|---|---|---|
| **ValidateApiDataNode** `validate_api_data` | Valida campos do payload e bifurca | `origem` (`body` / `queryParams` / chave do JSON de entrada), `regras` (tabela: `campo` com caminho aninhado `endereco.cidade`, `obrigatorio`, `tipo` ∈ string/number/boolean/array/any) | **`true`**, **`false`** → `passed`, `failedFields` (com motivo) | não | não |
| **ApiResponseNode** `api_response` | Define a resposta HTTP. **Bloco terminal** | `statusCode` (default 200), `contentType` (`application/json` / `text/plain` / `text/html`), `body`, `headers` (tabela) | **nenhuma** — encerra o fluxo | escreve a resposta HTTP | não |

### 9.6 Processo — escopo UNICO_PROCESSO

**FluxoProcessoProcessoTramiteNode** `fluxo_processo_processo_tramite`

Junto com `FluxoProcessoAtendimentoNode` (escopo CRM_SAC), estende uma base comum que concentra toda a lógica: **abre um processo novo ou tramita um existente**. As duas subclasses só diferem em como resolvem o processo e o pedido de origem.

O bloco do atendimento usa a validação de abertura do repositório para decidir: se já existe processo compatível → tramita; senão → abre.

Parâmetros compartilhados: `prioridade` (só na abertura), `fluxo`, `tipoTramite`, `motivoTramite`, `descricao`, `chaveNfeProcesso`, `chaveNfeFilterEventoProcesso`, `skusFilterProdutoEventoProcesso`, `ordemCompraFilterEventoProcesso`, `forcarAberturaNovoProcesso`, `forcaPedidoAbertura`, `forcaTransportadoraAbertura`, `forcaSolicitanteAbertura`.

Saída: `next` → `id` do processo, `new` (abriu ou tramitou), `processoTramites`. **Efeito externo: sim** — cria ou tramita processo.

### 9.7 Ferramenta de IA

**RAGSearchToolNode** `rag_search_tool` — escopo GLOBAL, registrado com tipo de conexão `tool`

**Não é um bloco de fluxo.** É uma **ferramenta** consumida pelo bloco de IA através de conexão do tipo ferramenta. Faz busca semântica em arquivos indexados e devolve trechos como texto para o modelo.

Parâmetros: `provider`, `credenciais`, `rag_arquivos` (**seleção múltipla**), `modelo_embedding` (desabilitado enquanto provedor e credenciais não forem escolhidos), `top_k` (default **5**).

**Saídas: nenhuma. Esquema de saída: nulo.** Não participa do grafo de execução normal — `execute()` **sempre lança** "não pode ser executado diretamente pelo fluxo".

Implementa quatro métodos do contrato de ferramenta (seção 12.3). A descrição exposta ao modelo lista os nomes dos arquivos selecionados, e o esquema de argumentos exige uma `query` não vazia.

**Efeito externo: sim** — gera embedding via API do provedor e consulta o banco vetorial.

### 9.8 Anatomia de um bloco

#### `NodeDefinition` — o contrato de metadados

Descreve um **tipo** de bloco para o canvas e para o compilador.

| Campo | Tipo | Papel |
|---|---|---|
| `type` | string | **Identificador único.** Chave no registro e no JSON do fluxo |
| `label` | string | Nome exibido |
| `icon` | string | Ícone |
| `info` | string | Descrição (aceita HTML) |
| `inputs` | int | Portas de entrada. `0` = bloco de início |
| `outputs` | string[] | Nomes das portas de saída. Vazio = bloco terminal |
| `parameters` | `ParameterDefinition[]` | O formulário de configuração |
| `outputSchema` | class-string? | Classe que descreve o formato do resultado. Alimenta o autocomplete do editor |
| `dynamicOutputs` | bool | Portas derivadas dos parâmetros |
| `singleConnection` | bool | Cada porta aceita no máximo uma conexão |
| `supportsExceptionOutput` | bool | Porta de exceção dedicada |
| `limit` | int? | Máximo de instâncias por fluxo |
| `onSaveCallback` | string? | Função do editor chamada ao salvar, para recalcular topologia |
| `extraConnections` | array | Portas fora do fluxo principal (hoje só a de ferramenta) |

O construtor valida que todo parâmetro é um `ParameterDefinition` e que o esquema de saída existe e estende a base correta.

#### `ConnectionDefinition` — a aresta

Cinco campos:

- `type` — `default` (fluxo normal), `exception` (desvio de erro), `tool` (liga ferramenta a bloco de IA)
- `from` / `to` — ids dos blocos
- `fromOutput` — **o nome da porta de saída na origem**. É o que o motor compara para escolher o caminho
- `toInput` — porta de entrada no destino (relevante para junção, que tem várias)

#### Os 12 tipos de campo de formulário

Campos comuns a todos: `name`, `label`, `type`, `required` (booleano **ou** expressão condicional), `defaultValue`, `isOptional`, `onChange`, `ngDisable`, `help`, `ngIf` / `ngShow`.

| Tipo | Comportamento |
|---|---|
| `uiText` | Texto literal |
| `uiMemo` | Texto multilinha literal |
| `uiNumber` | Numérico, com mínimo, máximo e máscara de casas |
| `uiCheckbox` | Booleano |
| **`uiTwigEditor`** | **Editor de expressão** — o tipo mais usado. O valor é um template renderizado em runtime contra o contexto |
| `uiList` | Combo de opção única, lista estática |
| `uiMultiList` | Seleção múltipla, lista estática |
| `uiSelectDynamicLoad` | Combo que, ao escolher, **carrega um sub-formulário dinâmico** da rota informada. Usado pelas ações de marketplace, cujos campos dependem da ação |
| `uiSuggest` | Autocomplete de entidade, com rota, campos de valor e exibição, callback de resolução no backend e filtros amarrados a outros parâmetros |
| `uiSuggestMultiple` | Idem, seleção múltipla |
| `uiGridForm` | **Tabela editável repetível**: cada linha é um mini-formulário de outros campos (um nível de aninhamento) |
| `uiCallAction` | Não é campo de dado: renderiza um **botão** que dispara uma função do editor |

> **✅ Copiar:** a definição do bloco descreve o formulário de forma declarativa, e o mesmo objeto serve ao canvas e à validação do compilador. Uma fonte, dois consumidores — é o que evita que o editor ofereça um campo que o motor não entende.

---

## 10. Agente de IA no atendimento

### 10.1 As três integrações

Cadastradas em `CRM\Model\OrigemAtendimentoConfiguracaoModel`, escolhidas **por canal**:

| Constante | Valor | Rótulo |
|---|---|---|
| `PANOBOT_IA_TIPO_INTEGRACAO_DIALOGFLOW_ES` | 1 | Dialogflow ES |
| `PANOBOT_IA_TIPO_INTEGRACAO_DIALOGFLOW_CX` | 2 | Dialogflow CX |
| `PANOBOT_IA_TIPO_INTEGRACAO_CONSTRUTOR` | 3 | Panobot IA Construtor |

> **⚠ Colisão de constantes:** `FluxoGrupo` (motor de processos) tem as **mesmas duas últimas constantes com valores diferentes** — `1 = CX`, `2 = Construtor`. Mesma ideia, numeração incompatível, dois módulos.

#### Quando usar cada uma

| Integração | Onde o agente vive | Quando usar |
|---|---|---|
| **Dialogflow ES** | Fora (plataforma clássica) | **Legado.** Restaram dois arquivos no módulo, e o controller de API correspondente está marcado como obsoleto. Não usar em canal novo |
| **Dialogflow CX** | Fora; o ERP é chamado por webhook e responde com um texto do catálogo — a **camada Panobot** | Quando o desenho da conversa (intenções, páginas, fluxos) já está montado na plataforma externa e o ERP só precisa fornecer dados e textos |
| **Fluxo Construtor** | **Dentro do ERP** — o fluxo é desenhado no admin e o bloco de IA chama o provedor diretamente | **Caminho atual.** Dá controle total do grafo, permite misturar passos determinísticos com IA e não depende de conta em plataforma externa |

`isIntegracaoPanobotValida()` define o que cada uma exige para ser considerada configurada:

- ES → identificador de projeto preenchido *(o getter tem typo no fonte: `getCredentiaslProjectId`)*
- CX → ambiente preenchido
- Construtor → versão de fluxo preenchida

Canal sem essa configuração **não é processado pelo bot** — e o log registra "Origem atendimento sem integração válida para processamento", sem erro.

> **🚫 Não replicar:** três caminhos vivos para a mesma função. É custo permanente de manutenção e de teste. Nasça com um, com ponto de extensão claro.

### 10.2 Como o legado se distingue do atual

Para quem for auditar o sistema atual:

- `CRM\Model\Dialogflow\ES\` — **legado**, dois arquivos
- `CRM\Model\Dialogflow\CX\` — atual do caminho Panobot
- `CRM\Model\Dialogflow\IntentResponse` e `QuestionUtil` — **compartilhados**, não são legado
- `CRM\Controller\Api\PanoBotIaApiController` — **legado**, marcado `@deprecated`
- `CRM\Controller\Api\Panobot\PanoBotIaApiController` — **o atual**

Os dois últimos têm **o mesmo nome de classe em namespaces diferentes**. Conferir o import antes de mexer.

### 10.3 A fila do bot

`CRM\Model\AtendimentoBotFila` → `crm.crm_atendimento_bot_fila`

Toda mensagem nova entra na fila pelo callback de gravação de `AtendimentoMensagem`.

Situações (de `InterfaceFilaBotSituacao`, **compartilhadas com o trâmite de processo**):

- `SITUACAO_NOVO = 1`
- `SITUACAO_RESPONDIDO = 2`
- `SITUACAO_SEM_RESPOSTA = 3`
- `SITUACAO_ERRO = 4`
- `SITUACAO_RESTRICAO = 5`
- `SITUACAO_PROCESSANDO = 6`

**Despacho** (`AtendimentoBotFilaController::processFila()`): ES e CX caem no mesmo caminho Dialogflow; Construtor cai no caminho do motor de fluxo.

#### Continuidade de conversa no caminho do motor de fluxo

1. busca o vínculo sessão↔atendimento mais recente;
2. **descarta a sessão se a versão do fluxo mudou no canal** — trocar o fluxo configurado reinicia a conversa de todos os atendimentos abertos, de propósito;
3. executa com o atendimento e o texto da pergunta como entrada;
4. vincula a sessão nova ao atendimento, **inclusive no caminho de erro** — sem isso a sessão de erro ficaria órfã e a próxima mensagem recomeçaria do zero.

Resultado → situação de fila: exceção tratada pelo gerenciador global → `ERRO`; rodou mas não respondeu o cliente → `SEM_RESPOSTA`; respondeu → `RESPONDIDO`.

### 10.4 Termos: normalizar a pergunta e barrar a resposta

`CRM\Model\AtendimentoBotTermo` → `crm.crm_atendimento_bot_termo`

Campos: `termo`, `tipo`, `origem` (array JSON de canais), `situacaoPedidoList` (array JSON), `intent`, `fluxo` (FK).

**Seis tipos, em dois papéis distintos.**

*Normalizar a pergunta* — aplicados antes de consultar o agente, sobre o texto já convertido para minúsculas e sem HTML:

- `TIPO_REMOCAO_TERMO = 1` — remove o termo da pergunta (ruído: saudações, assinaturas)
- `TIPO_DIVISAO_ESQUERDA = 3` — corta no termo e mantém o que vem **antes**
- `TIPO_DIVISAO_DIREITA = 4` — corta no termo e mantém o que vem **depois**

*Barrar a resposta* — três verificações em conjunção, aplicadas depois de a resposta ser gerada:

- `TIPO_RESTRICAO_INTENT = 2` — se o termo aparece **na última pergunta do cliente**, a resposta é invalidada
- `TIPO_RESTRICAO_INTENT_SITUACAO_PEDIDO = 6` — invalida se a situação do pedido estiver na lista do termo
- `TIPO_RESTRICAO_INTENT_PROCESSO = 5` — invalida se o fluxo do termo estiver entre os fluxos dos processos do pedido

**Condições de aplicabilidade** (comuns aos três de restrição): o canal do atendimento precisa estar na lista do termo — canal não listado deixa o termo **inerte** —, e a intenção do termo precisa estar vazia (vale para todas) ou ser igual à corrente.

**Quando uma restrição dispara:** o bot não responde, um erro de diagnóstico é registrado com resposta gerada, termo, posições e contexto, e **uma mensagem é gravada no histórico do atendimento**, visível ao operador: "Restrição de Resposta na API (Termo na Pergunta | Situação do Pedido | Processo)".

**Curto-circuito:** a validação inteira é pulada se não houver atendimento, se o texto for sintético (gerado por uma operação, sem id), ou se o provedor não declarar essa capacidade.

Cache de 30 minutos, em memória, agrupado por tipo.

> **✅ Copiar:** a separação entre *normalizar a entrada* e *barrar a saída* é limpa e resolve dois problemas diferentes com um cadastro só. E a mensagem no histórico é o que torna "por que o bot não respondeu?" respondível pelo operador, sem abrir log.

> **Nota histórica registrada no próprio código:** a verificação do termo no **texto da resposta** foi desativada em 2020; hoje só a pergunta é examinada. Vale decidir conscientemente na cópia.

### 10.5 Camada Panobot: o ERP como fonte de dados do agente

Panobot é o nome da integração em que **o agente está fora** e o ERP responde perguntas dele.

#### O ciclo de uma requisição

1. `PanobotOperationParameterFactory::create()` resolve o handler da operação e chama `defineParameters()`;
2. os filtros são montados e o texto do catálogo é escolhido (10.8);
3. o template é renderizado, carregando **só as variáveis que o texto cita** (5.8);
4. se a resposta é válida, um processo pode ser aberto, caso o texto esteja configurado para isso;
5. o `replyBlocked` do texto é aplicado ao atendimento;
6. **um log é gravado no atendimento dizendo qual texto respondeu e com quais parâmetros**;
7. o provedor serializa no formato esperado.

> **✅ Copiar o passo 6.** É o que torna a resposta do bot auditável: dá para abrir o atendimento e ver exatamente qual registro do catálogo respondeu e por quê.

#### O contrato do provedor

`AbstractPanobotBaseProvider` exige:

```php
abstract public function response(ProcessTextoPadraoResponse $r);
abstract public function erro(Exception $e);
abstract public function identifyParameters(): Enumerator;
abstract public function getTipoTextoPadraoUse(): int;   // atendimento ou processo
```

As capacidades são detectadas **por trait**, cada uma com um flag `providerUse*`:

| Trait | O que acrescenta |
|---|---|
| `...UseFilterAgent` | Identifica o agente (vira filtro) |
| `...UseFilterIntentOrTag` | Intenção/tag; guarda a original e a normalizada — **ambas viram filtro** |
| `...UseFilterCanalVenda` | Resolve o canal: loja do marketplace do pedido → loja do pedido → loja do atendimento |
| `...UseFilterIdentificador` | Seleção direta de um texto por identificador |
| `...UseVariableDefaultMessage` | Mensagem padrão de fallback do canal |
| `...UseVariableProdutoDerivacao` | Produto do atendimento e suas derivações |
| `...VerficaRestricaoResposta` | Aplica os termos de restrição (10.4) |

> **✅ Copiar:** capacidade por composição, não por `if` de tipo. Um provedor novo ganha ou perde comportamento incluindo ou não um trait, e o resto do código não muda.

Única implementação hoje: o provedor de Dialogflow CX.

### 10.6 As 16 operações que a IA pode pedir ao ERP

Cada operação tem exatamente um handler. O factory resolve **por convenção de nome** — `list-ordens-compra` → `ListOrdensCompra` → `ListOrdensCompraParameterProvider` — e lança se a classe não existir. Adicionar uma capacidade é criar uma constante e uma classe com o nome derivado; **não há registro a atualizar**.

**Dois modos de retorno, mutuamente exclusivos:**

- **O handler produz dados** — injeta um texto sintético cujo conteúdo é o JSON; esse JSON é o payload devolvido à IA.
- **O handler só define filtros** — a resposta vem do texto cadastrado no banco.

| Operação | Entrada | Saída |
|---|---|---|
| `mensagem` | — | Não produz dados; restringe a busca a textos do tipo mensagem (linguagem natural) |
| `parametros` | — | Idem, restringe ao tipo parâmetros (texto cujo conteúdo é JSON) |
| `lock-bot-response` | atendimento no contexto | `replyBlocked = true` — o bot para de responder aquele atendimento |
| `unlock-bot-response` | idem | `replyBlocked = false` |
| `consultar-frete` | `CEP` (obrigatório, validado), `SKU` (obrigatório), `QUANTIDADE` (default 1), `LOJA`, `CANAL_VENDA` | Não retorna JSON: alimenta as variáveis `CIDADE`, `CEP`, `VALOR_FRETE`, `PRAZO_FRETE`, `SKU` do texto |
| `caracteristica-produto` | `SKU` (ou o do atendimento), `NOME_CARACTERISTICA` (obrigatório, normalizado para minúsculas com hífen) | Alimenta `NOME_CARACTERISTICA` e `VALOR_CARACTERISTICA`. **Lança se a característica não existir** |
| `list-pedidos` | `CPF_CNPJ` (ou deriva do pedido, ou do cliente do atendimento), limite (default 15, máx. 100) | Lista de pedidos |
| `list-pedido-itens` | pedido (parâmetro ou do atendimento), limite (máx. 5000) | Itens do pedido |
| `list-pedido-transporte` | pedido; opcionais item e SKU; limite 15 | Transportes, com datas em formato ISO |
| `list-ordens-compra` | pedido; opcionais item e SKU; limite 15 | Ordens **agrupadas por número**, com itens aninhados |
| `list-notas-fiscais` | pedido | NFs de venda, de origem e de assistência, deduplicadas, cada uma com itens e links de XML e PDF. Cache de 10 min |
| `list-recibos-entrega` | pedido; limite 15 | Data, nome do recebedor e link do canhoto |
| `list-processos` | pedido; opcionais tipo de trâmite e fluxo atual (**aceitam código de integração ou id**), número do processo; limite 50 | Processos por abertura decrescente, com trâmites (mensagem, fluxo, assunto, motivo, pessoas, formulário, anexos). Busca em lote para evitar N+1 |
| `detail-produto` | `SKU` (ou o do atendimento) | Cinco blocos: dados do produto, variações, características, links e mídia. **Mídia é opcional — falha silenciada** |
| `list-manual-produto` | `SKU` (estrito, sem fallback) | Tipo e URL de cada anexo do produto |
| `list-produtos-relacionados` | `SKU`, percentual de diferença de valor (default 10), limite (default 5, máx. 20) | SKU, título e links, filtrados por faixa de preço e estoque mínimo. Cache de 1 min |

#### O contrato comum dos handlers

Um único método abstrato: `defineParameters(): void` — sem retorno, agindo por efeito no provedor.

Helpers herdados que valem copiar:

- **Leitura tolerante de parâmetro** — tenta a chave exata, depois em minúsculas, e cai para a query string. Modelos de linguagem não são consistentes com maiúsculas.
- **Limite com teto por operação** — lê o limite pedido e lança se exceder o máximo daquela operação. Sem isso, o agente pede 100 mil registros.
- **Resolução de pedido em cascata** — id interno → código externo → código no marketplace → identificador de entrega. **Copiar:** o cliente informa o número que ele tem, que raramente é o id interno.
- **Resolução de produto** — SKU, com fallback opcional para o produto do atendimento.
- **Links de download assinados** para documentos e notas fiscais.

> **✅ Copiar o desenho:** o agente externo **não consulta o banco**. Ele pede uma operação nomeada; o handler traduz aquilo em parâmetros. A superfície exposta é uma lista fechada de 16 verbos, não um esquema de banco.

> **⚠ Armadilha de segurança:** a consulta de pedidos por CPF/CNPJ informado na conversa **não verifica titularidade** — quem digitar um documento qualquer recebe os pedidos daquele documento. Ver 15.10.

### 10.7 O catálogo de variáveis de template

A "linguagem" que um texto do catálogo pode usar. Convenção: `CAIXA_ALTA` é valor escalar já formatado; `camelCase` é objeto de domínio navegável.

**Provedor** (sempre presentes)
- `agent`
- `intent`
- `respostaPadrao`

**Atendimento**
- `NOME_CLIENTE`
- `NUMERO_PROTOCOLO`
- `NUMERO_PEDIDO`
- `CODIGO_MARKETPLACE_PEDIDO`
- `ID_ENTREGA_MARKETPLACE_PEDIDO`
- `PREVISAO_ENTREGA`
- `LINK_RASTREIO`
- `NOME_TRANSPORTADORA`
- `TELEFONE_TRANSPORTADORA`
- `NOME_REDESPACHO`
- `TELEFONE_REDESPACHO`
- `NUMERO_NOTA`
- `STATUS_PEDIDO`
- `atendimento` (objeto)
- `PEDIDOS_CLIENTE_LIST`

**Processo e trâmite**
- `PROCESSO` (objeto)
- `PROCESSO_TRAMITES` (lista)

**Pedido**
- `pedido` (objeto)
- Rastreio: `PRAZO_ESTIMADO_REDESPACHO`, `DATA_ENTROU_TRANSPORTE`, `DATA_LIMITE_ATUALIZADA`, `DATA_LIMITE`, `ULTIMO_RASTREIO`, `LOG_RASTREIO`
- Prazos: `PRAZO_ESTIMADO`, `NOVO_PRAZO_ENTREGA`
- Listas: `NOTA_FISCAL_PEDIDO_LIST`, `ORDEM_COMPRA_PEDIDO_LIST`, `ITENS_PEDIDO_LIST`, `TRANSPORTE_PEDIDO_LIST`, `PROCESSO_PEDIDO_LIST`, `RECIBOS_ENTREGA_LIST`

**Produto**
- `PRODUTO_NOME`
- `PRODUTO_ALTURA`
- `PRODUTO_LARGURA`
- `PRODUTO_PROFUNDIDADE`
- `PRODUTO_PESO_BRUTO`
- `PRODUTO_PESO_LIQUIDO`
- `PRODUTO_SKU`
- `PRODUTO_COR`
- `PRODUTO_CATEGORIA`
- `produtoDerivacao` (objeto)
- `PRODUTO_DETAIL`, `PRODUTO_MANUAL_LIST`, `PRODUTOS_RELACIONADOS_LIST`

**Cotação de frete**
- `CIDADE`, `CEP`, `VALOR_FRETE`, `PRAZO_FRETE`, `SKU`

**Característica de produto**
- `NOME_CARACTERISTICA`, `VALOR_CARACTERISTICA`

#### O detalhe que faz a diferença

Cada grupo tem **subgrupos declarados**, e eles não são organização cosmética: **são a unidade de carregamento preguiçoso**. O renderizador procura o nome de cada variável no texto e só então carrega o subgrupo correspondente.

Variável não citada no texto = **zero consulta ao banco**.

> **✅ Copiar com essa granularidade.** Reimplementar o catálogo sem os subgrupos funciona, mas transforma cada resposta do bot em dezenas de consultas. É o tipo de degradação que só aparece em produção e é caro de corrigir depois.

Os traits que populam cada contexto também definem a **árvore de autocomplete** que o editor de textos oferece — de novo, uma fonte, dois consumidores.

### 10.8 Como o texto de resposta é escolhido

Consulta única sobre o catálogo, com a configuração em JSON, retornando **um** registro.

#### Regra central: "casa ou é curinga"

Para agente, intenção, identificador, situação do pedido, canal de venda e origem, a condição é:

```
valor configurado no texto = valor do parâmetro   OU   valor configurado é nulo/vazio
```

Um texto sem o campo configurado é **genérico** e concorre com todos.

Detalhes por filtro:

| Filtro | Comportamento |
|---|---|
| Agente, identificador | Igualdade escalar ou vazio |
| Intenção | Casa com a normalizada **ou** com a original, ou vazio |
| **Origem** | O parâmetro precisa estar no array configurado, **ou** o array estar vazio. **Assimetria:** se a origem não vier, só passam textos com origem vazia |
| **Situação do pedido** | Igual — sem o parâmetro (atendimento sem pedido), só passam textos sem situação configurada |
| Pedido | Dois efeitos: se o texto exige "em atraso", verifica se há transporte vencido e não cancelado; e havendo pedido, **exclui** textos marcados "pedido não encontrado" |
| Processo | Casa se o fluxo e a situação configurados estiverem entre os dos processos do pedido, ou se a condição não estiver habilitada |
| Tipo de resposta | `IN` **estrito, sem curinga** — é o que separa `mensagem` de `parametros` |

#### Desempate por especificidade

Ordenação decrescente, quatro níveis:

1. texto com condição de processo habilitada → peso **5**
2. texto que exige pedido em atraso → peso **4**
3. texto com origem configurada → peso **3**
4. **aleatório**

Quanto mais condições específicas o texto declara, maior a prioridade. Textos genéricos são último recurso. E entre textos igualmente específicos, **a escolha é aleatória** — variação intencional de resposta, para o bot não soar como robô repetindo a mesma frase.

> **✅ Copiar o desenho inteiro.** Especificidade decrescente com curinga permite cadastrar uma resposta genérica e ir refinando por canal, por situação, por atraso, sem tocar nas anteriores. E a aleatoriedade no empate é um detalhe pequeno com efeito grande na percepção do cliente.

### 10.9 Base de conhecimento (RAG)

#### Modelo

`FluxoConstrutorRAGArquivo` → `un_fluxo_construtor_rag_arquivo`

| Coluna | Tipo | Significado |
|---|---|---|
| `fcraq_id` | int | PK |
| `fcraq_nome` | varchar(255) | **Vira parte da descrição da ferramenta para o modelo** |
| `fcraq_descricao` | text | Idem |
| `doc_id` | FK → `DocumentoEletronico` | O arquivo |
| `fcraq_status` | smallint | `1` aguardando, `2` processando, `3` pronto, `4` erro |
| `fcraq_total_chunks` | int | |
| `fcraq_modelo_embedding` | varchar(255) | |
| `fcraq_dim_embedding` | int | |
| `fcraq_chunk_size` | int | |
| `fcraq_overlap` | int | |
| `fcraq_provider_id` | FK → `FluxoConstrutorParametro` | |
| `fcraq_credencial_id` | FK → `FluxoConstrutorParametro` | |
| `fcraq_erro_msg` | text | |
| `fcraq_criado_em` / `fcraq_atualizado_em` | datetime | |

`FluxoConstrutorRAGArquivoChunk` → `un_fluxo_construtor_rag_arquivo_chunk`

- `fcraqc_numero` — ordem do trecho
- `fcraqc_conteudo` — text
- `fcraqc_embedding` — **tipo vetorial, dimensão 1536**
- `ON DELETE CASCADE` a partir do arquivo

#### O ciclo

1. **Upload** — o arquivo vai para o documento eletrônico.
2. **Disparo** — gera uma **URL assinada válida por 1 hora** e chama um **serviço externo** de processamento. Defaults: tamanho de trecho **800**, sobreposição **100**.
3. **Retorno** — o serviço chama de volta um webhook com eventos `chunk`, `done` e `error`, autenticado por cabeçalho com **TTL de 7200 segundos**; o classificador distingue token válido, inválido e expirado. **Cada trecho é vetorizado no momento da gravação.**
4. **Busca** — a pergunta é vetorizada e comparada por distância vetorial:

```sql
SELECT fcraqc_conteudo
  FROM un_fluxo_construtor_rag_arquivo_chunk
 WHERE fcraq_id IN (:arquivos)
   AND fcraqc_embedding IS NOT NULL
 ORDER BY fcraqc_embedding <=> CAST(:queryEmbedding AS vector)
 LIMIT :topK
```

Os trechos são concatenados com um separador e devolvidos ao modelo como texto. `topK` default **5**.

> **✅ Copiar:** fatiamento assíncrono com estado próprio no arquivo. Subir um documento grande não pode travar a tela, e o usuário precisa saber se já está pronto.

> **⚠ Limitação a evitar:** apenas um dos provedores implementa vetorização. A escolha do provedor da base de conhecimento é mais restrita que a do agente, **sem que a interface explique isso**. Garanta paridade, ou explicite a restrição no formulário.

---

## 11. Fluxos ponta a ponta

Oito percursos completos, do gatilho ao efeito final, citando as tabelas tocadas em cada passo. É a seção que mostra como as peças se encaixam.

### 11.1 Mensagem de canal conversacional vira atendimento

**Gatilho:** o canal (mensageria ou rede social) entrega um webhook.

1. O controller de webhook na borda valida a assinatura e delega ao processador do canal.
2. O processador extrai a lista de mensagens do payload e, **para cada mensagem**, abre transação.
3. Identifica o remetente e monta a chave de trava `prefixo:identificador:loja`. Toma trava cooperativa de transação (`pg_advisory_xact_lock`).
4. Resolve a estratégia do canal e pergunta a ela como formatar um atendimento.
5. Busca ou cria o atendimento → **`crm.crm_atendimento`**, com `ate_origem` do canal, `loj_id`, `ate_identificador_plataforma_externa` e `emp_id` preenchido automaticamente pelo multi-tenancy.
6. O callback de gravação produz a linha de histórico → **`crm.crm_atendimento_log`**.
7. Se o responsável atual estiver desativado, o atendimento é solto.
8. Monta a mensagem → **`crm.crm_atendimento_mensagem`**. No persist, calcula o vencimento a partir da configuração do canal e grava em `atemsg_data_hora_limite`.
9. Se há anexo, valida extensão e tamanho contra a configuração do canal, sobe o arquivo → **`documento.dc_documento_eletronico`** (com dedupe por hash) e liga → **`crm.crm_atendimento_mensagem_anexo`**.
10. Atualiza `ate_ultima_mensagem_id` no atendimento.
11. O callback de gravação da mensagem **enfileira o bot** → **`crm.crm_atendimento_bot_fila`** com situação `NOVO`.
12. Commit. Fora da transação, dispara notificação assíncrona da interface.

**Se a mesma entrega chegar duas vezes em paralelo:** a segunda trava no passo 3 até a primeira terminar, e então encontra o atendimento já criado.

### 11.2 Operador responde e o prazo é medido

**Gatilho:** o operador abre a tela do atendimento.

1. `init()` verifica se a situação está entre aberto, finalizado, ignorado ou reservado, e move para **em andamento**, gravando o responsável → `crm_atendimento`, mais uma linha em `crm_atendimento_log`.
2. O formulário carrega o catálogo de respostas filtrado pelo contexto — situação do pedido, atraso, canal de venda → consulta em `crm_texto_padrao` sobre `tex_configuracao.opcoes`.
3. Se o operador tem restrição de texto livre → `crm_usuario_bloqueio_mensagem`, o campo de mensagem é travado e só o catálogo é oferecido.
4. O operador escolhe um texto. O renderizador procura no texto quais variáveis aparecem e **carrega só essas** do ERP.
5. A mensagem é gravada → `crm_atendimento_mensagem` com `usu_id` preenchido (autor interno) e `atemsg_manual` conforme a origem do texto.
6. A estratégia do canal envia a resposta ao cliente.
7. O operador encerra → `finalizar()` grava **finalizado** e limpa o motivo de cancelamento; nova linha em `crm_atendimento_log`.

**A medição depois:** o cálculo de atraso procura, para cada mensagem com prazo, o log de finalização que não tem outro "em andamento" depois dele, e soma a diferença contra `atemsg_data_hora_limite`. O custo de atendimento soma, para cada log de "em andamento", o tempo até a primeira finalização do mesmo usuário.

### 11.3 O bot do motor de fluxo responde e pausa esperando o cliente

**Gatilho:** a fila do bot tem uma mensagem nova num canal configurado com integração do tipo construtor.

1. O despachante lê a configuração do canal → `crm_origem_atendimento_configuracao`, confirma que a integração é válida (versão de fluxo preenchida).
2. Busca o vínculo sessão↔atendimento mais recente → `crm.crm_atendimento_fluxo_construtor_sessao`.
3. **Se a versão do fluxo da sessão não for a versão configurada hoje, descarta a sessão** — a conversa recomeça.
4. Monta os dados de entrada com o atendimento e o texto da pergunta.
5. O executor compila o JSON da versão → `un_fluxo_construtor_versao.fcv_configuracao`, em memória.
6. Se há sessão, restaura variáveis, metadados e blocos pendentes → `un_fluxo_construtor_sessao`.
7. Roda o loop. Digamos que o fluxo consulte o pedido, responda ao cliente e peça confirmação:
   - bloco de condição avalia se há pedido vinculado;
   - bloco de chamada HTTP ou operação de consulta obtém o rastreio;
   - **bloco de resposta com `await = true`** envia a mensagem → `crm_atendimento_mensagem` e **marca pausa**.
8. O executor guarda os blocos pendentes em `fcs_pending_nodes`, grava `fcs_variables` e `fcs_updated_at`, e **sai do loop**.
9. Grava a execução → `un_fluxo_construtor_execucao`, com o dump dos passos em documento eletrônico (só se o fluxo estiver em modo debug).
10. Vincula a sessão ao atendimento se for nova.
11. A fila recebe `RESPONDIDO`.

**Quando o cliente responde:** volta ao passo 1, a sessão é encontrada, o hash bate, e o loop **retoma dos blocos pendentes** com as variáveis preservadas.

**Se o cliente não responder:** o TTL da sessão (default 30 minutos, configurável por fluxo) expira e a próxima mensagem começa do zero.

### 11.4 O agente de IA consulta o ERP e responde

**Gatilho:** agente externo chama o webhook do ERP com uma operação.

1. O controller resolve o provedor e, pelo nome da operação, o handler — por convenção de nome, sem registro.
2. O handler lê os parâmetros (tolerante a maiúsculas), resolve pedido em cascata (id → código externo → código de marketplace → identificador de entrega) e produz **ou** dados **ou** filtros.
3. Os filtros do provedor são montados: agente, intenção (normalizada e original), origem, situação do pedido, canal de venda, processos do pedido, tipo de resposta.
4. A consulta ao catálogo aplica "casa ou é curinga" e ordena por especificidade — condição de processo (5), atraso (4), origem (3), aleatório.
5. **Um** texto é escolhido → `crm_texto_padrao`.
6. O renderizador varre o texto, carrega só os subgrupos de variáveis citados, e renderiza.
7. Os termos de restrição são aplicados: se o termo aparece na última pergunta, ou a situação do pedido está vetada, ou o fluxo do processo está vetado → **a resposta é invalidada**, um erro de diagnóstico é registrado e uma mensagem explicativa vai para o histórico do atendimento.
8. Se válida: o texto pode abrir um processo (se configurado), pode aplicar `replyBlocked` ao atendimento, e **um log é gravado dizendo qual texto respondeu e com quais parâmetros**.
9. O provedor serializa e devolve.

### 11.5 Atendimento vira processo e o trâmite volta como mensagem

**Gatilho:** o operador decide que a dúvida exige trabalho interno, ou um texto do catálogo configurado para abrir processo é usado pela IA.

1. O formulário de processo recebe o id do atendimento.
2. O controller monta o processo: fluxo, centro de custo, solicitante, prioridade. Valida os cinco obrigatórios.
3. Gera o número → transação curta em `processo.pro_sequencia_processo`, **confirmada antes** de o processo ser gravado.
4. Calcula a previsão a partir do prazo do fluxo (dias úteis + horas corridas).
5. Persiste → `processo.pro_processo`.
6. Cria o trâmite inicial → `processo.pro_processo_tramite`, herdando fluxo, tipo e centro de custo.
7. **Reescreve as quatro colunas de projeção** do processo com os valores do trâmite.
8. Grava o vínculo → **`processo.pro_processo_atendimento_origem`**.
9. Se a etapa tem evento configurado, cria a linha de execução → `processo.pro_processo_tramite_evento_processo` com situação `FILA`.
10. Dispara as notificações da etapa conforme o destino → `un_notificacao` (uma por contato) e registra em `processo.pro_processo_tramite_notificacao`.

**A volta, a cada trâmite seguinte:** com `notificaOrigemProcesso` ligado, o controller busca a **última** linha de vínculo do processo e envia a mensagem do trâmite de volta ao atendimento — gravando em `crm_atendimento_mensagem` com **`protra_id` preenchido**, que é o que amarra a mensagem ao passo que a gerou.

O cliente acompanha o andamento sem sair da conversa que abriu, e o CRM não precisa saber o que é um trâmite.

### 11.6 Evento de processo emite nota fiscal

**Gatilho:** um trâmite chega numa etapa que tem evento de emissão configurado.

1. Na gravação do trâmite, a linha de execução é criada → `processo.pro_processo_tramite_evento_processo`, situação `FILA`, com os parâmetros do evento copiados.
2. O consumidor de fila lê até 250 linhas em `FILA` ou em `ERRO` com menos de duas tentativas.
3. Para cada uma, abre transação própria e despacha pelo código do evento.
4. O executor de emissão lê os parâmetros tipados, aplica os filtros (nota fiscal, SKUs, ordem de compra) e decide.
5. **Se as condições não se aplicam** → situação `IGNORADO`. Não é falha, e é o desfecho mais comum.
6. Se emite: grava a NF e preenche `nf_id` na linha de execução; situação `FINALIZADO`.
7. Se falha: situação `ERRO`, mensagem em `event_log`, contador incrementado em `event_params.errors` — **e o consumidor encerra a execução inteira** (ver armadilha 15.11).

A grid de eventos permite reprocessar manualmente linhas em erro ou ignoradas — é o caminho de recuperação.

### 11.7 Requisição HTTP entra por fluxo de API

**Gatilho:** um sistema externo chama a URL do endpoint.

1. O controller extrai o código do fluxo da URL. Ausente → `400`.
2. Busca o fluxo por código **e tipo API**. Não encontrado → `404`.
3. Confere o método contra os métodos permitidos em `fc_configuracao`. Não permitido → `405`.
4. Resolve a versão: `?versao=` na query, senão a última publicada. Nenhuma → `503`. Rascunho pedido explicitamente → `400`.
5. Faz o parse do corpo e dos cabeçalhos, monta os dados de entrada do tipo API.
6. Executa o fluxo. Tipicamente: bloco de validação bifurca entre dados válidos e inválidos; o ramo válido faz o trabalho; os dois ramos terminam num bloco de resposta.
7. Se um bloco de resposta produziu saída, ela é devolvida com o status, o tipo de conteúdo e os cabeçalhos configurados. Senão, `200 {"success": true}`.

**Não há sessão nem retomada neste modo** — cada requisição é independente.

### 11.8 Dois atendimentos negociam entre si

O percurso mais incomum, e o que mostra a flexibilidade do motor. Ocorre em disputas de marketplace onde há um atendimento do comprador e outro da plataforma.

1. O fluxo do atendimento A chega num **bloco de aguardar evento**: altera a situação do atendimento para tirá-lo da fila do bot, marca pausa e guarda os pendentes.
2. O fluxo do atendimento B, que corre em paralelo, resolve a disputa e chega num **bloco de disparar evento**, apontando A como alvo.
3. O bloco cria uma mensagem no atendimento A → `crm_atendimento_mensagem`, com o resultado em `atemsg_params`, e **enfileira o bot de A** → `crm_atendimento_bot_fila` com situação `NOVO`.
4. O consumidor da fila pega A, encontra a sessão pausada, e **retoma do bloco de aguardar evento** com o payload do resultado disponível no contexto.
5. O fluxo de A segue: decide, responde ao cliente, encerra.

**O conceito:** a pausa não é só "esperar o cliente". É um mecanismo geral de espera, e a retomada pode ser disparada por qualquer coisa que enfileire o bot — inclusive outro fluxo.

---

## 12. Contratos de extensão

Assinatura e responsabilidade de cada ponto de extensão. São seis.

### 12.1 Canal conversacional novo

**1. Processador de entrada** — estende a base, cujo `processar()` é `final`:

```php
abstract protected function getPayloadMensagens(array $payload): array;
// Extrai a lista de mensagens do payload bruto do canal.

abstract protected function getIdentificadorRemetente(array $mensagemPayload): string;
// Quem é o cliente neste canal (telefone, id de conta, e-mail).
// É metade da chave de trava e de agrupamento.

abstract protected function getLockPrefix(): string;
// Namespace da trava de concorrência. Distingue canais.

abstract protected function resolverStrategy(array $mensagemPayload): CrmOrigemFormatterStrategyInterface;
// Qual estratégia formata e responde esta mensagem.
// Permite um processador servir vários canais próximos.
```

Ganchos opcionais: `prepararAtendimentoFormatter()`, `prepararMensagemFormatter()`, `prepararMensagem()`, `prepararAnexoFormatter()`, `isRemocao()`, `processarRemocao()`, `postPersistNewAtendimento()`, `postPersistNewMessage()`, `getAsyncUrl()`.

**2. Estratégia de formatação e resposta:**

```php
public function getOrigem(): int;
// O identificador do canal.

public function createAtendimentoFormatter(): AtendimentoCrmBaseFormatterResponse;
public function createMensagemFormatter(): AtendimentoMensagemCrmBaseFormatterResponse;
public function createAnexoFormatter(): ?AtendimentoMensagemAnexoCrmBaseFormatterResponse;
// Converte payload em entidade. Anexo pode ser nulo (canal sem anexo).

public function reply(AtendimentoMensagem $m, LojaConfiguracao $c, array $anexos = []): EventResponse;
// Envia a resposta ao cliente. O retorno diz se concluiu e, se não, por quê.
```

**3. Registro no factory** — um `case` mapeando canal → estratégia.

**O que você não implementa:** transação, trava de concorrência, agrupamento por remetente, liberação de responsável inativo, notificação da interface, cálculo de prazo, trilha de auditoria.

### 12.2 Bloco de fluxo novo

```php
abstract public function execute(): AbstractNodeOutput;
// A operação. Recebe o contexto por setter antes da chamada.

abstract public static function getDefinition(): NodeDefinition;
// Metadados: tipo, rótulo, ícone, entradas, saídas nomeadas,
// parâmetros do formulário, esquema de saída, limites, callbacks.

abstract public static function beanModel(array $data): AbstractNode;
// Reconstrói a instância a partir do JSON do grafo.
```

Mais:

- uma subclasse do resultado, descrevendo o que o bloco entrega ao contexto (reaproveitar um existente é legítimo — dois blocos já compartilham);
- os tipos de campo do formulário, escolhidos entre os 12 disponíveis;
- se a falha deve ser fatal, uma exceção que estenda a base fatal — ela **não** é capturável pelo tratamento configurado;
- **registro com o escopo correto.** Sem isso o bloco não existe nem para o compilador nem para o canvas.

O registro valida a herança e indexa pelo `type` da definição — o tipo é a chave que aparece no JSON do grafo.

### 12.3 Ferramenta de IA nova

Um bloco que, **além** do contrato acima, implementa:

```php
public function getAIToolName(): string;
// Nome da função exposta ao modelo.

public function getAIToolDescription(): string;
// Descrição que o modelo lê para decidir se chama.
// Vale compor dinamicamente — a ferramenta de busca lista os arquivos selecionados.

public function getAIToolParameters(): array;
// Esquema dos argumentos aceitos.

public function executeAITool(array $arguments): string;
// Executa e devolve resultado textual.
```

Registro com o tipo de conexão de ferramenta. O `execute()` normal deve **lançar** — a ferramenta não participa do grafo de execução.

A ligação ao agente é feita **no canvas**, por conexão. Em runtime, o executor percorre as conexões de ferramenta que chegam ao bloco de IA e monta o conjunto que o bloco declara ao provedor.

### 12.4 Executor de evento de processo novo

1. Uma constante de código no catálogo de eventos.
2. Um *value object* de parâmetros tipado, lendo o JSON da definição.
3. Um executor com um método de processamento, que recebe a linha de execução e decide entre finalizar, ignorar ou falhar.
4. Um `case` no despachante da fila.

**Responsabilidade do executor:** definir a situação final de forma explícita. "Ignorado" quando as condições não se aplicam; "erro" apenas quando algo deu errado de verdade. Confundir os dois torna a fila ilegível.

### 12.5 Provedor de IA novo

1. Cliente HTTP estendendo a base de provedores.
2. Se suportar vetorização, implementar também a interface de embedding — **sem ela, o provedor não serve para base de conhecimento**.
3. Uma semente que cadastre o provedor e seus modelos no catálogo de parâmetros.

### 12.6 Operação de agente nova

1. Uma constante na lista de operações.
2. Uma classe com o nome derivado da constante pela convenção kebab → Pascal + sufixo, implementando `defineParameters(): void`.

Não há registro a atualizar. **Mas há uma responsabilidade obrigatória:** se a operação consulta dados de uma pessoa, **verificar titularidade** (ver 15.10).

---

## 13. Integrações externas

O que o sistema fala com o mundo, por categoria.

| Categoria | Com quem | Protocolo | Direção |
|---|---|---|---|
| **Mensageria** | Plataforma de WhatsApp Business | HTTP + webhook | Ambas |
| | Plataforma Meta (Messenger, Instagram Direct, comentários) | HTTP + webhook | Ambas |
| **Marketplaces** | Mercado Livre, Magazine Luiza, Shopee, TikTok Shop, Kabum, Colombo, Leroy Merlin, Via Varejo, Amazon, MadeiraMadeira, B2W *(arquivado)* | HTTP, cada um com sua API | Ambas — pedidos, perguntas, reclamações, pós-venda |
| **IA — agente externo** | Plataforma de agente conversacional (versões clássica e nova) | Webhook de entrada | Entrada |
| **IA — provedores de modelo** | Provedores de modelo de linguagem (um com vetorização, outro sem) | HTTP | Saída |
| **RAG** | Serviço próprio de fatiamento de documento | HTTP + webhook autenticado por token de vida curta | Ambas |
| **Armazenamento** | Armazenamento de objetos | SDK | Saída |
| **E-mail** | SMTP, configurado por empresa | SMTP | Saída |
| **Push** | Serviço de notificação push (com detecção de token de app) | HTTP | Saída |
| **Transporte** | Transportadoras e correios, para cotação e rastreio | HTTP | Saída |
| **Fiscal** | Emissão de nota fiscal | Interno ao ERP | — |

**Padrão de entrada (webhook):** controller de borda → validação → processador do domínio. Todos entram por trava de concorrência e transação por item.

**Padrão de saída:** credenciais nunca no código nem no documento versionado — sempre em registro de parâmetro ou configuração por empresa.

> **⚠ Observação:** o token do webhook de RAG tem TTL e classificação de expirado, mas essa disciplina **não é uniforme** em todas as entradas. Padronize na cópia: toda entrada externa com autenticação verificável e idempotência por identificador de evento.

---

## 14. Decisões de arquitetura

Cada decisão com a alternativa descartada, o motivo, o custo aceito e o nível de confiança que temos hoje.

### 14.1 Separar o desenho do processo da instância

**Decisão:** um "fluxo" cadastrável define as etapas; um "processo" é uma instância percorrendo esse desenho; o passo executado (trâmite) é uma linha imutável.

**Alternativa descartada:** uma máquina de estados codificada por tipo de demanda, com tabela e tela para cada.

**Motivo:** cada tipo novo custaria desenvolvimento, e a empresa tem dezenas. Com o desenho em dado, criar um tipo é cadastro.

**Custo aceito:** o motor não pode ter opinião sobre as etapas — daí a classificação em quatro categorias (6.7), que é o mínimo de semântica para responder "acabou?".

**Confiança: alta.** Oito módulos usam o mesmo motor sem adaptação.

### 14.2 O processo é a projeção do último trâmite

**Decisão:** o estado atual é denormalizado a partir do último trâmite, reescrito a cada passo, num único método.

**Alternativa descartada:** o processo ser dono do próprio estado, com o trâmite servindo de log paralelo.

**Motivo:** com estado próprio, log e realidade divergem — e divergem sempre, porque algum caminho atualiza um e esquece o outro. Com projeção, a divergência é impossível por construção.

**Custo aceito:** quatro colunas redundantes no processo.

**Confiança: alta.** Em anos de operação não houve um caso de estado divergente do histórico.

### 14.3 Canal de entrada como configuração, não como código por canal

**Decisão:** o canal é um registro que carrega prazo, regras de anexo, limites e integração de bot. Canais podem ser criados sem release.

**Alternativa descartada:** uma classe por canal, com as regras embutidas.

**Motivo:** as regras que mudam (prazo, quem atende, tamanho de anexo) **não são as mesmas** que exigem código (o protocolo do canal). Separar as duas fez ajuste de SLA deixar de ser tarefa de desenvolvimento.

**Custo aceito e ressalva importante:** a separação ficou **meio feita**. Os canais conhecidos são constantes no código; os canais cadastrados recebem identificadores gerados por timestamp para não colidir com as constantes. Funciona, mas é híbrido.

**Confiança: alta no princípio, baixa na forma.** No produto novo, **escolha um lado**: todo canal é registro, com o protocolo referenciado por um identificador de implementação.

### 14.4 Um motor de fluxo para automação determinística e para agente de IA

**Decisão:** um motor só, com um bloco de IA e ferramentas ligadas por um tipo especial de conexão.

**Alternativa descartada:** um produto de automação e um produto de agente, separados.

**Motivo:** os fluxos reais são mistos. "Se o cliente perguntar sobre prazo, consulte o pedido e responda com IA; se pedir cancelamento, abra processo e não responda nada" é um fluxo único. Com dois produtos, cada transição entre eles vira integração.

**Benefício não previsto:** as ferramentas do agente **são os mesmos blocos** da automação. A capacidade construída para um lado serve ao outro sem trabalho adicional.

**Confiança: alta.** É a decisão mais bem-sucedida do conjunto.

### 14.5 Versionar o desenho e compilar em memória a cada execução

**Decisão:** o desenho é guardado como documento; a estrutura executável é reconstruída a cada corrida, com cache apenas dentro da requisição. Nada compilado é persistido.

**Alternativa descartada:** persistir um artefato compilado por versão publicada.

**Motivo:** simplicidade de deploy e de invalidação. Não existe artefato para ficar obsoleto, nem migração quando o formato de um bloco muda, nem risco de rodar um compilado velho depois de uma correção no motor.

**Custo aceito:** cada execução paga a reconstrução do grafo. Aceitável nos volumes atuais (conversas de atendimento), e a validação pesada acontece no save.

**Confiança: média.** Funciona bem para o volume atual. **Recomendação:** copiar adaptando. Se seus fluxos rodarem em volume alto — automação de chão de fábrica disparada por evento de máquina, por exemplo — meça cedo. O caminho de upgrade é claro: guardar o compilado com a assinatura do desenho e invalidar por ela. Não pague essa complexidade antes de precisar.

### 14.6 Validar o desenho ao salvar, não ao executar

**Decisão:** o mesmo compilador roda no save e recusa desenhos inválidos.

**Alternativa descartada:** validar na primeira execução.

**Motivo:** um erro de desenho descoberto em produção, numa conversa com cliente, custa muito mais caro que um erro descoberto na tela do desenhista. E há uma classe de erro que **nunca** apareceria em runtime: uma conexão de ferramenta incompatível simplesmente não faz nada. Erro silencioso é pior que erro barulhento.

**Custo aceito:** o save fica mais lento e pode recusar um desenho intermediário que o usuário queria só guardar. Mitigado por haver um rascunho separado.

**Confiança: alta.**

### 14.7 Derivar o prazo máximo de execução do desenho

**Decisão:** em vez de pedir um timeout ao usuário, calculá-lo — custo fixo por bloco, mais o tempo de cada chamada externa, mais as esperas, mais o teto para cada chamada de IA — limitado entre um piso e um teto globais.

**Alternativa descartada:** campo de timeout no formulário do fluxo.

**Motivo:** é uma decisão que o usuário não tem como tomar bem, e que fica errada assim que o fluxo cresce.

**Custo aceito:** o cálculo é heurístico e pode superestimar.

**Confiança: alta.**

### 14.8 Isolar a numeração sequencial em estrutura própria

**Decisão:** o número de negócio é composto (ano + sequência por empresa/ano + sufixo) e a sequência vive numa estrutura separada, incrementada em transação curta.

**Alternativa descartada:** usar a chave primária como número de negócio.

**Motivo:** o número é lido, ditado por telefone e usado em documento. Precisa reiniciar por ano, ser por empresa e não revelar volume total. Chave primária não faz nada disso.

**Custo aceito e que precisa estar documentado:** a sequência é confirmada antes de o processo ser gravado, então **falha na gravação consome o número**. Buracos são esperados.

**Confiança: alta no princípio.** O sufixo de plataforma é bagagem histórica — não replicar.

### 14.9 Enfileirar os efeitos colaterais em vez de executá-los no trâmite

**Decisão:** chegar numa etapa cria uma linha de execução em fila; um consumidor separado executa a ação no ERP.

**Alternativa descartada:** o trâmite chamar a ação diretamente, na mesma transação.

**Motivo:** ações externas falham, demoram e às vezes dependem de terceiros. Na mesma transação, uma emissão de nota que falha derruba o trâmite que o usuário acabou de fazer.

**Benefício adicional:** o estado "ignorado" separado de "erro" torna a fila legível.

**Custo aceito:** latência entre o trâmite e o efeito, e a necessidade de uma tela de reprocessamento.

**Confiança: alta no desenho, baixa na implementação atual** — ver armadilha 15.11.

### 14.10 Gerar a trilha de auditoria no ciclo de gravação

**Decisão:** o histórico é produzido pelo próprio ato de gravar, não pelo chamador.

**Alternativa descartada:** cada caminho de código registrar o que fez.

**Motivo:** com registro manual, os caminhos que esquecem são os automáticos — jobs, integrações, bots — que são os que mais precisam de rastro.

**Custo aceito:** é preciso deduplicar, e autor e texto precisam ser passados por campos transientes.

**Confiança: alta no princípio, baixa na forma.** Use um contexto de operação propagado na transação.

### 14.11 Prazo por mensagem, não por conversa

**Decisão:** o vencimento é calculado e gravado em cada mensagem, no momento do persist.

**Alternativa descartada:** um prazo único no atendimento, medido da abertura ao encerramento.

**Motivo:** numa conversa de oito turnos, o prazo único diz muito pouco. O que importa ao cliente é quanto tempo cada resposta demorou.

**Custo aceito:** as métricas ficam mais complexas de calcular (ver 5.11).

**Confiança: alta.**

### 14.12 Duas medidas de tempo, não uma

**Decisão:** medir separadamente o **atraso** (compromisso com o cliente) e o **custo de atendimento** (esforço do operador).

**Alternativa descartada:** um número único de "tempo de atendimento".

**Motivo:** um operador pode ter atraso zero e custo altíssimo, ou o contrário. Um número só junta qualidade de serviço com produtividade e não permite conversar sobre nenhuma das duas.

**Confiança: alta.**

---

## 15. Armadilhas e lições aprendidas

A seção que não se recupera lendo código de chegada. Cada item custou incidente real. Nove dos treze vêm de uma auditoria formal do motor de fluxo feita em jul/2026, com casos reais de produção como evidência: 19 achados, 1 crítico e 8 altos.

### 15.1 Loop de resposta automática entre sistemas

**O que acontece:** a confirmação automática de abertura chega a um endereço que também é automático. Ele responde. A resposta abre um atendimento novo. Que gera outra confirmação.

**Sintoma observável:** crescimento súbito de atendimentos numa origem de e-mail, sempre entre os mesmos dois endereços, com mensagens quase idênticas.

**Causa raiz:** responder automaticamente sem verificar se o remetente é uma máquina.

**Contramedida desde o início:** verificar cabeçalhos de automação da mensagem antes de qualquer resposta automática; manter denylist de endereços; e **limitar respostas automáticas por par (remetente, destinatário) por janela de tempo**. A denylist sozinha é remediação, não prevenção.

### 15.2 Entrega duplicada e concorrente de webhook

**O que acontece:** o canal entrega o mesmo evento duas vezes, ou duas mensagens do mesmo cliente chegam simultaneamente. Sem trava, nascem dois atendimentos para a mesma conversa.

**Sintoma:** conversas duplicadas com o mesmo cliente, criadas no mesmo segundo.

**O que se fez:** trava cooperativa por (canal, remetente, loja), tomada dentro da transação e liberada com ela, mais cache dentro do lote.

**Contramedida:** **idempotência de webhook é requisito, não otimização.** Guarde o identificador do evento no canal de origem e trate reentrega como operação nula. A trava resolve concorrência; a chave de idempotência resolve reentrega — são problemas diferentes e precisam das duas.

### 15.3 Concorrência na memória da conversa

**O que acontece:** duas mensagens do mesmo cliente processadas ao mesmo tempo leem a mesma sessão, cada uma evolui o estado e **a última a gravar apaga o trabalho da outra**.

**Sintoma:** o bot "esquece" o que acabou de perguntar; variáveis somem no meio da conversa; ocasionalmente, mensagem de recursão detectada.

**Estado atual:** conhecido e **não resolvido**. É o achado mais grave da auditoria interna.

**Contramedida desde o início:** a sessão de conversa precisa de **controle de versão otimista** ou de serialização por chave de conversa. Não é caro se for feito no começo; é caríssimo depois, porque significa reescrever o loop de execução.

### 15.4 Retomada envenenada

**O que acontece:** a execução para no meio esperando algo e guarda onde parou. Entre uma mensagem e outra, o desenho é editado, uma entidade referenciada é excluída, ou um passo guardou uma referência que não resolve mais. Na retomada, a sessão falha — **e falha de novo na mensagem seguinte, e na seguinte, até o TTL expirar**. O cliente fica sem resposta o tempo todo.

**Sintoma:** um cliente específico deixa de receber respostas do bot, enquanto os demais funcionam. Erros repetidos na mesma sessão.

**Três lições:**

1. **O que se guarda entre passos deve ser dado, não referência.** Guardar identificadores e reresolver, tolerando ausência, em vez de guardar objetos serializados.
2. **A retomada precisa validar a assinatura do desenho** — o sistema atual faz isso pelo hash, e é o que evita a pior classe do problema.
3. **Uma sessão que falha repetidas vezes deve se autodestruir.** Falhar sempre é pior que recomeçar: recomeçar dá uma resposta ruim, falhar não dá nenhuma.

### 15.5 Efeitos externos re-executados na retomada

**O que acontece:** um passo envia mensagem ao cliente ou chama uma API, e a execução falha depois disso. Na retomada, o passo roda de novo.

**Sintoma:** cliente recebe a mesma mensagem duas vezes; chamada externa duplicada.

**Contramedida:** todo passo com efeito externo precisa de **chave de idempotência** derivada de (sessão, passo, tentativa), e o registro do efeito deve ser gravado **antes** de ser considerado consumido.

### 15.6 Tratamento de erro que não cumpre o contrato configurado

**O que acontece:** o desenhista configura "em caso de erro, siga por este caminho", mas certas classes de falha — erros de linguagem em vez de exceções de aplicação — **escapam do tratamento** e sobem como falha fatal. Pior: uma chamada de IA que falhou pode ser lida como sucesso e o fluxo segue com conteúdo vazio.

**Sintoma:** o ramo de erro desenhado nunca é percorrido; ou o fluxo segue normalmente produzindo resposta em branco.

**Contramedida:** o contrato de erro configurado pelo usuário precisa valer para **toda** falha capturável, e o sucesso de uma chamada externa precisa ser **verificado explicitamente**, não inferido da ausência de exceção.

### 15.7 Perda silenciosa de ramos paralelos

**O que acontece:** o fluxo se abre em ramos que depois se juntam. A estrutura que guarda "quem já chegou" tem espaço para um só — um segundo fan-out sobrescreve o primeiro e **um ramo desaparece sem erro**.

**Sintoma:** parte do fluxo simplesmente não executa, sem nada no log.

**Contramedida:** estado de junção precisa ser **indexado por rodada**, não por identificador do bloco, e precisa ser limpo ao fim da execução. **Estado com chave de identidade que sobrevive entre execuções é a origem de uma família inteira de bugs** — é o padrão comum por trás de 15.3, 15.4 e deste.

### 15.8 Modo de teste com efeitos reais

**O que acontece:** o teste reverte a transação do banco, mas a mensagem já foi enviada ao cliente, a API já foi chamada e o modelo de IA já foi cobrado.

**Sintoma:** cliente recebe mensagem de um fluxo que "só estava sendo testado".

**Contramedida:** modo de teste precisa de **interruptor explícito de efeitos externos** em cada bloco que os produz. Rollback de transação protege o banco, não o mundo. Alguns blocos do sistema atual já verificam o modo teste — mas não todos, e essa inconsistência é pior que nenhum.

### 15.9 Cache com chave incompleta

**O que acontece:** um cache de dados de entrada calcula a chave sem incluir o identificador da conversa. Duas conversas diferentes compartilham a entrada.

**Sintoma:** dados de um cliente aparecem na conversa de outro. Raro, intermitente, e passa em todo teste.

**Contramedida:** em sistema multiempresa e multiconversa, **toda chave de cache precisa de revisão explícita quanto a tenant e a sujeito**. Faça disso item de revisão de código, não de bom senso.

### 15.10 Consulta sem verificação de titularidade

**O que acontece:** uma operação exposta ao agente permite consultar pedidos por documento informado **na conversa**. Nada garante que o documento é de quem está falando. O mesmo vale para o bloco que vincula um pedido ao atendimento a partir de um identificador informado pelo cliente.

**Sintoma:** nenhum — é exploração silenciosa.

**Contramedida:** **toda operação exposta a um agente é superfície de ataque.** O agente é dirigido por texto do usuário; tratá-lo como chamador confiável é o mesmo erro que confiar em parâmetro de URL. A verificação de titularidade tem que estar **na operação**, não no desenho do fluxo — porque o desenho muda e quem desenha não é quem pensa em segurança.

### 15.11 Fila que para inteira no primeiro erro

**O que acontece:** o consumidor de eventos, ao falhar, interrompe o processamento de todo o lote. Um evento problemático segura todos os que estão atrás dele.

**Sintoma:** eventos parados em fila sem explicação, todos posteriores a um único evento com erro.

**Contramedida:** falha em um item de fila deve **isolá-lo** — marcar, contar tentativa, seguir —, nunca parar o consumo. E o contador de tentativas deve viver em coluna, não dentro de um JSON de parâmetros.

### 15.12 Acoplamento entre canal e regra de negócio

**O que acontece:** a regra "não responder fora do horário comercial" é escrita dentro do tratamento de um canal. Seis meses depois um canal novo precisa da mesma regra e ela é copiada. Agora são duas, e elas divergem.

**Sintoma:** comportamentos diferentes entre canais para a mesma regra, sem que ninguém tenha decidido isso.

**Contramedida:** a única coisa específica de canal é **protocolo** — como ler o payload, identificar o remetente, enviar de volta. Prazo, horário, roteamento, contenção e bot são do atendimento. **Se a estratégia de um canal está crescendo, provavelmente há regra de negócio dentro dela.** O teste está na fase 6 do roteiro (17.6).

### 15.13 Nomes que enganam

O custo de um nome ruim é permanente. Quatro exemplos reais:

- uma entidade chamada **"bloqueio de mensagem do usuário"** controla se o **operador interno** pode escrever texto livre — não tem nada a ver com bloquear cliente;
- dois módulos têm constantes com o **mesmo nome e valores diferentes** para a mesma ideia (qual integração de IA usar): `1 = ES, 2 = CX, 3 = Construtor` num, `1 = CX, 2 = Construtor` no outro;
- um estado chamado **"em trâmite"** não é estado — dura entre duas gravações e existe só para produzir uma linha de histórico;
- um parâmetro rotulado **"SKU Produto"** tem a constante interna chamada `pedidoId`.

**Contramedida:** nomeie pelo que a coisa faz para o negócio, e faça **revisão de nomes antes de a primeira versão ir para produção**. Depois não se muda mais.

### 15.14 Dumps de execução que crescem sem limite

**O que acontece:** o resultado de um subfluxo embute os passos de debug do filho recursivamente. Dumps reais chegaram a dezenas de megabytes, com dados pessoais replicados em três estruturas dentro do mesmo documento.

**Sintoma:** armazenamento crescendo rápido; telas de execução lentas.

**Contramedida:** guardar **referência** à execução do filho, não o conteúdo. E definir retenção para dados de depuração que contêm dado pessoal — a retenção do log de debug costuma ser diferente da retenção do canal de atendimento, e isso é tema de conformidade.

---

## 16. O que copiar, o que adaptar, o que não replicar

### 16.1 Copiar como está

| Conceito | Seção | Por quê |
|---|---|---|
| Separação desenho × instância no workflow | 6.3 | Permite criar tipos de demanda por cadastro, não por release |
| Processo como projeção do último trâmite | 6.2 | Torna divergência entre log e estado impossível por construção |
| Classificação das etapas em quatro categorias | 6.7 | O mínimo de semântica que o motor precisa; tudo mais fica configurável |
| Combinação tipo × motivo restringindo opções por etapa | 6.7 | Validação de domínio em dado, não em código |
| Efeitos colaterais enfileirados | 6.8 | Ação externa na mesma transação derruba o trabalho do usuário |
| Estado "ignorado" separado de "erro" | 6.8 | Sem ele, evento condicional que não se aplica parece falha |
| Destinatário de notificação derivado do contexto | 6.7 | Lista estática de pessoas num workflow envelhece em semanas |
| Visibilidade herdada na árvore de setores | 7.4 | Conceder acesso num nível cobre toda a subárvore, inclusive o que ainda não existe |
| Distinção participante × responsável pelo setor | 7.2 | É o que permite endereçar a uma pessoa sem travar o trabalho quando ela falta |
| Prazo por mensagem, não por conversa | 5.5 | Permite medir resposta turno a turno |
| Ajuste de dia útil também sobre as horas | 5.5 | Sem ele, prazo de sexta à tarde vence de madrugada no sábado |
| Duas medidas de tempo separadas | 5.11 | Medem coisas opostas; juntas viram número que não significa nada |
| Trilha de auditoria gerada no ciclo de gravação | 4.2 | É o único jeito de os caminhos automáticos deixarem rastro |
| Isolamento de tenant por interface marcadora + filtro automático | 4.1 | Cada consulta nova nasce filtrada |
| Deduplicação de arquivo por hash no upload | 4.3 | Anexos repetidos são comuns |
| Permissão por operação, não por entidade | 4.4 | Permite "ver" sem "editar" sem um segundo sistema |
| Grupo de campos customizados plugável por ponto de uso | 4.6 | Evita que adicionar campo numa etapa vire migration |
| Template de notificação no banco, renderizado uma vez para N destinatários | 4.5 | Torna "por que não recebeu?" respondível |
| Um motor de fluxo para automação e IA | 14.4 | Os fluxos reais são mistos |
| Capacidades do agente ligadas por conexão no canvas | 9.7, 12.3 | A capacidade fica no mesmo lugar que o resto do fluxo |
| Validar o desenho no save, recusando o inválido | 14.6 | Conexão incompatível seria erro silencioso em runtime |
| Timeout derivado do desenho | 14.7 | Remove uma decisão que o usuário não toma bem |
| Credenciais fora do documento versionado | 8.3 | O JSON do fluxo é copiado, exportado e logado |
| Carregar só as variáveis que o template cita | 5.8, 10.7 | Custo proporcional ao uso real |
| Seleção de resposta por especificidade decrescente com curinga | 10.8 | Permite refinar sem tocar no que já existe |
| Aleatoriedade no empate entre respostas | 10.8 | Detalhe pequeno, efeito grande na percepção do cliente |
| Log de qual resposta o bot usou e por quê | 10.5 | Torna a resposta automática auditável pelo operador |
| Separar "normalizar a pergunta" de "barrar a resposta" | 10.4 | Dois problemas diferentes, um cadastro só |
| Mensagem no histórico quando o bot se recusa a responder | 10.4 | O operador entende sem abrir log |
| Capacidade de provedor por composição, não por `if` de tipo | 10.5 | Provedor novo ganha comportamento sem tocar no resto |
| Resolução de pedido em cascata | 10.6 | O cliente informa o número que ele tem, não o id interno |
| Superfície do agente como lista fechada de verbos | 10.6 | Não expõe esquema de banco ao modelo |
| Fatiamento assíncrono de documento com estado próprio | 10.9 | Subir documento grande não trava a tela |
| Tipo de solicitação por item, não pela solicitação | 5.10 | Uma devolução pode ser troca de um item e reembolso de outro |
| Fixar versão do endpoint por query string, recusando rascunho | 8.6 | Teste e migração gradual de consumidor |
| `codigo_integracao` em entidades cadastráveis | 4.7 | Integrações sempre precisam; improvisar depois é caro |
| Definição declarativa do bloco servindo canvas e compilador | 9.8 | Uma fonte, dois consumidores |

### 16.2 Copiar adaptando

| Conceito | Adaptação necessária |
|---|---|
| Canal como configuração | **Escolher um lado.** Todo canal é registro, com implementação de protocolo referenciada por identificador. Nada de constantes + registros convivendo |
| Compilar o desenho em memória a cada execução | Ótimo padrão de partida. Se o volume for alto, guarde o compilado com a assinatura do desenho e invalide por ela. **Meça antes de complicar** |
| Memória de conversa em sessão com TTL | Adicionar **controle de versão otimista desde o início** (15.3). Depois é reescrever o executor |
| Retomada por blocos pendentes | Guardar identificadores, não referências; validar assinatura do desenho; **autodestruir sessão que falha repetidamente** |
| Modo de teste | Não confiar em rollback. Interruptor explícito de efeitos externos **por bloco**, uniforme |
| Numeração composta do processo | Manter ano + sequência por empresa. Documentar que buracos são esperados. Descartar o sufixo de plataforma |
| Catálogo de respostas prontas | Excelente conceito. Mas **separe em duas tabelas** o catálogo humano e o catálogo da IA: público, ciclo de vida e critério de seleção são diferentes |
| Contenções de entrada | Copiar as três ideias, **renomear todas** (15.13) |
| Etapa inicial do fluxo | Tornar **explícita** no cadastro, em vez de adotar a primeira cadastrada |
| Prazo do processo calculado na abertura | **Recalcular** quando o processo muda de fluxo por transferência |
| Base de conhecimento | Garantir que **todos** os provedores suportados façam vetorização, ou explicitar a restrição na interface |
| Código hierárquico de centro de custo | Resolver a concorrência **no cálculo**, não na restrição de unicidade |
| Campos customizados em coluna de texto | Se precisar filtrar ou agregar por valor, use coluna por tipo ou JSON indexado. Texto puro não consulta |
| Bloco de chamada HTTP marcando sucesso em qualquer status | Oferecer opção "falhar em status ≥ 400", com default seguro |
| Nomes de porta `next` e `output` | Unificar. São a mesma coisa com nomes diferentes por acidente histórico |
| Flags de comportamento no orquestrador | Substituir por **casos de uso nomeados** |
| Duplicação do enum no histórico de solicitação | A intenção (histórico imutável) é correta; resolva **guardando o rótulo**, não duplicando constantes |
| Desligamento do filtro de tenant | Tornar escopo explícito e limitado, em vez de toggle global |

### 16.3 Não replicar

| Conceito | Por quê |
|---|---|
| Identificadores de canal gerados por timestamp | Solução para conviver com constantes fixas herdadas. Num produto novo não há faixa reservada a evitar |
| Sufixo de plataforma embutido no número do processo | Carrega dado de origem técnica que ninguém consulta e obriga caminho alternativo quando a plataforma não existe |
| Campo multiuso de protocolo com três significados | Use tabela de identificadores externos ou colunas nomeadas |
| Campos transientes na entidade para carimbar autor no histórico | Frágil e não óbvio. Use contexto de operação propagado na transação |
| Estado de negócio servindo de trava de fila | Reserva de fila é infraestrutura. Mantenha fora do enum |
| Estado transitório só para gerar linha de log | O log deve registrar a transferência sem que o registro passe por um estado falso |
| Trava de regressão de estado que **desfaz em silêncio** | Se a regra existe, deve **lançar**. Código que a viola precisa saber |
| Três integrações de IA vivas em paralelo | Custo permanente de manutenção e teste. Nasça com uma, com extensão clara |
| Arquivo público com URL não assinada | Anexo de atendimento contém documento de cliente, laudo e nota. **É o ponto de segurança mais evidente a corrigir** |
| Exceção de tenant codificada em consulta de indicador | Regra de um tenant dentro de SQL compartilhado. Isso é configuração |
| Consumidor de fila que encerra no primeiro erro | Isole o item, não pare o consumo |
| Dump de subfluxo embutido recursivamente | Guarde referência, não conteúdo |
| Traits de configuração de um módulo usados por outro | Dependência invertida entre domínios. Extraia para um lugar neutro |
| Operação de agente sem verificação de titularidade | Superfície de ataque direta. Ver 15.10 |

---

## 17. Roteiro de implementação sugerido

Fases pensadas para **entregar valor ao fim de cada uma**, e não para reproduzir a ordem histórica do sistema atual — que foi CRM primeiro e motor de processo depois, o inverso do que faria sentido hoje, especialmente para uma indústria.

### 17.1 Fase 1 — Fundamentos + Centro de custo + Motor de processo

**Entrega:** a indústria já tem workflow interno configurável. Não conformidade, aprovação de compra, manutenção, liberação de crédito, inspeção de qualidade — tudo vira cadastro.

**Escopo:**
- Fundamentos: multi-tenancy, contexto de operação para auditoria, arquivos com URL assinada, permissão por rota
- Árvore de setores com visibilidade herdada
- Fluxo, etapas, motivos, assuntos e trilha
- Processo, trâmite e as quatro colunas de projeção
- Numeração
- Anexos e campos customizados por etapa
- Recebimento e liberação com a regra de responsável

**Pré-requisito de nada.** É a base.

**Critério de pronto:** um analista configura um tipo de demanda novo **sem desenvolvedor**, e o processo caminha entre setores com histórico completo.

### 17.2 Fase 2 — Eventos de processo

**Entrega:** o workflow passa a *fazer coisas* no ERP, não só registrar.

**Escopo:** definição de evento amarrada a um ponto do desenho; fila com estados (incluindo "ignorado"); consumidor com **isolamento de falha por item** e limite de tentativas em coluna; dois ou três executores reais que importem para a indústria.

**Depende da fase 1.**

**Critério de pronto:** concluir uma etapa dispara uma ação no ERP, a ação é observável na fila, e **uma falha não trava as outras**.

### 17.3 Fase 3 — Motor de fluxo visual (automação determinística)

**Entrega:** automação configurável sem release, já útil sem nenhuma IA.

**Escopo:** fluxo, versão com rascunho, blocos de controle e de integração, conexões normal e de erro, compilação com validação no save, executor com blocos pendentes, sessão com TTL **e versionamento otimista** (15.3), timeout derivado, modo de teste com interruptor de efeitos externos.

**Independe das fases 1 e 2**, mas ganha muito com elas: um bloco que abre processo é das automações mais úteis.

**Critério de pronto:** um fluxo desenhado na tela roda em produção, pausa, retoma e é auditável passo a passo.

### 17.4 Fase 4 — Atendimento

**Entrega:** a operação de conversa com o cliente.

**Escopo:** canal como registro; ciclo de vida com os cinco estados; prazo por mensagem; trilha automática; **um único canal implementado de ponta a ponta** — recomendação: e-mail, que é o mais simples e exercita o loop da armadilha 15.1; catálogo de respostas; as contenções renomeadas; métricas.

**Depende da fase 1** apenas para a ponte com processo, que pode ficar para a fase 5.

**Critério de pronto:** um canal real em produção, com prazo medido e histórico completo.

### 17.5 Fase 5 — Ponte atendimento ↔ processo

**Entrega:** a conversa vira trabalho interno e o andamento volta para a conversa.

**Escopo:** o vínculo entre os dois; abertura de processo a partir do atendimento; retorno de cada trâmite como mensagem, **amarrada ao trâmite que a gerou**.

**Depende das fases 1 e 4.** É pequena — o valor vem de as duas pontas já existirem.

**Critério de pronto:** o cliente acompanha o andamento sem sair da conversa que abriu.

### 17.6 Fase 6 — Segundo canal, e o teste real da abstração

**Entrega:** validação de que a separação canal × regra de negócio funcionou.

**Escopo:** um canal conversacional (mensageria), que exige idempotência de webhook (15.2) e trava contra duplicidade.

**Critério de pronto — e este é o critério que importa:** implementar o segundo canal tocou **apenas** as três peças de protocolo. Se foi preciso mexer em prazo, roteamento ou contenção, **a abstração está errada e é o momento de corrigir**, não depois do quinto canal.

### 17.7 Fase 7 — Agente de IA

**Entrega:** resposta automática às perguntas mecânicas.

**Escopo:** bloco de provedor de IA, conexão de ferramenta, um conjunto pequeno de operações de consulta **com verificação de titularidade** (15.10), catálogo de respostas da IA separado do humano, termos de normalização e restrição, silenciamento por conversa, transferência para humano.

**Depende da fase 3** (o motor) **e da fase 4** (ter o que responder).

**Critério de pronto:** o agente responde uma classe de pergunta de ponta a ponta e passa para o humano quando não sabe, **sem que o humano precise desligar o bot no canal inteiro**.

### 17.8 Fase 8 — Base de conhecimento

**Entrega:** o agente responde a partir de documentos da empresa — manual de máquina, ficha técnica, procedimento.

**Escopo:** upload, fatiamento assíncrono com estado, indexação vetorial, busca por similaridade, ferramenta de busca ligada ao agente.

**Depende da fase 7.**

**Critério de pronto:** subir um manual muda a resposta do agente **sem tocar no desenho do fluxo**.

### 17.9 O que fazer em paralelo, desde a fase 1

Três coisas que não são fase e não podem esperar:

- **Contexto de operação** (quem fez, por quê, de onde) propagado na transação — a trilha de auditoria de todos os domínios depende dele;
- **Idempotência como padrão de projeto** — toda entrada externa (webhook, API, fila) com chave de evento;
- **Revisão de nomes** antes de cada domínio ir para produção (15.13).

---

## 18. Glossário

| Termo de domínio | Significado | Onde está no sistema atual |
|---|---|---|
| **Atendimento** | Unidade de trabalho correspondente a uma conversa com alguém de fora | `CRM\Model\Atendimento` |
| **Canal de entrada** (origem) | Por onde a mensagem chegou, com as regras de prazo, anexo e automação | `AtendimentoOrigemInterface` + `OrigemAtendimentoConfiguracao` |
| **Mensagem** | Um turno da conversa. Carrega o vencimento do prazo | `AtendimentoMensagem` |
| **Contenção** | Regra que impede entrada ou resposta indesejada | `SpamAtendimento`, `AtendimentoRespostaAutomaticaBloqueio`, `UsuarioBloqueioMensagem` |
| **Catálogo de respostas** (texto padrão) | Respostas prontas com variáveis, resolvidas no envio | `TextoPadrao` |
| **Processo** | Instância de uma demanda interna percorrendo um fluxo | `Unico\Model\Processo\Processo` |
| **Fluxo** | O desenho de um tipo de processo | `Unico\Model\Processo\Fluxo` |
| **Trâmite** | Um passo executado de um processo. Imutável | `ProcessoTramite` |
| **Etapa** (tipo de trâmite) | Um passo possível, classificado em uma de quatro categorias | `TipoTramite` |
| **Assunto** | Subclassificação de um fluxo | `FluxoAssunto` |
| **Motivo** | Justificativa de um trâmite, restrita pela etapa | `MotivoTramite` + `TipoTramiteMotivoTramite` |
| **Trilha organizacional** | Por quais setores a demanda passa, em ordem | `FluxoCentroCusto` |
| **Evento de processo** | Intenção de ação no ERP disparada ao chegar num ponto do desenho | `EventoProcesso` + `ProcessoTramiteEventoProcesso` |
| **Centro de custo** | Nó da árvore organizacional. Define destino e visibilidade | `CentroCusto` + `CentroCustoUsuario` |
| **Motor de fluxo** (fluxo construtor) | O motor de automação visual | `Unico\Model\FluxoConstrutor\*` |
| **Bloco** (node) | Uma operação dentro de um fluxo visual | `Node\Base\AbstractNode` |
| **Conexão** | Ligação entre blocos: normal, erro ou ferramenta | `Connection\*` |
| **Ferramenta** | Bloco que o modelo de IA pode chamar por conta própria | `AIToolNodeInterface` |
| **Sessão** | Memória de uma conversa no motor de fluxo | `FluxoConstrutorSessao` |
| **Execução** | O registro de uma corrida do fluxo | `FluxoConstrutorExecucao` |
| **Escopo** | Em que tipos de fluxo um bloco pode aparecer | `NodeInterface::USE_*` |
| **Base de conhecimento** (RAG) | Documentos fatiados e indexados para busca semântica | `FluxoConstrutorRAGArquivo` + `...Chunk` |
| **Camada Panobot** | A integração em que o agente vive fora e o ERP responde perguntas dele | `CRM\Controller\Api\Panobot\*` |
| **Operação** | Um verbo que o agente externo pode pedir ao ERP | `InterfaceOperationsPanobotIA` |
| **Termo** | Cadastro que normaliza a pergunta ou barra a resposta do bot | `AtendimentoBotTermo` |
| **Característica** | Campo customizado, agrupado e plugável por ponto de uso | `Caracteristica` + `GrupoCaracteristica` |
| **Grupo de envio** | Política de canais de notificação — **não** é grupo de pessoas | `GrupoEnvio` |
| **Tenant** | A empresa. Eixo de isolamento de dados | `Empresa` + `EmpresaBase` |
| **Canal de venda** | A loja própria ou a conta num marketplace | `Loja` |
| **Conector** | A integração de um marketplace | `Marketplace` |

---

## 19. Apêndice A — esquema de dados

Todas as tabelas dos quatro domínios, por schema. Colunas em ordem de leitura, não física.

### Schema `crm` — atendimento

| Tabela | Colunas |
|---|---|
| `crm_atendimento` | `ate_id` PK · `emp_id` FK · `ate_numero_protocolo` · `ate_data_hora` · `ate_email` · `ate_email_origem` · `ate_nome` · `ate_telefone` · `ate_numero_pedido` · `ate_situacao` · `ate_motivo_cancelamento` · `ate_modalidade` · `ate_origem` · `ate_nova_mensagem` · `usu_id_cliente` FK · `ped_id` FK · `usu_id_responsavel` FK · `ate_ultima_mensagem_id` FK · `ate_log_email_copia` json · `proder_id` FK · `mot_id` FK · `loj_id` FK · `ate_situacao_marketplace` · `ate_revisado` · `ate_data_hora_sessao` · `mkp_id` FK · `ate_id_pai` FK (self, ON DELETE SET NULL) · `ate_panobot_parametros` json · `ate_identificador_plataforma_externa` · `ate_data_plataforma_externa` json. **Índices:** situação, telefone, origem, data-hora |
| `crm_atendimento_mensagem` | `atemsg_id` PK bigint · `ate_id` FK · `atemsg_data_hora` · `atemsg_data_hora_limite` · `atemsg_data_hora_visualizada` · `atemsg_observacao` text · `not_id` FK · `usu_id` FK · `atemsg_situacao` · `atemsg_tipo` · `protra_id` FK · `atemsg_assunto` · `atemsg_respondido` · `atemsg_revisado` · `atemsg_params` json · `atemsg_manual` · `atemsg_identificador_plataforma_externa` · `atemsg_id_mensagem_pai` FK (self) |
| `crm_atendimento_mensagem_anexo` | id · `atemsg_id` FK · `doc_id` FK |
| `crm_atendimento_log` | `atelog_id` PK · `ate_id` FK · `atelog_data_hora` · `usu_id` FK · `atelog_situacao` · `atelog_observacao` text · `atelog_tipo` · `atelog_data_array` json |
| `crm_atendimento_produto` | `atepro_id` PK · `ate_id` FK · `proder_id` FK |
| `crm_atendimento_produto_assistencia` | `ateproass_id` PK · `atepro_id` FK · `proder_id` FK · `ateproass_quantidade` |
| `crm_motivo_atendimento` | `mot_id` PK · `mot_descricao` · `mot_ativo` |
| `crm_origem_atendimento_configuracao` | `ate_origem` **PK** bigint · `orconf_nome` · `orconf_configuracao` json · `mkp_id` FK |
| `crm_origem_atendimento_usuario` | `oriate_id` PK · `ate_origem` · `usu_id` FK · `cencus_id` FK. **Dois unique parciais:** (origem, usuário) WHERE usuário não nulo; (origem, centro de custo) WHERE centro não nulo |
| `crm_origem_atendimento_caixa_entrada_email` | `caixa_id` PK · `ate_origem` · `caixa_email` **unique** |
| `crm_spam_atendimento` | `spam_id` PK · `spam_email` · `spam_termo` · `spam_origem` |
| `crm_atendimento_resposta_automatica_bloqueio` | `resblq_id` PK · `emp_id` FK · `resblq_email` |
| `crm_usuario_bloqueio_mensagem` | `id_bloqueio_msg` PK · `usu_id` FK · `msg_livres_bloqueio_msg` |
| `crm_texto_padrao` | `tex_id` PK · `tex_tipo` · `tex_titulo` · `tex_texto` text · `tex_descricao` text · `tex_configuracao` json · `usu_id` FK · `cencus_id` FK |
| `crm_faq_grupo` | `faqgru_id` PK · `loj_id` FK **obrigatório** · `faqgru_nome` · `faqgru_id_pai` FK (self) |
| `crm_faq_duvida` | `faqduv_id` PK · `loj_id` FK · `faqduv_pergunta` · `faqduv_resposta` text · `faqgru_id` FK |
| `crm_solicitacao_troca_reembolso` | `str_id` PK · `str_status` · `ped_id` FK **obrigatório** · `str_current_step` · `cupom_id` FK · `str_opc_coleta` · `str_forma_reemb` |
| `crm_solicitacao_troca_reembolso_historico` | `strh_id` PK · `str_id` FK · `strh_status` · `usu_id` FK · `strh_data_hora` |
| `crm_solicitacao_troca_reembolso_pedido_item` | `strpi_id` PK · `str_id` FK · `pedit_id` FK · `strpi_tipo_sol` · `strpi_recebido` |
| `crm_solicitacao_troca_reembolso_produto_derivacao` | `strpd_id` PK · `str_id` FK · `proder_id` FK |
| `crm_atendimento_bot_fila` | id · atendimento · mensagem · situação |
| `crm_atendimento_bot_termo` | id · `termo` · `tipo` · `origem` json · `situacaoPedidoList` json · `intent` · `flu_id` FK |
| `crm_atendimento_fluxo_construtor_sessao` | id · `ate_id` FK · `fcs_id` FK |

### Schema `processo` — workflow

| Tabela | Colunas |
|---|---|
| `pro_fluxo` | `flu_id` PK · `emp_id` FK · `flu_nome` · `flu_descricao` · `flu_previsao_dia` · `flu_previsao_hora` · `flu_previsao_minuto` · `flu_ativo` · `flu_configuracao` json · `flu_codigo_integracao` · `flugru_id` FK |
| `pro_fluxo_grupo` | `flugru_id` PK · `flugru_nome` · `flu_configuracao` json |
| `pro_fluxo_assunto` | `fluass_id` PK · `flu_id` FK · `fluass_nome` · `fluass_ativo` |
| `pro_fluxo_tipo_tramite` | id · `flu_id` FK · `tiptra_id` FK · `flu_id_transferencia` FK · `flutip_fluxo_transferencia_obrigatorio` · `flutip_destino_notificacao` · `flutip_quantidade_max_tramtie` · `flutip_obrigatoriedade_documentos` · `flutip_automatizado_panobot` · `ev_id` FK · `gre_id` FK · `grupo_id` FK · `flu_configuracao` json |
| `pro_fluxo_centro_custo` | id · `flu_id` FK · `cencus_id` FK · `flucen_ordem` |
| `pro_processo` | `pro_id` PK · `emp_id` FK · `pro_numero` · `pro_prioridade` · `pro_data_hora_abertura` · `pro_data_hora_prevista` · `pro_descricao_abertura` text · `pro_origem` · `pro_numero_protocolo` · `flu_id_atual` FK · `tiptra_id_atual` FK · `cencus_id_atual` FK · `protra_id_atual` FK · `pes_id_abertura` FK · `pes_id_solicitante` FK · `ped_id` FK · `nf_id` FK · `oc_id` FK · `tra_id` FK |
| `pro_processo_tramite` | `protra_id` PK · `pro_id` FK · `tiptra_id` FK · `cencus_id` FK · `flu_id` FK · `protra_data_hora` · `protra_mensagem` text · `fluass_id` FK · `pes_id_destino` FK · `pes_id_operacao` FK **obrigatório** · `protra_tempo_gasto_minuto` · `protra_nova_mensagem` · `protra_assunto_notificacao` text · `mot_id` FK · `protra_panobot_execucoes` · `protra_panobot_data_hora_envio` · `protra_panobot_situacao` · `tralog_id` FK |
| `pro_processo_tramite_caracteristica` | **PK composta** (`protra_id`, `car_id`) · `procar_valor` text |
| `pro_processo_tramite_documento_eletronico` | id · `protra_id` FK · `doc_id` FK · `nf_id` FK · `prodoc_tipo` |
| `pro_processo_tramite_notificacao` | id · `not_id` FK · `protra_id` FK |
| `pro_sequencia_processo` | `seqpro_id` PK · `emp_id` FK · `seqpro_sequencia` · `seqpro_ano`. **UNIQUE (emp_id, seqpro_ano)** |
| `pro_tipo_tramite` | `tiptra_id` PK · `emp_id` FK · `tiptra_tipo` · `tiptra_nome` · `tiptra_descricao` · `tiptra_codigo_integracao` |
| `pro_motivo_tramite` | `mot_id` PK · `mot_descricao` |
| `pro_tipo_motivo_tramite` | `tipmot_id` PK · `mot_id` FK · `tiptra_id` FK |
| `pro_evento_processo` | `evpro_id` PK · `evpro_codigo` · `evpro_nome` · `evpro_tipo` · `flu_id` FK · `fluass_id` FK · `tiptra_id` FK · `cencus_id` FK · `usu_id` FK · `evpro_id_pre_requisito` FK (self) · `evpro_parametro` json · `evpro_ordem` · `evepro_ativo` · `evepro_template` text |
| `pro_processo_tramite_evento_processo` | `event_id` PK · `protra_id` FK · `evpro_id` FK · `nf_id` FK · `mov_id` FK · `event_data_hora_situacao` · `event_situacao` · `event_log` text · `event_params` json |
| `pro_processo_atendimento_origem` | `proate_id` PK · `pro_id` FK · `ate_id` FK |
| `pro_processo_fluxo_construtor_sessao` | id · processo · sessão |

### Schema padrão — centro de custo, motor de fluxo, notificação

| Tabela | Colunas |
|---|---|
| `un_centro_custo` | `cencus_id` PK · `emp_id` FK · `cencus_codigo` · `cencus_nome` · `cencus_ativo` · `cencus_id_pai` FK (self). **UNIQUE (emp_id, cencus_codigo)** |
| `un_centro_custo_usuario` | `cenusu_id` PK · `cencus_id` FK · `usu_id` FK · `cenusu_responsavel` |
| `un_fluxo_construtor` | `fc_id` PK · `fc_tipo` · `fc_codigo` · `fc_nome` · `fc_descricao` text · `fc_debug` · `fc_ttl_sesao` · `fc_configuracao` json. **UNIQUE (fc_tipo, fc_codigo)** |
| `un_fluxo_construtor_versao` | `fcv_id` PK · `fc_id` FK · `fcv_codigo_versao` · `fcv_configuracao` json · `fcv_descricao_versao` text · `fcv_data_criacao` · `usu_id` FK · `fcv_hash`. **UNIQUE (fc_id, fcv_codigo_versao)** |
| `un_fluxo_construtor_parametro` | `fcp_id` PK · `fcp_codigo` · `fc_id` FK nullable · `fcp_tipo` · `fcp_configuracao` json |
| `un_fluxo_construtor_sessao` | `fcs_id` PK · `fcs_session_id` **unique** · `fcv_id` FK · `fcs_variables` json · `fcs_metadata` json · `fcs_pending_nodes` json · `fcs_updated_at` · `fcs_tipo` · `fcs_fcv_hash` |
| `un_fluxo_construtor_execucao` | `fce_id` PK · `fcs_id` FK · `doc_id` FK · `fcs_updated_at` |
| `un_fluxo_construtor_rag_arquivo` | `fcraq_id` PK · `fcraq_nome` · `fcraq_descricao` text · `doc_id` FK · `fcraq_status` · `fcraq_total_chunks` · `fcraq_modelo_embedding` · `fcraq_dim_embedding` · `fcraq_chunk_size` · `fcraq_overlap` · `fcraq_provider_id` FK · `fcraq_credencial_id` FK · `fcraq_erro_msg` text · `fcraq_criado_em` · `fcraq_atualizado_em` |
| `un_fluxo_construtor_rag_arquivo_chunk` | `fcraqc_id` PK · `fcraq_id` FK (CASCADE) · `fcraqc_numero` · `fcraqc_conteudo` text · `fcraqc_embedding` **vetor(1536)** |
| `un_tipo_notificacao` | id · nome. ~55 registros fixos |
| `un_tipo_notificacao_evento` | `ev_id` PK · `emp_id` FK · `tip_id` FK · `loj_id` FK · `ev_nome` · `ev_codigo` · `ev_template_tipo` · `ev_template` text · `ev_template_titulo` · `ev_reply_to_padrao` · `ev_ativo` · `ev_config` json · `ev_tipo` · `ev_prioridade` · `ev_agendar` · `ev_dados_agendamento` json |
| `un_tipo_notificacao_evento_destinatario` | `dest_id` PK · `ev_id` FK · `dest_destinatario` |
| `un_notificacao` | `not_id` PK · `emp_id` FK · `tip_id` FK · `loj_id` FK · `not_tipo` · `not_situacao` · `not_destinatario` · `not_titulo` · `not_mensagem` · `not_origem` · `not_reply_to` · `not_params` json · `not_data_hora_envio` · `not_erro` |
| `un_notificacao_admin` | id · situação · prioridade · `notadm_conf` json · `notadm_opcoes` json · agendamento |
| `un_grupo_envio` | `gre_id` PK · `gre_nome` |
| `un_grupo_envio_tipo_notificacao_evento` | `gne_id` PK · `gre_id` FK · `ev_id` FK. **UNIQUE nos dois** |
| `un_loja` | `loj_id` PK · nome · logo · ícone · url · ativo · `mkp_id` FK. **Sem `emp_id`** |
| `un_marketplace` | `mkp_id` PK · nome · código |
| `un_loja_empresa` | **PK composta** (`loj_id`, `emp_id`) |
| `un_usuario_perfil` | `usu_id` FK · `perfil_id` FK |
| `un_perfil` | `perfil_id` PK · `emp_id` FK · nome · tipo |
| `un_perfil_router` | `perfil_id` FK · `router_name` FK |
| `un_midia` | `midia_id` PK · `midia_file` binário · `midia_raw_s3_path` · mime type · md5 |

### Outros schemas

| Tabela | Conteúdo |
|---|---|
| `documento.dc_documento_eletronico` | `doc_id` PK · `emp_id` FK · `tip_id` FK · `doc_documento` text · `midia_id` FK · `doc_identificador` · `log_id` FK · `doc_data_hora` · `doc_tipo` · `doc_file_md5` (indexado) · `doc_bucket` |
| `produto.pr_grupo_caracteristica` | `grupo_id` PK · nome · tipo |
| `produto.pr_caracteristica` | `car_id` PK · `grupo_id` FK · nome · tipo de valor · obrigatoriedade · ordem · ajuda · default · `midia_id` FK · `car_lista_opcoes` json · `car_config` json · flags de filtro e SEO |
| `admin.ad_empresa` | `emp_id` PK · dados da empresa · FK para empresa pai (self) |
| `faderim.fd_router` | `router_name` PK · `router_title` · `router_controller` · `router_path` unique · FK para página |

---

## 20. Apêndice B — enumerações

Todas as constantes dos quatro domínios, num lugar só.

### Atendimento

**`Atendimento::SITUACAO_*`** — `1` ABERTO · `2` EM_ANDAMENTO · `3` FINALIZADO · `4` IGNORADO · `5` TRAMITADO · `6` LOCK · `7` PENDENTE_VERIFICACAO

**`Atendimento::MOTIVO_CANCELAMENTO_*`** — `1` DUPLICADO · `2` INVALIDO · `3` EMAIL_SPAM · `4` AGRADECIMENTO_CLIENTE

**`Atendimento::MODALIDADE_*`** — `1` DUVIDA_CLIENTE · `2` PEDIDO

**`Atendimento::SITUACAO_MARKETPLACE_*`** — `1` DEVOLUCAO_ENVIADA · `2` AGUARDANDO_DEVOLUCAO · `3` DEVOLUCAO_RECEBIDA · `4` DEVOLUCAO_FINALIZADA

**`AtendimentoOrigemInterface::ORIGEM_*`** — `1` SITE · `2` EMAIL · `3` MERCADO_LIVRE_PRODUTO · `4` MERCADO_LIVRE_PEDIDO · `5` SITE_ASSISTENCIA · `6` MERCADO_LIVRE_RECLAMACAO · `7` EMAIL_LOGISTICA · `8` EMAIL_LOGISTICA_TRANSPORTADORA · `9` CHARGEBACK · `10` MAGAZINE_LUIZA_PEDIDO · `11` COMENTARIO · `12` COLOMBO_PEDIDO · `13` SKYHUB_PEDIDO · `14` SKYHUB_RECLAMACAO · `15` EMAIL_TROCA · `16` EMAIL_RASTREIO · `17` LEROY_MERLIN_PEDIDO · `18` KABUM_PEDIDO · `19` KABUM_OFERTA · `20` DEVOLUCAO_MARKETPLACE · `21` VIA_VAREJO_PEDIDO · `22` WHATSAPP · `23` JURIDICO · `24` MAGAZINE_LUIZA_PRODUTO · `25` META_MESSENGER · `26` META_INSTAGRAM · `28` SHOPEE_PERGUNTA · `29` SHOPEE_PEDIDO · `30` SHOPEE_PRODUTO · `31` MAGAZINE_LUIZA_CONVERSA · `32` TIKTOK_SHOP_PEDIDO · `33` TIKTOK_SHOP_PRODUTO · `34` TIKTOK_SHOP_GERAL · `35` AMAZON_MANUAL_PEDIDO · `36` MADEIRA_MADEIRA_PEDIDO · `37` MERCADO_LIVRE_RECLAMACAO_MEDIACAO · `38` POS_VENDA_PEDIDOS · `40` META_COMENTARIO_INSTAGRAM · `41` META_COMENTARIO_FACEBOOK *(27 e 39 vagos)*

**`AtendimentoMensagem::SITUACAO_*`** — `1` NOVA · `2` LIDA · `3` REMOVIDA
**`AtendimentoMensagem::TIPO_*`** — `1` PADRAO · `2` EVENTO

**`AtendimentoLog::TIPO_*`** — `1` PADRAO · `2` FORMULARIO · `3` ALTERACAO_PEDIDO

**`OrigemAtendimentoConfiguracaoModel`** — `TIPO_EXTENSAO_PERMITIDA = 1` · `TIPO_EXTENSAO_NAO_PERMITIDA = 2` · `PANOBOT_IA_TIPO_INTEGRACAO_DIALOGFLOW_ES = 1` · `..._DIALOGFLOW_CX = 2` · `..._CONSTRUTOR = 3`

**`TextoPadrao::TIPO_*`** — `1` GLOBAL · `2` USUARIO · `3` CENTRO_CUSTO · `4` PANOBOT_IA_ATENDIMENTO · `5` PANOBOT_IA_PROCESSO_TRAMITE

**`TextoPadraoConfiguracao`** — `TIPO_RESPOSTA_MENSAGEM = 1` · `TIPO_RESPOSTA_PARAMETROS = 2` · `TIPO_RENDER_RESPOSTA_PARAMETRO_TEXTO = 1` · `TIPO_RENDER_RESPOSTA_PARAMETRO_JSON = 2`

**`MensagemPadraoWhatsApp::TIPO_*`** — `1` TEXTO_PADRAO · `2` TEMPLATE

**`SolicitacaoTrocaReembolso::STATUS_*`** — `1` INICIADA · `2` EM_ANALISE · `3` APROVADA · `4` EM_ANDAMENTO · `5` ITENS_RECEBIDOS · `6` ITENS_RECEBIDOS_PARCIALMENTE · `7` NEGADA · `8` FINALIZADA
**`...::OPCAO_COLETA_*`** — `1` POSTAGEM_CORREIOS · `2` TRANSPORTADORA
**`...::REEMBOLSO_*`** — `1` PIX · `2` TRANSFERENCIA_BANCARIA
**`SolicitacaoTrocaReembolsoPedidoItem::TIPO_SOLICITACAO_*`** — `1` TROCA · `2` REEMBOLSO · `3` REEMBOLSO_POR_VOUCHER · `4` ASSISTENCIA

**`AtendimentoBotTermo` (tipos)** — `1` REMOCAO_TERMO · `2` RESTRICAO_INTENT · `3` DIVISAO_ESQUERDA · `4` DIVISAO_DIREITA · `5` RESTRICAO_INTENT_PROCESSO · `6` RESTRICAO_INTENT_SITUACAO_PEDIDO

**`InterfaceFilaBotSituacao::SITUACAO_*`** — `1` NOVO · `2` RESPONDIDO · `3` SEM_RESPOSTA · `4` ERRO · `5` RESTRICAO · `6` PROCESSANDO

**`InterfaceOperationsPanobotIA::OPERATION_*`** — `mensagem` · `parametros` · `consultar-frete` · `caracteristica-produto` · `list-pedidos` · `list-pedido-itens` · `list-processos` · `lock-bot-response` · `unlock-bot-response` · `detail-produto` · `list-ordens-compra` · `list-pedido-transporte` · `list-notas-fiscais` · `list-manual-produto` · `list-produtos-relacionados` · `list-recibos-entrega`

### Processo

**`Processo::PRIORIDADE_*`** — `1` BAIXA · `2` NORMAL · `3` ALTA · `4` URGENTE
**`Processo::ORIGEM_*`** — `1` MANUAL · `2` AUTOMATICO

**`TipoTramite::TIPO_*`** — `1` ABERTURA · `2` PROCESSANDO · `3` CONCLUIDO · `4` CANCELAMENTO

**`FluxoTipoTramite::DESTINO_NOTIFICACAO_*`** — `0` SEM_NECESSIDADE · `1` CLIENTE · `2` TRANSPORTADORA · `3` FORNECEDOR · `4` ORIGEM_PROCESSO
**`FluxoTipoTramite::TIPO_*`** — `1` NOTIFICACAO · `2` GRUPO_ENVIO

**`FluxoGrupo`** — `PANOBOT_IA_TIPO_INTEGRACAO_DIALOGFLOW_CX = 1` · `..._CONSTRUTOR = 2` *(⚠ numeração diferente do CRM)*

**`EventoProcesso::CODIGO_*`** — `1` DIVERGENCIA_ORDEM_COMPRA · `2` DIVERGENCIA_CTE · `3` BOLETO_DIVERGENTE · `4` PEDIDOS_EM_ANALISE · `5` RENOVACAO_CERTIFICADO · `6` EMISSAO_NFE · `7` ORDEM_COMPRA_ATRASADA · `8` ALTERACAO_PRAZO_ENTREGA · `9` ALTERAR_SITUACAO_PEDIDO · `10` MOVIMENTACAO_FINANCEIRA · `11` PEDIDO_MARKETPLACE · `12` GESTAO_PAGAMENTO · `13` PEDIDO_ERRO_INTEGRACAO · `14` CANCELAMENTO_OC
**`EventoProcesso::TIPO_*`** — `1` NOVO_PROCESSO · `2` VALIDACAO_TRAMITE

**`ProcessoTramiteEventoProcesso::SITUACAO_*`** — `1` FILA · `2` AGUARDANDO_TERCEIRO · `3` FINALIZADO · `4` ERRO · `5` IGNORADO

**`ProcessoTramiteDocumentoEletronico::TIPO_*`** — `1` ANEXO · `2` NFE_DANFE · `3` NFE_XML · `4` NFE_DANFE_E_XML

### Motor de fluxo

**`NodeInterface::USE_*`** — `GLOBAL` · `CRM_SAC` · `UNICO_PROCESSO` · `USE_API`
**`NodeInterface::TYPE_EXCEPTION_*`** — `PATH` · `IGNORE` · `STOP` *(default)*

**`FluxoConstrutor::USE_*`** — `1` CRM_SAC · `2` UNICO_PROCESSO · `3` API
**`FluxoConstrutor`** — `DEFAULT_TTL_SESSAO = 30` min · `MIN_EXECUTION_TIMEOUT_SECONDS = 600` · `MAX_EXECUTION_TIMEOUT_SECONDS = 900` · `EXECUTION_TIMEOUT_POR_NODE_SECONDS = 10`

**`FluxoConstrutorVersao::CODIGO_DRAFT`** — `'draft'`

**`FluxoConstrutorParametro::TIPO_*`** — `1` CREDENCIAIS_HTTP_REQUEST · `2` PROVIDER_IA · `3` CREDENCIAIS_PROVIDER_IA · `4` GLOBAL_VARIABLE · `5` PROVIDER_IA_MODEL *(não persiste)* · `6` GLOBAL_FUNCTION · `7` PROVIDER_IA_MODEL_EMBEDDING *(não persiste)*
**`...::ESCOPO_*`** — `por_fluxo` · `compartilhada`

**`FluxoConstrutorSessao::TIPO_*`** — `1` PRODUCAO · `2` TESTE · `3` ERRO

**`FluxoConstrutorRAGArquivo::STATUS_*`** — `1` AGUARDANDO · `2` PROCESSANDO · `3` PRONTO · `4` ERRO

**Tipos de conexão** — `default` · `exception` · `tool`
**Nomes de porta canônicos** — `next` · `true` · `false` · `default` · `output`
**Tipos de campo de formulário** — `uiText` · `uiMemo` · `uiNumber` · `uiCheckbox` · `uiTwigEditor` · `uiList` · `uiMultiList` · `uiSelectDynamicLoad` · `uiSuggest` · `uiSuggestMultiple` · `uiGridForm` · `uiCallAction`

**Limites de bloco** — bloco inicial: 1 por fluxo · bloco de erro global: 1 por fluxo · bloco de espera: 10 por fluxo, 0–60 segundos cada · junção: 2–10 entradas · chamada HTTP: timeout 1–240s, default 30 · bloco de IA: máximo 2 ferramentas conectadas, 10 iterações de chamada de ferramenta

### Transversais

**`Notificacao::TIPO_*`** — `1` EMAIL · `2` SMS *(não implementado)* · `3` MARKETPLACE · `4` PUSH · `6` WHATSAPP · `7` PUSH_APP · `8` META_MESSENGER *(não implementado)* *(5 vago)*
**`Notificacao::SITUACAO_*`** — `1` FILA · `2` ENVIADO · `3` ERRO
**`TipoNotificacaoEvento`** — `NOTIFICACAO_EXTERNA = 1` · `NOTIFICACAO_INTERNA = 2` · `CODIGO_PADRAO = 1` · `PERIODO_DIARIO = 1` · `PERIODO_MENSAL = 2`
**`NotificacaoAdmin::SITUACAO_*`** — `1` PENDENTE · `2` TODOS_LIDOS · `3` CANCELADA · `4` AGENDADA
**`NotificacaoAdmin::PRIORIDADE_*`** — `1` URGENTE · `2` ALERTA · `3` NORMAL

**`DocumentoEletronico::TIPO_*`** — `1` TEXTO · `2` ARQUIVO · `3` REPOSITORIO · `4` URL

**`GrupoCaracteristica::TIPO_*`** — `1` PRODUTO · `2` PROCESSO · `3` PERSONALIZACAO · `4` COMPOSICAO_PRODUTO · `5` AVALIACAO_PRODUTO
**`Caracteristica::TIPO_*`** — `1` LISTA · `2` LISTA_MULTIPLA · `3` TEXTO · `4` BOOLEANO · `5` NUMERO · `6` GRID_MULTIPLO · `7` ARQUIVO

**`Usuario::TIPO_*`** — `1` MASTER · `2` ADMINISTRADOR · `3` SITE · `4` WEB_SERVICE
**`Perfil::TIPO_*`** — `1` NORMAL · `2` PERFIL_FORNECEDOR · `3` PERFIL_TRANSPORTADORA · `4` PERFIL_TECNICO

---

*Fim do documento. Dúvidas sobre qualquer seção devem ser dirigidas ao time que mantém o sistema atual — a leitura do código é possível, mas as seções 14 e 15 não estão escritas em lugar nenhum além daqui.*
