# API do AgendAI (leitura) — Oportunidades

Guia de uso (login, convenções, erros): https://api-docs.agendai.pro/md/guia.md


Funil comercial (CRM) — oportunidades, etapas, histórico.

- `GET /oportunidades` — Lista oportunidades (paginada, com filtros e ordenação)
- `GET /oportunidades/{id}` — Detalha uma oportunidade
- `GET /oportunidades/{id}/historico` — Linha do tempo (histórico) da oportunidade, com paginação por cursor

### GET /oportunidades

**Lista oportunidades (paginada, com filtros e ordenação)** · operationId `listarOportunidades`

Autenticação: `Authorization: Bearer <token>`.

Lista as oportunidades comerciais (CRM) do tenant do usuário, uma linha por oportunidade, com paginação por página
(`page`/`pageSize`), filtros combináveis (todos opcionais, combinados com E) e ordenação.

## Modelo de dados
- Uma **oportunidade** pertence a um **pipeline** e está em uma **etapa** dele (`pipeline` → `etapas`, ordenadas por
  `position`). O `fluxo` do pipeline (`COMERCIAL`, `CS_PARA_COMERCIAL`, `CS_PARA_CS`, `COMERCIAL_PARA_CS`) é herdado
  pela oportunidade.
- **`status`** (ciclo de vida): `ABERTA` (em negociação), `ACEITA` (**ganha**; o enum usa "ACEITA", não "ganha") e
  `PERDIDA`. As etapas têm `papel` (`NENHUM`, `PROPOSTA_ENVIADA`, `AJUSTE_SOLICITADO`, `ACEITE`, `PERDA`); as etapas de
  papel `ACEITE` e `PERDA` são as colunas "fechadas" do quadro.
- **`tipo`**: `VENDA_NOVA`, `ADITIVO` ou `SERVICO`.
- **Pessoas**: `vendedor` é um registro de **vendedor** (tabela de vendedores, não de usuários; seu `id` é o aceito em
  `vendedorId`). O **CS responsável** (`csResponsavel`, um usuário) só aparece no detalhe (`GET /oportunidades/{id}`).
- **`empresa`**: a empresa (cliente ou prospect) dona da oportunidade. `contato` é opcional.
- **Valores**: `valorEstimado` é um **número JSON em reais (BRL)**, com até 2 casas decimais (não é string, não é centavos);
  `null` = sem valor estimado. Datas de calendário (`dataPrevisaoFechamento`) são `YYYY-MM-DD`; instantes
  (`createdAt`, `entrouNaEtapaEm`) são ISO 8601 em UTC. Dias "civis" (filtros `criadaDe`/`criadaAte`, `atrasada`, `diasNaEtapa`)
  usam o fuso de São Paulo.

## Quem enxerga o quê (escopo de visibilidade)
Não há erro 403 por falta de permissão: o escopo **filtra a lista**.
- Usuário com escopo total vê **todas** as oportunidades do tenant. Legado (acesso por setor desligado): papel manager/admin/owner,
  `type=ADMIN` ou `is_admin` (gestor de qualquer setor). Acesso por setor ligado: Dono/Admin, gestor do Comercial e, com `ESCOPO_COMERCIAL`
  desligado, gestor de qualquer setor.
- Usuário vinculado a **um único vendedor** vê **somente as oportunidades desse vendedor** (`vendedorId` igual ao dele). Com `ESCOPO_COMERCIAL`
  ligado, o vínculo de vendedor só vale para membro ou gestor do Comercial; os demais caem no perfil restrito.
- Qualquer outro usuário (perfil "restrito") vê **somente as oportunidades em que é responsável por alguma tarefa**; nele,
  `contrato` vem sempre `null`.
Filtros como `vendedorId` só reduzem ainda mais o que o escopo já permite.
Sempre há isolamento por tenant.

## Filtros
Todos opcionais. Combinações sem resultado possível são recusadas com `400` em vez de devolver lista vazia:
`semPrevisao` com `previsaoDe`/`previsaoAte`/`atrasada`; `semValor` com `valorMin`; `situacao` com `status`;
`criadaDe` maior que `criadaAte`. Parâmetros desconhecidos também geram `400`. Os booleanos de query são as strings
`true`/`false`; `false` equivale a "sem filtro" (não filtra pelo oposto).

## Ordenação e paginação
`sort` (`criadaEm` padrão, `valor`, `previsao`, `codigo`) e `dir` (`desc` padrão). Em `valor` e `previsao` os nulos ficam por último
em qualquer direção; `codigo` ordena pela data de criação/sequência (equivale a `criadaEm`), e o `id` desempata.
`page` começa em 1; `pageSize` padrão 25, máximo 100. O corpo traz `total` (de todos os registros que casam com o filtro, sem paginar).

