# Processos e Centro de Custo

> Contexto consolidado em set/2026, a partir da atividade #479827 (documentar o CRM do admin).
> Serve como referência de entendimento do motor de processos para futuras implementações.

> Mecânicas relacionadas — **não duplicar aqui**:
> - Atendimento do CRM (a outra ponta da ponte) → [../CRM/atendimento-crm.md](../CRM/atendimento-crm.md)
> - Automação e IA do trâmite → [../FluxoConstrutor/ia-atendimento-e-motor-fluxo.md](../FluxoConstrutor/ia-atendimento-e-motor-fluxo.md)

---

## O que é

O módulo de **Processos** é um motor genérico de workflow. Ele não sabe o que é nota fiscal, pedido ou atendimento: sabe que alguém abre uma demanda, ela passa por etapas, cada etapa tem um responsável e um prazo, e em algum momento ela termina.

Apesar de morar no módulo `Unico` e ser usado por meia dúzia de outros módulos, é a peça que o CRM usa quando um atendimento precisa virar trabalho interno.

A modelagem separa deliberadamente duas coisas:

- o **fluxo** — o *desenho*. Cadastrado uma vez, diz quais etapas existem, em que ordem, quem responde por cada uma e quanto tempo cada demanda deveria levar;
- o **processo** — a *instância*. Uma demanda concreta percorrendo aquele desenho, com número, solicitante e histórico.

E existe uma terceira peça que é fácil de subestimar: o **trâmite**. Cada passo do processo é uma linha imutável em `pro_processo_tramite`. O processo em si não guarda "seu estado" — ele **espelha o último trâmite**.

> Este é o conceito central do módulo. `pro_processo` tem `flu_id_atual`, `tiptra_id_atual`, `cencus_id_atual` e `protra_id_atual`: quatro colunas que são pura denormalização da última linha de trâmite, reescritas a cada passo por `ProcessoController::persisteProcessoTramite()`. O trâmite é o log de eventos; o processo é a projeção do último evento. Perder essa distinção é o erro mais comum ao mexer aqui.

Pontos de partida do código:

- `src/Unico/Controller/ProcessoController.php` — o motor (785 linhas): abre, tramita, transfere, recebe, notifica
- `src/Unico/Model/Processo/` — as 18 entidades do schema `processo.*`
- `src/Unico/Repository/Processo/` — as consultas, a numeração e a fila de eventos
- `src/Unico/Model/CentroCusto.php` — a dimensão organizacional, que fica fora do schema `processo.*`

> **Atenção de navegação:** não existe `src/Unico/Controller/Processo/`. Os controllers do motor estão na **raiz** de `src/Unico/Controller/`. Os subdiretórios que existem são `Form/Processo/`, `Grid/Processo/` e `Job/Processo/`.

---

## O desenho: fluxo

### `Fluxo`

**Entidade:** `Unico\Model\Processo\Fluxo` → `processo.pro_fluxo`

| Propriedade | Coluna | Observação |
|---|---|---|
| `id` | `flu_id` | PK |
| `nome` / `descricao` | `flu_nome` / `flu_descricao` | |
| `previsaoDia` / `previsaoHora` / `previsaoMinuto` | `flu_previsao_dia` / `_hora` / `_minuto` | O SLA do fluxo. Ver abaixo |
| `ativo` | `flu_ativo` | |
| `configuracao` | `flu_configuracao` | `json_array` |
| `codigoIntegracao` | `flu_codigo_integracao` | Chave para amarrar o fluxo a um sistema externo |
| `fluxoGrupo` | `flugru_id` | `@ManyToOne` opcional |

O prazo do processo é calculado uma única vez, na abertura, por `ProcessoController::defineDataPrevisao()`: os **dias são somados como dias úteis** (`addDiaUtil`), as horas e minutos são somados como intervalo corrido. O resultado vai para `pro_data_hora_prevista` e **não é recalculado** quando o processo muda de fluxo por transferência.

### `FluxoGrupo`