## Campos derivados do item
- `diasNaEtapa`: dias civis desde `entrouNaEtapaEm`.
- `proximaTarefa`: tarefa aberta de menor prazo (tarefas `ADIADA` não contam; só tarefas que o usuário enxerga); `tarefasVencidas`: abertas com prazo vencido.
- `proposta`: selo da versão mais recente da proposta que não é rascunho nem substituída; `termometro`/`termometroMotivo` só existem
  enquanto a proposta espera o cliente (`ENVIADA` ou `AJUSTE_SOLICITADO`) e a oportunidade está aberta.
- `contrato`: selo do contrato da oportunidade.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `search` | query | string | não | Busca textual (sem diferenciar maiúsculas) em código, título, nome fantasia ou razão social da empresa e nome do contato. Máx. 100 caracteres; espaços das pontas são removidos. Ex.: `Alfa` |
| `vendedorId` | query | string (uuid) | não | UUID do vendedor (o `vendedor.id` que vem nos itens desta lista). Ex.: `0b8f6a52-3c41-4d0e-9a77-5f2d1c6e8a10` |
| `empresaId` | query | string (uuid) | não | UUID da empresa. Ex.: `7d3c2f10-8a4b-4e65-b1d9-2c9e5a7f0b33` |
| `produtoId` | query | string (uuid) | não | UUID do produto da oportunidade (o `produto.id` que vem nos itens desta lista). Ex.: `5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1` |
| `fluxo` | query | string — um de: `COMERCIAL`, `CS_PARA_COMERCIAL`, `CS_PARA_CS`, `COMERCIAL_PARA_CS` | não | Fluxo do pipeline em que a oportunidade está. Ex.: `COMERCIAL` |
| `comTarefaVencida` | query | string — um de: `true`, `false` | não | `true` = só oportunidades com pelo menos uma tarefa vencida (entre as tarefas que o usuário enxerga). `false` = sem filtro. Ex.: `true` |
| `previsaoDe` | query | string (date) | não | Previsão de fechamento a partir desta data (inclusiva), `YYYY-MM-DD`. Ex.: `2026-11-01` |
| `previsaoAte` | query | string (date) | não | Previsão de fechamento até esta data (inclusiva), `YYYY-MM-DD`. Ex.: `2026-12-31` |
| `semPrevisao` | query | string — um de: `true`, `false` | não | `true` = só oportunidades sem data de previsão. Não combina com `previsaoDe`, `previsaoAte` nem `atrasada`. `false` = sem filtro. Ex.: `true` |
| `atrasada` | query | string — um de: `true`, `false` | não | `true` = só `ABERTA` com previsão de fechamento anterior a hoje (dia civil de São Paulo). `false` = sem filtro. Ex.: `true` |
| `situacao` | query | string — um de: `abertas`, `fechadas` | não | `abertas` = status `ABERTA`; `fechadas` = `ACEITA` ou `PERDIDA`. Não combina com `status`. Ex.: `abertas` |
| `valorMin` | query | number; mín. 0; máx. 999999999999.99 | não | Valor estimado mínimo, inclusivo, em reais (0 a 999999999999.99). Oportunidades sem valor ficam de fora. Não combina com `semValor`. Atenção: `valorMin=` vazio vira 0 (sem 400) e exclui as sem valor. Ex.: `10000` |
| `semValor` | query | string — um de: `true`, `false` | não | `true` = só oportunidades sem valor estimado. Não combina com `valorMin`. `false` = sem filtro. Ex.: `true` |
| `criadaDe` | query | string (date) | não | Criada a partir deste dia (inclusivo, dia civil de São Paulo), `YYYY-MM-DD`. Ex.: `2026-10-01` |
| `criadaAte` | query | string (date) | não | Criada até este dia (inclusivo, dia civil de São Paulo), `YYYY-MM-DD`. Deve ser maior ou igual a `criadaDe`. Ex.: `2026-10-31` |
| `pipelineId` | query | string (uuid) | não | UUID do pipeline (o `pipeline.id` que vem nos itens desta lista). Ex.: `9c4e7a12-5d38-4b0f-a6e1-3f8b2d1c0e55` |
| `etapaId` | query | string (uuid) | não | UUID da etapa (ids de etapa vêm em `etapas[].id` do quadro ou em `etapasDoPipeline[].id` do detalhe). Ex.: `2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88` |
| `status` | query | string — um de: `ABERTA`, `ACEITA`, `PERDIDA` | não | Status exato. Não combina com `situacao`. Ex.: `ABERTA` |
| `tipo` | query | string — um de: `VENDA_NOVA`, `ADITIVO`, `SERVICO` | não | Tipo da oportunidade. Ex.: `VENDA_NOVA` |
| `projetoId` | query | string (uuid) | não | UUID do projeto vinculado. Ex.: `4f0a8c36-1e92-4d7b-8c05-9a3e6b1d2f70` |
| `sort` | query | string — um de: `criadaEm`, `valor`, `previsao`, `codigo`; padrão `"criadaEm"` | não | Campo de ordenação. `valor` e `previsao` põem nulos por último. Ex.: `valor` |
| `dir` | query | string — um de: `asc`, `desc`; padrão `"desc"` | não | Direção da ordenação. Ex.: `desc` |
| `page` | query | integer; padrão `1`; mín. 1 | não | Número da página, a partir de 1. Ex.: `1` |
| `pageSize` | query | integer; padrão `25`; mín. 1; máx. 100 | não | Itens por página (1 a 100). Ex.: `25` |

#### Resposta 200

Página de oportunidades.

Tipo da raiz: OportunidadeLista

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `items` | qualquer[] | sim |  |
| `items[].id` | string (uuid) | sim |  |
| `items[].codigo` | string | sim | Código legível "OP-AAAA-NNNN" (sequência por ano). |
| `items[].titulo` | string | sim |  |
| `items[].tipo` | string — um de: `VENDA_NOVA`, `ADITIVO`, `SERVICO` | sim |  |
| `items[].status` | string — um de: `ABERTA`, `ACEITA`, `PERDIDA` | sim | ACEITA = ganha. |
| `items[].fluxo` | string — um de: `COMERCIAL`, `CS_PARA_COMERCIAL`, `CS_PARA_CS`, `COMERCIAL_PARA_CS` | sim | Herdado do pipeline. |
| `items[].etapaId` | string (uuid) | sim | Etapa atual (nome em `etapa.nome` da lista, ou `etapas[].nome` do quadro). |
| `items[].position` | integer | sim | Ordem do cartão dentro da etapa (ordem de arraste). |
| `items[].valorEstimado` | number \| null | sim | Valor estimado em reais (até 2 casas); nulo = não informado. |
| `items[].dataPrevisaoFechamento` | string (date) \| null | sim | YYYY-MM-DD. |
| `items[].empresa` | OportunidadeEmpresa | sim |  |
| `items[].empresa.id` | string (uuid) | sim |  |
| `items[].empresa.nome` | string | sim | Nome fantasia (ou razão social, ou "(sem nome)"). |
| `items[].empresa.cnpj` | string \| null | sim | CNPJ normalizado (14 caracteres, sem pontuação) ou nulo. |
| `items[].empresa.relacao` | string — um de: `CLIENTE`, `PROSPECT`, `TERCEIRO` | sim |  |
| `items[].empresa.ativo` | boolean | sim | Falso para cliente inativado. |
| `items[].contato` | OportunidadeContato \| qualquer \| null | sim |  |
| `items[].vendedor` | OportunidadeVendedorRef | sim | Vendedor da oportunidade (id do cadastro de vendedores, o mesmo de `vendedorId` nos filtros). |
| `items[].vendedor.id` | string (uuid) | sim |  |
| `items[].vendedor.nome` | string | sim |  |
| `items[].vendedor.iniciais` | string | sim | Primeira letra do primeiro e do último nome, maiúsculas (ex.: "VE"); "?" se sem nome. |
| `items[].entrouNaEtapaEm` | string (date-time) | sim |  |
| `items[].diasNaEtapa` | integer | sim | Dias civis (São Paulo) na etapa atual. |
| `items[].proximaTarefa` | OportunidadeProximaTarefa \| qualquer \| null | sim | Tarefa aberta de menor prazo (ADIADA não conta; só as que o usuário enxerga). |
| `items[].tarefasVencidas` | integer | sim |  |
| `items[].proposta` | OportunidadePropostaSelo \| qualquer \| null | sim |  |
| `items[].termometro` | string — um de: `QUENTE`, `MORNA`, `FRIA`, `null` \| null | sim | Só enquanto a proposta espera o cliente (ENVIADA/AJUSTE_SOLICITADO) e a oportunidade está ABERTA. |
| `items[].termometroMotivo` | string — um de: `AJUSTE_SOLICITADO`, `ITEM_ALTERADO`, `ABERTURAS_RECENTES`, `VENCE_SEM_ABERTURA`, `SEM_ABERTURA_RECENTE`, `ABERTA`, `AGUARDANDO_ABERTURA`, `null` \| null | sim | Regra que decidiu o termômetro. QUENTE: ajuste solicitado, item alterado ou 2+ aberturas reais nas últimas 72 h. FRIA: validade em até 3 dias sem abertura, ou sem abertura/entrega há mais de 5 dias. MORNA: o resto. |
| `items[].contrato` | OportunidadeContratoSelo \| qualquer \| null | sim | Nulo para o usuário restrito (que não vê contratos). |
| `items[].pipeline` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). |
| `items[].pipeline.id` | string (uuid) | sim |  |
| `items[].pipeline.nome` | string | sim |  |
| `items[].etapa` | object | sim |  |
| `items[].etapa.id` | string (uuid) | sim |  |
| `items[].etapa.nome` | string | sim |  |
| `items[].etapa.papel` | string — um de: `NENHUM`, `PROPOSTA_ENVIADA`, `AJUSTE_SOLICITADO`, `ACEITE`, `PERDA` | sim |  |
| `items[].produto` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). |
| `items[].produto.id` | string (uuid) | sim |  |
| `items[].produto.nome` | string | sim |  |
| `items[].createdAt` | string (date-time) | sim |  |
| `page` | integer | sim |  |
| `pageSize` | integer | sim |  |
| `total` | integer | sim | Total de registros que casam com os filtros (todas as páginas). |