**Entidade:** → `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 e são **diferentes das do CRM**, apesar do nome parecido:

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

> Cuidado: no CRM, `OrigemAtendimentoConfiguracaoModel` usa `1 = Dialogflow ES`, `2 = Dialogflow CX`, `3 = Construtor`. Aqui `1 = Dialogflow CX` e `2 = Construtor`. **Os números não são intercambiáveis entre os dois módulos.**

O JSON é lido por `FluxoGrupoConfiguracaoModel` (value object, sem tabela), que reusa os traits `TraitConfiguracaoDialogflowCXModel` e `TraitConfiguracaoFluxoConstrutorModel` — ambos definidos em `src/CRM/Model/`. Único consumidor: `FluxoGrupo::getConfiguracaoModel()`.

### `FluxoAssunto`

**Entidade:** → `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

**Entidade:** → `processo.pro_fluxo_tipo_tramite`

É a tabela que responde "neste fluxo, quais etapas são possíveis e o que cada uma faz".

| Propriedade | Coluna | Observação |
|---|---|---|
| `fluxo` | `flu_id` | |
| `tipoTramite` | `tiptra_id` | A etapa |
| `fluxoTransferencia` | `flu_id_transferencia` | Se preenchido, concluir esta etapa **joga o processo em outro fluxo**. Ver "transferência" |
| `fluxoTransferenciaObrigatorio` | `flutip_fluxo_transferencia_obrigatorio` | |
| `destinoNotificacao` | `flutip_destino_notificacao` | `0` sem necessidade, `1` cliente, `2` transportadora, `3` fornecedor, `4` origem do processo |
| `quantidadeMaxTramite` | `flutip_quantidade_max_tramtie` | Teto de repetições da etapa (o typo `tramtie` está no fonte) |
| `obrigatoriedadeDocumentos` | `flutip_obrigatoriedade_documentos` | Exige anexo para avançar |
| `automatizadoPanobot` | `flutip_automatizado_panobot` | A etapa é executada pela IA, não por humano |
| `eventoProcesso` | `ev_id` | Evento disparado ao entrar na etapa |
| `grupo` / `grupoEnvio` | `grupo_id` / `gre_id` | Destinatários da notificação |
| `configuracao` | `flu_configuracao` | `json_array` |

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

### `FluxoCentroCusto`

**Entidade:** → `processo.pro_fluxo_centro_custo` (`flu_id`, `cencus_id`, `flucen_ordem`)

A **trilha organizacional** do fluxo: por quais centros de custo a demanda passa e em que ordem. O primeiro da lista (`->first()`) é quem recebe o processo na abertura e na transferência.

---

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

### `Processo`

**Entidade:** `Unico\Model\Processo\Processo` → `processo.pro_processo`
**Repositório:** `Unico\Repository\Processo\ProcessoRepository`

| Propriedade | Coluna | Observação |
|---|---|---|
| `id` | `pro_id` | PK |
| `numero` | `pro_numero` | O identificador de negócio. Ver "numeração" |
| `prioridade` | `pro_prioridade` | `1` baixa, `2` normal, `3` alta, `4` urgente |
| `dataHoraAbertura` | `pro_data_hora_abertura` | |
| `dataHoraPrevista` | `pro_data_hora_prevista` | Calculada na abertura a partir do fluxo |
| `descricaoAbertura` | `pro_descricao_abertura` | |
| `origem` | `pro_origem` | `1 = ORIGEM_MANUAL`, `2 = ORIGEM_AUTOMATICO` |
| `numeroProtocolo` | `pro_numero_protocolo` | Protocolo externo |
| **`fluxoAtual`** | `flu_id_atual` | **Espelho do último trâmite** |
| **`tipoTramite`** | `tiptra_id_atual` | **Espelho do último trâmite** |
| **`centroCusto`** | `cencus_id_atual` | **Espelho do último trâmite** |
| **`processoTramiteAtual`** | `protra_id_atual` | **Espelho do último trâmite** |
| `pessoaAbertura` / `pessoaSolicitante` | `pes_id_abertura` / `pes_id_solicitante` | Quem registrou × quem pediu. Ambos obrigatórios |
| `pedido` | `ped_id` | Vínculos opcionais com o domínio que originou a demanda |
| `notaFiscal` | `nf_id` | |
| `ordemCompra` | `oc_id` | |
| `transportadora` | `tra_id` | |