Exemplo (dados fictícios):

```json
{
  "items": [
    {
      "id": "3b7e1a90-6c24-4f58-8d0b-e5a2c91f7d46",
      "codigo": "OP-2026-0042",
      "titulo": "Implantação ERP - Empresa Alfa Ltda",
      "tipo": "VENDA_NOVA",
      "status": "ABERTA",
      "fluxo": "COMERCIAL",
      "etapaId": "2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88",
      "position": 3,
      "valorEstimado": 48500.5,
      "dataPrevisaoFechamento": "2026-12-15",
      "empresa": {
        "id": "7d3c2f10-8a4b-4e65-b1d9-2c9e5a7f0b33",
        "nome": "Empresa Alfa Ltda",
        "cnpj": "12345678000195",
        "relacao": "PROSPECT",
        "ativo": true
      },
      "contato": {
        "id": "c1a4d8e2-0b97-4f36-a5d1-7e2b9c3f6084",
        "nome": "Contato Exemplo",
        "cargo": "Diretor Financeiro",
        "email": "contato@alfa.example",
        "telefone": "11999990000"
      },
      "vendedor": {
        "id": "0b8f6a52-3c41-4d0e-9a77-5f2d1c6e8a10",
        "nome": "Vendedor Exemplo",
        "iniciais": "VE"
      },
      "entrouNaEtapaEm": "2026-11-03T14:22:10.000Z",
      "diasNaEtapa": 8,
      "proximaTarefa": {
        "id": "a9d2f4b1-3e60-4c78-9b15-0f8e7d6c5a21",
        "titulo": "Ligar para o contato",
        "tipo": "LIGACAO",
        "prazo": "2026-11-12T17:00:00.000Z",
        "vencida": false
      },
      "tarefasVencidas": 0,
      "proposta": {
        "versao": 1,
        "status": "ENVIADA",
        "aberturas": 2,
        "validadeAte": "2026-11-30"
      },
      "termometro": "MORNA",
      "termometroMotivo": "ABERTA",
      "contrato": {
        "situacao": "semContrato",
        "contratoId": null,
        "categoria": null,
        "assinados": null,
        "total": null,
        "quantidade": 0,
        "clienteObrigatorio": false,
        "prospect": true,
        "podeGerar": false
      },
      "pipeline": {
        "id": "9c4e7a12-5d38-4b0f-a6e1-3f8b2d1c0e55",
        "nome": "Pipeline Comercial"
      },
      "etapa": {
        "id": "2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88",
        "nome": "Proposta enviada",
        "papel": "PROPOSTA_ENVIADA"
      },
      "produto": {
        "id": "5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1",
        "nome": "Produto Exemplo"
      },
      "createdAt": "2026-10-20T12:05:00.000Z"
    }
  ],
  "page": 1,
  "pageSize": 25,
  "total": 1
}
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.

---

### GET /oportunidades/{id}

**Detalha uma oportunidade** · operationId `obterOportunidade`

Autenticação: `Authorization: Bearer <token>`.

Devolve o detalhe completo de uma oportunidade: dados do cartão mais pipeline e todas as suas etapas (`etapasDoPipeline`), empresa, contato,
projeto, produto, tipo de projeto, vendedor, CS responsável, briefing, cláusula especial, motivo de perda, escopo (itens com horas), auditoria
de criação e as `permissoes` que o usuário tem sobre ela.

- `valorEstimado` e `horasEstimadas` (e `escopo[].horas`) são **números JSON** (reais e horas decimais), `null` quando não informados.
- `fechadaEm` só existe quando `status` é `ACEITA` ou `PERDIDA`; `motivoPerda`/`motivoPerdaDescricao` só quando `PERDIDA`
  (`PRECO`, `CONCORRENTE`, `SEM_ORCAMENTO`, `SEM_RETORNO`, `ADIADO`, `OUTRO`).
- `etapa.probabilidade` é um inteiro 0 a 100 (ou `null`), usado no valor ponderado do quadro.
- `permissoes` é um reflexo informativo (o usuário desta API é somente leitura): `podeEditar`/`podeMover` exigem status `ABERTA` e que o usuário seja
  escopo total ou dono (vendedor) da oportunidade; `podeTrocarPipeline` exige gestor do CRM e status `ABERTA`; `podeReabrir` exige gestor do CRM e
  status `PERDIDA`. Gestor = papel manager/admin/owner no legado; Dono/Admin ou gestor do Comercial no acesso por setor.

## Permissão e escopo
Qualquer usuário autenticado, mas só enxerga oportunidades do seu escopo (todas / as do seu vendedor / aquelas em que é responsável por tarefa).
Uma oportunidade que existe mas está **fora do escopo** responde `404` com `code = OPORTUNIDADE_NAO_ENCONTRADA` (nunca 403), para não revelar existência.
Id que não é UUID responde `400`.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `id` | path | string (uuid) | sim | UUID da oportunidade. Ex.: `3b7e1a90-6c24-4f58-8d0b-e5a2c91f7d46` |

#### Resposta 200

Detalhe da oportunidade.

Tipo da raiz: OportunidadeDetalhe

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `id` | string (uuid) | sim |  |
| `codigo` | string | sim | "OP-AAAA-NNNN". |
| `numero` | integer | sim | Número sequencial (parte final do código). |
| `titulo` | string | sim |  |
| `tipo` | string — um de: `VENDA_NOVA`, `ADITIVO`, `SERVICO` | sim |  |
| `status` | string — um de: `ABERTA`, `ACEITA`, `PERDIDA` | sim |  |
| `pipeline` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). |
| `pipeline.id` | string (uuid) | sim |  |
| `pipeline.nome` | string | sim |  |
| `fluxo` | string — um de: `COMERCIAL`, `CS_PARA_COMERCIAL`, `CS_PARA_CS`, `COMERCIAL_PARA_CS` | sim |  |
| `etapa` | object | sim |  |
| `etapa.id` | string (uuid) | sim |  |
| `etapa.nome` | string | sim |  |
| `etapa.papel` | string — um de: `NENHUM`, `PROPOSTA_ENVIADA`, `AJUSTE_SOLICITADO`, `ACEITE`, `PERDA` | sim |  |
| `etapa.probabilidade` | integer \| null | sim | 0 a 100. |
| `etapasDoPipeline` | object[] | sim | Todas as etapas do pipeline, em ordem. |
| `etapasDoPipeline[].id` | string (uuid) | sim |  |
| `etapasDoPipeline[].nome` | string | sim |  |
| `etapasDoPipeline[].position` | integer | sim |  |
| `etapasDoPipeline[].papel` | string — um de: `NENHUM`, `PROPOSTA_ENVIADA`, `AJUSTE_SOLICITADO`, `ACEITE`, `PERDA` | sim |  |
| `etapasDoPipeline[].cor` | string \| null | sim |  |
| `entrouNaEtapaEm` | string (date-time) | sim |  |
| `diasNaEtapa` | integer | sim |  |
| `empresa` | OportunidadeEmpresa | sim |  |
| `empresa.id` | string (uuid) | sim |  |
| `empresa.nome` | string | sim | Nome fantasia (ou razão social, ou "(sem nome)"). |
| `empresa.cnpj` | string \| null | sim | CNPJ normalizado (14 caracteres, sem pontuação) ou nulo. |
| `empresa.relacao` | string — um de: `CLIENTE`, `PROSPECT`, `TERCEIRO` | sim |  |
| `empresa.ativo` | boolean | sim | Falso para cliente inativado. |
| `contato` | OportunidadeContato \| qualquer \| null | sim |  |
| `projeto` | OportunidadePessoaRef \| qualquer \| null | sim | Projeto vinculado (nome do projeto). |
| `produto` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). |
| `produto.id` | string (uuid) | sim |  |
| `produto.nome` | string | sim |  |
| `tipoProjeto` | OportunidadePessoaRef \| qualquer \| null | sim |  |
| `vendedor` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). |
| `vendedor.id` | string (uuid) | sim |  |
| `vendedor.nome` | string | sim |  |
| `csResponsavel` | OportunidadePessoaRef \| qualquer \| null | sim | Usuário de CS responsável (id de usuário). |
| `valorEstimado` | number \| null | sim | Reais. |
| `horasEstimadas` | number \| null | sim | Horas (decimal). |
| `dataPrevisaoFechamento` | string (date) \| null | sim |  |
| `briefing` | string \| null | sim |  |
| `clausulaEspecial` | string \| null | sim | Texto livre que, no contrato gerado, prevalece sobre as demais cláusulas. |
| `motivoPerda` | string — um de: `PRECO`, `CONCORRENTE`, `SEM_ORCAMENTO`, `SEM_RETORNO`, `ADIADO`, `OUTRO`, `null` \| null | sim |  |
| `motivoPerdaDescricao` | string \| null | sim |  |
| `fechadaEm` | string (date-time) \| null | sim |  |
| `escopo` | object[] | sim |  |
| `escopo[].id` | string (uuid) | sim |  |
| `escopo[].ordem` | integer | sim |  |
| `escopo[].descricao` | string \| null | sim |  |
| `escopo[].horas` | number \| null | sim | Horas (decimal). |
| `escopo[].modulo` | OportunidadePessoaRef \| qualquer \| null | sim |  |
| `escopo[].subModulo` | OportunidadePessoaRef \| qualquer \| null | sim |  |
| `createdBy` | OportunidadePessoaRef | sim | Referência {id, nome}. nome nunca é nulo (nome ausente no cadastro vira "(sem nome)"). |
| `createdBy.id` | string (uuid) | sim |  |
| `createdBy.nome` | string | sim |  |
| `createdAt` | string (date-time) | sim |  |
| `updatedAt` | string (date-time) | sim |  |
| `proposta` | OportunidadePropostaSelo \| qualquer \| null | sim |  |
| `termometro` | string — um de: `QUENTE`, `MORNA`, `FRIA`, `null` \| null | sim |  |
| `termometroMotivo` | string — um de: `AJUSTE_SOLICITADO`, `ITEM_ALTERADO`, `ABERTURAS_RECENTES`, `VENCE_SEM_ABERTURA`, `SEM_ABERTURA_RECENTE`, `ABERTA`, `AGUARDANDO_ABERTURA`, `null` \| null | sim |  |
| `contrato` | OportunidadeContratoSelo \| qualquer \| null | sim | Nulo para o usuário restrito. |
| `permissoes` | object | sim |  |
| `permissoes.podeEditar` | boolean | sim |  |
| `permissoes.podeMover` | boolean | sim |  |
| `permissoes.podeTrocarPipeline` | boolean | sim |  |
| `permissoes.podeReabrir` | boolean | sim |  |

Exemplo (dados fictícios):

```json
{
  "id": "3b7e1a90-6c24-4f58-8d0b-e5a2c91f7d46",
  "codigo": "OP-2026-0042",
  "numero": 42,
  "titulo": "Implantação ERP - Empresa Alfa Ltda",
  "tipo": "VENDA_NOVA",
  "status": "ABERTA",
  "pipeline": {
    "id": "9c4e7a12-5d38-4b0f-a6e1-3f8b2d1c0e55",
    "nome": "Pipeline Comercial"
  },
  "fluxo": "COMERCIAL",
  "etapa": {
    "id": "2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88",
    "nome": "Proposta enviada",
    "papel": "PROPOSTA_ENVIADA",
    "probabilidade": 50
  },
  "etapasDoPipeline": [
    {
      "id": "1a5c7e90-3b24-4d68-9f01-8e2b4a6c0d13",
      "nome": "Qualificação",
      "position": 1,
      "papel": "NENHUM",
      "cor": "#9ca3af"
    },
    {
      "id": "2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88",
      "nome": "Proposta enviada",
      "position": 3,
      "papel": "PROPOSTA_ENVIADA",
      "cor": "#3b82f6"
    }
  ],
  "entrouNaEtapaEm": "2026-11-03T14:22:10.000Z",
  "diasNaEtapa": 8,
  "empresa": {
    "id": "7d3c2f10-8a4b-4e65-b1d9-2c9e5a7f0b33",
    "nome": "Empresa Alfa Ltda",
    "cnpj": "12345678000195",
    "relacao": "PROSPECT",
    "ativo": true
  },
  "contato": {
    "id": "c1a4d8e2-0b97-4f36-a5d1-7e2b9c3f6084",
    "nome": "Contato Exemplo",
    "cargo": "Diretor Financeiro",
    "email": "contato@alfa.example",
    "telefone": "11999990000"
  },
  "projeto": null,
  "produto": {
    "id": "5a1e9c47-2b60-4f8d-8e13-6d0a4b92c7f1",
    "nome": "Produto Exemplo"
  },
  "tipoProjeto": {
    "id": "8e2d4a60-9b13-4c75-a0f8-1b6c3d7e9f42",
    "nome": "Implantação"
  },
  "vendedor": {
    "id": "0b8f6a52-3c41-4d0e-9a77-5f2d1c6e8a10",
    "nome": "Vendedor Exemplo"
  },
  "csResponsavel": null,
  "valorEstimado": 48500.5,
  "horasEstimadas": 120,
  "dataPrevisaoFechamento": "2026-12-15",
  "briefing": "Cliente quer migrar do sistema atual até o fim do ano.",
  "clausulaEspecial": null,
  "motivoPerda": null,
  "motivoPerdaDescricao": null,
  "fechadaEm": null,
  "escopo": [
    {
      "id": "e7a3c9d1-5b02-4f64-8a1d-3c0b6e9f2d85",
      "ordem": 1,
      "descricao": "Treinamento da equipe",
      "horas": 40,
      "modulo": {
        "id": "d3f9b5a7-4c20-4e18-8a6b-0e1c7f2d9b54",
        "nome": "Módulo Exemplo"
      },
      "subModulo": null
    }
  ],
  "createdBy": {
    "id": "f4c7a1d9-6e32-4b85-9c0a-2d8e5b1f7a63",
    "nome": "Usuário Exemplo"
  },
  "createdAt": "2026-10-20T12:05:00.000Z",
  "updatedAt": "2026-11-03T14:22:10.000Z",
  "proposta": {
    "versao": 1,
    "status": "ENVIADA",
    "aberturas": 0,
    "validadeAte": "2026-11-30"
  },
  "termometro": "MORNA",
  "termometroMotivo": "AGUARDANDO_ABERTURA",
  "contrato": {
    "situacao": "semContrato",
    "contratoId": null,
    "categoria": null,
    "assinados": null,
    "total": null,
    "quantidade": 0,
    "clienteObrigatorio": false,
    "prospect": true,
    "podeGerar": false
  },
  "permissoes": {
    "podeEditar": true,
    "podeMover": true,
    "podeTrocarPipeline": false,
    "podeReabrir": false
  }
}
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.
- **404**: Registro não encontrado (ou de outra organização).

---

### GET /oportunidades/{id}/historico

**Linha do tempo (histórico) da oportunidade, com paginação por cursor** · operationId `listarHistoricoOportunidade`

Autenticação: `Authorization: Bearer <token>`.

Linha do tempo da oportunidade, do **mais recente para o mais antigo** (`createdAt` desc, `id` desc): criação, edições, mudanças de etapa/pipeline,
perda/aceite/reabertura, tarefas, propostas (inclusive ações do cliente na proposta pública), notas, contrato e formulário.

- Paginação por **cursor**: envie o `nextCursor` recebido como `cursor` na chamada seguinte, com os mesmos demais parâmetros. `nextCursor = null`
  indica a última página. O cursor é opaco (base64url); um cursor malformado gera `400` com `message` em string única (`"Cursor inválido."`), não
  na lista `campo: motivo` dos demais erros de validação. `limit` padrão 30, máximo 100.
- `origem` filtra pela origem do evento (`OPORTUNIDADE`, `TAREFA`, `PROPOSTA`, `NOTA`, `CONTRATO`, `FORMULARIO`).
- Notas excluídas não aparecem.
- Cada item: `acao` é um código textual (ex.: `CRIADA`, `EDITADA`, `ETAPA_ALTERADA`, `PIPELINE_ALTERADO`, `ACEITA`, `PERDIDA`, `REABERTA`,
  `NOTA_ADICIONADA`, `TAREFA_CRIADA`, `TAREFA_CONCLUIDA`, `PROPOSTA_ENVIADA`, `ANEXO_ADICIONADO`...; a lista não é fechada, trate valores desconhecidos como texto);
  `dados` é um objeto JSON livre com os detalhes da ação (depende de `acao`; pode ser `null`); `texto` é o conteúdo livre (por exemplo, o texto da nota).
  `autor` é o usuário que agiu; `null` quando quem agiu foi o cliente na proposta pública (aí `destinatario` traz o contato) ou o próprio
  sistema (expiração automática, eventos do provedor de assinatura, falha técnica do formulário). Nesse último caso `destinatario` também é `null`.
  `editadaEm`/`editadaPor` só existem em nota editada. `podeAlterar` indica se o usuário poderia editar/excluir a nota (só `NOTA_ADICIONADA`).