`insereProcesso()` valida, antes de gravar, que existem fluxo, pessoa de abertura, solicitante, centro de custo e prioridade — e, se o tipo de trâmite não foi informado, adota o **primeiro** `FluxoTipoTramite` do fluxo. É uma dependência de ordem de cadastro que não está sinalizada em tela.

### `ProcessoTramite`

**Entidade:** → `processo.pro_processo_tramite`

| Propriedade | Coluna | Observação |
|---|---|---|
| `processo` | `pro_id` | |
| `tipoTramite` | `tiptra_id` | A etapa executada |
| `fluxo` | `flu_id` | Guardado na linha: o fluxo pode mudar ao longo do processo |
| `centroCusto` | `cencus_id` | Para onde a demanda foi |
| `fluxoAssunto` | `fluass_id` | |
| `dataHora` | `protra_data_hora` | |
| `mensagem` | `protra_mensagem` | O texto do trâmite |
| `pessoaOperacao` | `pes_id_operacao` | Quem executou. **Obrigatório** |
| `pessoaDestino` | `pes_id_destino` | Para quem foi. Nulo = endereçado ao centro de custo, não a uma pessoa |
| `motivoTramite` | `mot_id` | |
| `tempoGastoMinuto` | `protra_tempo_gasto_minuto` | Apontamento de esforço |
| `novaMensagem` | `protra_nova_mensagem` | Flag de não lido |
| `assuntoNotificacao` | `protra_assunto_notificacao` | |
| `panobotExecucoes` / `panobotDataHoraEnvio` / `panobotSituacao` | `protra_panobot_*` | Controle da automação por IA da etapa |
| `pedidoTransporteLog` | `tralog_id` | Quando o trâmite nasceu de um evento de rastreio |

`pessoaDestino` nulo é significativo: significa "qualquer um do centro de custo pode pegar". Com pessoa preenchida, só ela (ou um responsável do centro de custo) recebe.

Satélites do trâmite:

- `ProcessoTramiteCaracteristica` → `processo.pro_processo_tramite_caracteristica` — campos customizados por trâmite: `car_id` aponta para a característica cadastrada e `procar_valor` guarda o texto. É o mecanismo de extensão de formulário sem migration.
- `ProcessoTramiteDocumentoEletronico` → `processo.pro_processo_tramite_documento_eletronico` — anexos, com `prodoc_tipo`: `1` anexo comum, `2` DANFE, `3` XML da NF-e, `4` DANFE e XML.
- `ProcessoTramiteNotificacao` → `processo.pro_processo_tramite_notificacao` — quais notificações foram disparadas por aquele trâmite.

### 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` do processo** |
| `receberProcesso()` | Alguém do centro de custo assume a demanda. Valida privilégio antes |
| `liberar()` | Devolve o trâmite ao centro de custo |
| `doRealizaTramiteTransferencia()` | Move o processo para outro fluxo |
| `notificarEventoTramite()` | Dispara `ProcessoNotificacaoController` |
| `notificarOrigemProcesso()` | Responde no atendimento que originou o processo |
| `newInstanceByEventoProcesso()` | Abre um processo a partir de um evento, sem tela |
| `verificaProcessoExistenteAberto()` | Guarda de duplicidade, estática |

Flags de comportamento que o chamador liga antes de `insereProcesso()`, 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 de processo casam

### 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)`, então uma transferência nunca dispara outra em cadeia.

---

## Numeração do processo

O número **não** é o `pro_id`. É uma string de 22 caracteres montada por `ProcessoRepository::nextNumeroProcesso()`:

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