## Permissão e escopo
Qualquer usuário autenticado que enxergue a oportunidade (mesmo escopo do detalhe); fora do escopo ou inexistente = `404` `OPORTUNIDADE_NAO_ENCONTRADA`.
Para o perfil **restrito** (só vê oportunidades em que tem tarefa), eventos de origem `CONTRATO` e formulários nascidos de contrato são omitidos, e
`origem=CONTRATO` devolve lista vazia.

#### Parâmetros

| Nome | Onde | Tipo | Obrigatório | Descrição |
|---|---|---|---|---|
| `id` | path | string (uuid) | sim | UUID da oportunidade. Ex.: `3b7e1a90-6c24-4f58-8d0b-e5a2c91f7d46` |
| `origem` | query | string — um de: `OPORTUNIDADE`, `TAREFA`, `PROPOSTA`, `NOTA`, `CONTRATO`, `FORMULARIO` | não | Filtra pela origem do evento. Ex.: `NOTA` |
| `cursor` | query | string | não | Cursor opaco devolvido em `nextCursor` da página anterior. Ex.: `WyIyMDI2LTExLTAzVDE0OjIyOjEwLjAwMFoiLCI3YzJhOWYxMC00ZDM4LTRiNjUtOGUwMS0zYTVjN2U5ZDJiNDYiXQ` |
| `limit` | query | integer; padrão `30`; mín. 1; máx. 100 | não | Itens por página (1 a 100). Ex.: `30` |

#### Resposta 200

Página do histórico.

Tipo da raiz: OportunidadeHistoricoPagina

| Campo | Tipo | Sempre presente | Descrição |
|---|---|---|---|
| `items` | OportunidadeHistoricoItem[] | sim |  |
| `items[].id` | string (uuid) | sim |  |
| `items[].origem` | string — um de: `OPORTUNIDADE`, `TAREFA`, `PROPOSTA`, `NOTA`, `CONTRATO`, `FORMULARIO` | sim |  |
| `items[].acao` | string | sim | Código da ação (ex.: CRIADA, ETAPA_ALTERADA, NOTA_ADICIONADA). Lista não fechada. |
| `items[].entidadeTipo` | string \| null | sim | Tipo da entidade relacionada (ex.: "proposta", "oportunidade_anexo"), quando houver. |
| `items[].entidadeId` | string (uuid) \| null | sim |  |
| `items[].texto` | string \| null | sim |  |
| `items[].dados` | object \| null | sim | Detalhes da ação; a forma depende de `acao`. |
| `items[].autor` | OportunidadePessoaRef \| qualquer \| null | sim | Usuário que agiu; nulo quando quem agiu foi o cliente na proposta pública (aí `destinatario` traz o contato) ou o próprio sistema (expiração automática, eventos do provedor de assinatura, falha técnica do formulário; aí `destinatario` também é nulo). |
| `items[].destinatario` | OportunidadePessoaRef \| qualquer \| null | sim | Contato do cliente que agiu na proposta (só id e nome). |
| `items[].createdAt` | string (date-time) | sim |  |
| `items[].editadaEm` | string (date-time) \| null | sim |  |
| `items[].editadaPor` | OportunidadePessoaRef \| qualquer \| null | sim |  |
| `items[].podeAlterar` | boolean | sim | Só verdadeiro em `NOTA_ADICIONADA` do próprio autor (ou escopo total). |
| `nextCursor` | string \| null | sim | Cursor para a próxima página; nulo na última. |