- **`AAAA`** — o ano corrente
- **`sequencia`** — contador por empresa e por ano, vindo de `pro_sequencia_processo`
- **`unique`** — `BaseController::getClientPlatform(false)`, isto é, a plataforma que abriu o processo. No portal da transportadora não existe plataforma, e o valor vira `crc32(md5(cnpj_da_empresa . timestamp))`

A sequência é isolada em tabela própria justamente para poder ser incrementada em transação curta, sem travar a criação do processo:

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

```sql
UNIQUE INDEX UNIQ_8AF9B4037A663008C5121B76 ON processo.pro_sequencia_processo (emp_id, seqpro_ano)
```

`SequenciaProcessoRepository::nextSequenciaProcesso()` abre transação, faz `SELECT ... ORDER BY seqpro_sequencia DESC LIMIT 1 FOR UPDATE`, e então incrementa ou insere.

Duas consequências que valem registro:

- **Buracos na numeração são esperados.** O `commit()` da sequência acontece antes do processo ser persistido; se a gravação do processo falhar, o número já foi consumido.
- **Na primeira abertura do ano o `FOR UPDATE` não trava nada** — não existe linha para bloquear. Duas aberturas simultâneas no dia 1º de janeiro tentam o mesmo `INSERT`; quem perde recebe violação do unique index, não uma sequência duplicada. O erro é o comportamento correto, mas é um erro, não um retry.

---

## Tipos de trâmite e motivos

### `TipoTramite`

**Entidade:** → `processo.pro_tipo_tramite`

| Propriedade | Coluna |
|---|---|
| `tipo` | `tiptra_tipo` |
| `nome` / `descricao` | `tiptra_nome` / `tiptra_descricao` |
| `codigoIntegracao` | `tiptra_codigo_integracao` |

O tipo de trâmite é **cadastrado pelo usuário**, 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. É por ela que o código responde "este processo ainda está vivo?" — por exemplo, `TramitaProcessoNotaEntregueController::validacoesTramitaProcesso()` recusa tramitar quando o tipo atual já é `TIPO_CONCLUIDO` ou `TIPO_CANCELAMENTO`.

**O conceito a reter:** os nomes das etapas são configuráveis e específicos de cada empresa; a máquina de estados que o motor conhece tem só quatro posições. É o que permite cadastrar workflows arbitrários sem que o motor precise conhecê-los.

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

- `MotivoTramite` → `processo.pro_motivo_tramite` (`mot_descricao`)
- `TipoTramiteMotivoTramite` → `processo.pro_tipo_motivo_tramite`

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

> Duas pegadinhas de nomenclatura aqui: a tabela é `pro_tipo_motivo_tramite` (e não `pro_tipo_tramite_motivo_tramite`, como o nome da classe sugeriria), e a entidade aponta para `MotivoTramiteRepository` — não tem repositório próprio.

---

## Eventos de processo

Evento é o mecanismo que liga o motor genérico ao resto do ERP: "quando o processo chegar nesta etapa, faça alguma coisa lá fora".

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

**Entidade:** → `processo.pro_evento_processo`

| Propriedade | Coluna | Observação |
|---|---|---|
| `codigo` | `evpro_codigo` | Qual ação. Ver lista |
| `nome` | `evpro_nome` | |
| `tipo` | `evpro_tipo` | `1 = TIPO_NOVO_PROCESSO`, `2 = TIPO_VALIDACAO_TRAMITE` |
| `fluxo` / `fluxoAssunto` / `tipoTramite` / `centroCusto` | `flu_id` / `fluass_id` / `tiptra_id` / `cencus_id` | O gatilho: em que ponto do desenho o evento vale |
| `preRequisito` | `evpro_id_pre_requisito` | Autorrelação — encadeia eventos |
| `ordem` | `evpro_ordem` | |
| `parametro` | `evpro_parametro` | `json_array`, lido pelos configs de `Model/Processo/EventoProcesso/` |
| `template` | `evepro_template` | |
| `ativo` | `evepro_ativo` | |

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 em `src/Unico/Model/Processo/EventoProcesso/`, todos `extends AbstractConfiguracaoBase`:

- `EventoProcessoAlteracaoPrazo`
- `EventoProcessoAlterarSituacaoPedido`
- `EventoProcessoCancelamentoOc`
- `EventoProcessoEmissaoNFe`
- `EventoProcessoMovimentacaoFinanceira`
- `EventoProcessoPedidoMarketplace`

`EventoProcessoFilter` (também sem tabela) encapsula os filtros de busca do evento — processo, pedido, ordem de compra, nota fiscal, empresa destino. Consumidor único: `EventoProcessoBaseFormController`.

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

**Entidade:** → `processo.pro_processo_tramite_evento_processo`

| Propriedade | Coluna | Observação |
|---|---|---|
| `processoTramite` | `protra_id` | O trâmite que disparou |
| `eventoProcesso` | `evpro_id` | A definição |
| `situacao` | `event_situacao` | Ver abaixo |
| `dataHoraSituacao` | `event_data_hora_situacao` | |
| `log` | `event_log` | Mensagem de erro quando falha |
| `params` | `event_params` | `json_array` — guarda também o contador de tentativas |
| `notaFiscal` / `movimentacao` | `nf_id` / `mov_id` | Resultado produzido pelo evento |

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

`IGNORADO` não é falha: é o executor dizendo "as condições não se aplicam a este caso", e é o desfecho mais comum nos eventos condicionais.

### O job de eventos

`Job/Processo/ProcessoTramiteEventoProcessoJobController` lê a fila (`ProcessoTramiteEventoProcessoRepository::getFilaJob()`) e despacha por código:

```sql
WHERE event_situacao = FILA
   OR (event_situacao = ERRO AND coalesce((event_params->>'errors')::smallint, 2) < 2)
LIMIT 250
```

Ou seja: **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:

- `ProcessoTramiteEventoProcessoEmissaoNotaFiscalController`
- `ProcessoTramiteEventoProcessoAlteracaoPrazoEntregaController`
- `ProcessoTramiteEventoProcessoAlterarSituacaoPedido`
- `ProcessoTramiteEventoProcessoMovimentacaoFinanceira`
- `ProcessoTramiteEventoProcessoCancelamentoOc`

Os demais códigos são de tipo `TIPO_NOVO_PROCESSO`: o "efeito" deles é abrir um processo, não executar uma ação.

> **Comportamento a conhecer antes de depurar a fila:** o job roda cada evento em transação própria, mas no `catch` ele 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.

### Disparadores de processo já 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 (`processaDialogflowCx()`, `processaFluxoConstrutor()`, `vincularSessaoProcesso()`)
- `ProcessoTramiteDocumentoEletronicoController` — anexos, com upload e download em S3
- `ProcessoTramiteEventoProcessoController` — criação da linha de execução do evento

---

## Centro de custo

É a dimensão organizacional do motor: responde **para onde** a demanda vai e **quem pode vê-la**.

> Apesar de ser usado quase exclusivamente pelo motor de processos, o centro de custo fica no schema **`public`**, não em `processo.*`.

### Modelo

**Entidade:** `Unico\Model\CentroCusto` → `un_centro_custo`

| Propriedade | Coluna | Observação |
|---|---|---|
| `id` | `cencus_id` | |
| `codigo` | `cencus_codigo` | Código hierárquico `001.002.003` |
| `nome` | `cencus_nome` | |
| `ativo` | `cencus_ativo` | |
| `centroCustoPai` | `cencus_id_pai` | Autorrelação — a árvore |

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

**Entidade:** `Unico\Model\CentroCustoUsuario` → `un_centro_custo_usuario` (`cencus_id`, `usu_id`, `cenusu_responsavel`)

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

### O código hierárquico

`CentroCustoRepository::getNextCodigo()` monta o próximo código a partir do pai:

- **sem pai** — pega o `max(cencus_codigo)` entre as raízes, incrementa e 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`).

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

> Não há transação nem lock em `getNextCodigo()`. Duas criações simultâneas sob o mesmo pai calculam o mesmo código; a colisão é barrada pelo unique constraint `(emp_id, cencus_codigo)`.

### Visibilidade e recebimento

`CentroCustoUsuarioRepository::validaPrivilegioUsuarioCentroCusto()` é a regra de autorização do módulo. Ela usa um `WITH RECURSIVE` que sobe do centro de custo do trâmite até a raiz e verifica se o usuário está vinculado a **qualquer um dos ancestrais**.

**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 da empresa como árvore e conceder acesso num nível só.

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

- trâmite **sem pessoa de destino** → basta estar no centro de custo (ou em um ancestral);
- trâmite **com pessoa de destino** → exige `cenusu_responsavel = true`, isto é, só o responsável do setor pode tomar para si algo que foi endereçado a outra pessoa.

### Variante do portal

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

---

## A ponte com o atendimento

**Entidade:** `ProcessoAtendimentoOrigem` → `processo.pro_processo_atendimento_origem` (`pro_id`, `ate_id`)

Uma tabela de duas colunas, e é a peça que conecta CRM e Processos. Ela é invisível para quem lê só um dos dois módulos.

**Quando é gravada:** `ProcessoFormController::beanModelAtendimentoProcesso()` cria a linha quando o formulário de processo recebe `atendimento_id`. O mesmo acontece pelo caminho da IA em `DialogFlowApiController`, quando um texto padrão configurado com `getRealizaAberturaProcesso()` abre um processo a partir da conversa.

**Para que serve depois:** `ProcessoController::notificarOrigemProcesso()` busca a **última** linha do processo (`orderBy id desc`) e, a cada trâmite, envia a mensagem do trâmite de volta como resposta no atendimento — via `AtendimentoController::sendMessage()`, passando o `ProcessoTramite` para que a mensagem fique amarrada a ele (`crm_atendimento_mensagem.protra_id`).

O efeito prático: o cliente acompanha o andamento do processo interno sem sair da conversa que ele abriu, e sem que o CRM precise saber o que é um trâmite. Só funciona com `setNotificaOrigemProcesso(true)`.

Os dois sentidos existem:

- **atendimento → processo** — o operador (ou a IA) decide que a dúvida exige trabalho interno e abre o processo pelo próprio atendimento;
- **processo → atendimento** — cada trâmite do processo vira mensagem no atendimento de origem.

---

## Quem usa o motor fora do `Unico`

O motor é genérico de propósito, e essa é a prova: 8 módulos o consomem.

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

Todos entram pelo mesmo lugar: instanciam `ProcessoController`, preenchem o `Processo`, ligam as flags de comportamento e chamam `insereProcesso()`. Nenhum deles manipula `pro_processo_tramite` diretamente.

---

## Telas

| Diretório | Controllers |
|---|---|
| `src/Unico/Controller/Grid/Processo/` | `FluxoGridController`, `FluxoGrupoGridController`, `FluxoAssuntoGridController`, `TipoTramiteGridController`, `MotivoTramiteGridController`, `EventoProcessoGridController`, `ProcessoGridController`, `ProcessoTramiteGridController`, `ProcessoTramiteNotificacaoGridController`, `ProcessoTramiteEventoProcessoGridController`, `ProcessoTramiteEventoNotaFiscalItemGridController` |
| `src/Unico/Controller/Form/Processo/` | `FluxoFormController`, `FluxoGrupoFormController`, `TipoTramiteFormController`, `MotivoTramiteFormController`, `ProcessoFormController`, `ProcessoHistoricoFormController`, `EventoProcessoFormController`, `EventoProcessoFactory`, `ProcessoTramiteEventoProcessoFormController` |
| `src/Unico/Controller/Job/Processo/` | O job de eventos, seus 5 executores e `ProcessoTramitePanobotIaOperationJobController` |
| `src/Unico/Controller/Report/` | `ProcessoReportController`, `ProcessoDevolucaoDinheiroReportController` |

A grid de eventos permite reprocessar manualmente linhas em `SITUACAO_ERRO` ou `SITUACAO_IGNORADO` — é o caminho de recuperação quando a fila trava.