Exemplo (dados fictícios):

```json
{
  "items": [
    {
      "id": "7c2a9f10-4d38-4b65-8e01-3a5c7e9d2b46",
      "origem": "NOTA",
      "acao": "NOTA_ADICIONADA",
      "entidadeTipo": null,
      "entidadeId": null,
      "texto": "Cliente pediu desconto no setup.",
      "dados": null,
      "autor": {
        "id": "f4c7a1d9-6e32-4b85-9c0a-2d8e5b1f7a63",
        "nome": "Usuário Exemplo"
      },
      "destinatario": null,
      "createdAt": "2026-11-03T14:22:10.000Z",
      "editadaEm": null,
      "editadaPor": null,
      "podeAlterar": true
    },
    {
      "id": "1d8b4e70-9a25-4c63-b0f7-5e2a8c1d3f94",
      "origem": "OPORTUNIDADE",
      "acao": "ETAPA_ALTERADA",
      "entidadeTipo": null,
      "entidadeId": null,
      "texto": null,
      "dados": {
        "de": {
          "id": "1a5c7e90-3b24-4d68-9f01-8e2b4a6c0d13",
          "nome": "Qualificação",
          "papel": "NENHUM"
        },
        "para": {
          "id": "2e6b9d03-7c14-4a8f-9b52-1d7f3a0c6e88",
          "nome": "Proposta enviada",
          "papel": "PROPOSTA_ENVIADA"
        },
        "origem": "MANUAL",
        "posicao": 0,
        "pendencias": []
      },
      "autor": {
        "id": "f4c7a1d9-6e32-4b85-9c0a-2d8e5b1f7a63",
        "nome": "Usuário Exemplo"
      },
      "destinatario": null,
      "createdAt": "2026-11-01T10:00:00.000Z",
      "editadaEm": null,
      "editadaPor": null,
      "podeAlterar": false
    }
  ],
  "nextCursor": null
}
```

#### Erros

- **400**: Parâmetros inválidos. `message` é uma lista com um item por problema, no formato `campo: motivo`.
- **401**: Token ausente, inválido, expirado ou de usuário desativado. Faça login de novo.
- **404**: Registro não encontrado (ou de outra organização).
