# PLAN.md — Vendedor IA — Rei dos Uniformes

Fonte de verdade comercial: [`docs/REQUIREMENTS.md`](docs/REQUIREMENTS.md) (resumo executável) e
[`docs/ESPECIFICACAO_TECNICA_1.0.md`](docs/ESPECIFICACAO_TECNICA_1.0.md) (detalhe completo, inclui a
correção §0.1 de custo-base vs. preço de venda). Trabalho contínuo neste único repositório — sem
V1/V2/V3, sem cópias.

Ordem das fases é rígida: **banco → Motor Comercial → testes do motor aprovados → só então API/painel**.
Nenhuma fase de UI começa antes da Fase 3 estar aprovada.

---

## Fase 0 — Correção de especificação (concluída antes de qualquer código)

- [x] Identificar e corrigir a inconsistência custo vs. preço de venda do produto.
- [x] Atualizar `docs/REQUIREMENTS.md` e `docs/ESPECIFICACAO_TECNICA_1.0.md` (§0.1) com a correção.

**Critério de aceite:** os dois documentos descrevem o mesmo modelo (`produtos.custo_base_unitario`
independente de `faixas_produto.preco_base_unitario`), sem contradição entre eles.

---

## Fase 1 — Estrutura do repositório e infraestrutura de banco

Depende de: Fase 0.

- [x] `package.json`, `tsconfig.json`, `vitest.config.ts`, `.gitignore`, `.env.example`.
- [x] `docker-compose.yml` (PostgreSQL local para desenvolvimento).
- [x] `knexfile.js` + `db/migrations/*` cobrindo todas as 25 tabelas do modelo relacional
      (ESPECIFICACAO_TECNICA_1.0.md §2), incluindo `produtos.custo_base_unitario`.
- [x] `db/seeds/*` com os dados de catálogo/regras já fornecidos no REQUIREMENTS.md (sem inventar
      preço/custo ausente — faixas de venda ficam NULL onde não foi definido).
- [x] PostgreSQL 16 local via Postgres.app (instalado sem privilégios de admin — binários copiados
      para `/Applications`, cluster inicializado via `initdb`/`pg_ctl`, ver README).
- [x] `npm run migrate` executado contra Postgres real — 25 migrations aplicadas com sucesso.
- [x] `npm run seed` executado com sucesso — catálogo e regras gerais carregados.
- [x] Schema real inspecionado (`\d` de `produtos`, `custos_tamanho_produto`, `faixas_produto`) e
      dados seedados conferidos linha a linha; constraints de não-negatividade e enum testadas
      manualmente (rejeitam preço negativo e política de personalização inválida).

**Critério de aceite:** `npm run migrate` cria as 25 tabelas sem erro em um Postgres vazio; `npm run
seed` popula catálogo e regras gerais sem exceções; nenhuma constraint inventa preço/custo ausente
(ficam NULL onde a especificação diz NÃO DEFINIDO). **Atingido e validado contra banco real.**

---

## Fase 2 — Motor Comercial (módulo isolado, determinístico, sem I/O)

Depende de: Fase 0 (modelo corrigido). Não depende de Postgres estar rodando — funções puras
recebendo dados já carregados (ESPECIFICACAO_TECNICA_1.0.md §1, §4).

- [x] `src/motor-comercial/types.ts` — contratos de entrada/saída.
- [x] `src/motor-comercial/faixas.ts` — seleção de faixa por quantidade; normalização "preço zero/nulo
      = NÃO DEFINIDO".
- [x] `src/motor-comercial/custo-produto.ts` — custo-base vs. custo especial por tamanho (substituição,
      não soma — correção §0.1).
- [x] `src/motor-comercial/regras.ts` — resolução de prioridade cliente > grupo > geral, com detecção
      de conflito (exceções e regras).
- [x] `src/motor-comercial/programa-arte.ts` — linha de serviço separada.
- [x] `src/motor-comercial/calcular.ts` — orquestração do algoritmo completo (§4, passos 1–9).
- [x] `src/providers/*` — interfaces `IAProvider`, `WhatsAppProvider`, `ERPProvider` (contratos apenas,
      sem implementação — Meta/OpenAI/Bling ficam para fase futura).

**Critério de aceite:** módulo compila sem erros de tipo (`npm run build`/`tsc --noEmit`); zero
dependência de rede, banco ou IA no caminho de cálculo; mesma função é o único lugar que calcula
preço/custo/margem (sem duplicação em outro arquivo).

---

## Fase 3 — Testes obrigatórios do Motor Comercial

Depende de: Fase 2. **Bloqueia todas as fases seguintes.**

- [x] Os 11 casos obrigatórios da especificação (§12), com nomenclatura ligada aos motivos de bloqueio.
- [x] Casos adicionais decorrentes da correção §0.1 (custo do produto NULL bloqueia; custo especial
      substitui custo-base; tamanho sem custo especial cai no custo-base).
- [x] Casos de robustez (conflito de regras/exceções, faixa de personalização sem cobertura, margem
      sem regra definida, classificação de cliente novo/recorrente indefinida).
- [x] Suíte executada e 100% verde.

**Critério de aceite:** `npm test` roda os 23 testes e todos passam; nenhum teste depende de mock que
pule o Motor Comercial (proibido pela estratégia de testes, §11).

> Observação sobre o teste obrigatório #11 ("alterar tabela vigente após orçamento existente →
> orçamento antigo preserva snapshot"): o Motor Comercial é uma função pura sem estado — o teste
> unitário criado nesta fase prova a pré-condição necessária (determinismo, ausência de mutação/
> cache interno). A imutabilidade fim-a-fim via `snapshots_orcamento` foi fechada na Fase 4 com um
> teste de integração contra Postgres real (ver `src/__tests__/integracao/orcamento.integration.test.ts`).

---

## Fase 4 — Camada de persistência (repositórios) e testes de integração de API

Depende de: Fase 3 aprovada. Postgres local já está acessível (Postgres.app).

- [x] Migration 026: reforça em banco que preço de venda = 0 é rejeitado (só `> 0` ou `NULL`) — defesa
      em profundidade além da normalização já feita no Motor Comercial.
- [x] `src/db/connection.ts` — fábrica de conexão Knex para código de aplicação.
- [x] `src/repositorios/{produtos,personalizacoes,clientes,regras}.ts` — leitura pura do banco,
      convertendo para os tipos do Motor Comercial (nenhuma regra de cálculo aqui).
- [x] `src/orcamento/montarEntrada.ts` — orquestra os repositórios para montar `EntradaMotorComercial`
      a partir de IDs reais.
- [x] `src/orcamento/persistirOrcamento.ts` — persiste o resultado já calculado em
      `orcamentos`/`itens_orcamento`/`personalizacoes_item`/`snapshots_orcamento`, em transação única;
      recusa persistir um resultado BLOQUEADO (nada de válido para gravar).
- [x] Testes de integração de CRUD contra Postgres real: preço 0 rejeitado pelo banco; preço NULL
      aceito e chega como `null` (nunca grátis) ao Motor Comercial.
- [x] Teste de snapshot completo (fecha o Teste obrigatório #11): orçamento persistido, tabela vigente
      alterada depois, snapshot antigo confirmado intocado em `snapshots_orcamento`, novo cálculo
      confirmado refletindo a tabela atual — banco de teste dedicado (`vendedor_ia_rei_test`), sem
      tocar no banco de desenvolvimento.

**Critério de aceite:** suíte de integração verde contra Postgres real (não mock); nenhuma lógica de
preço/custo/margem fora de `src/motor-comercial`. **Atingido — 28/28 testes passando (23 unitários + 5
de integração), banco de teste limpo automaticamente após a suíte.**

---

## Fase 5 — API REST

Depende de: Fase 4 aprovada pelo usuário (aprovado em 2026-09-12).

### 5.0 Correção — arquitetura de cliente novo/recorrente (ver docs/ESPECIFICACAO_TECNICA_1.0.md §0.2/§0.2.1)

A decisão da Fase 1 (campo manual `cliente_recorrente`) é substituída por override humano auditável >
derivação automática > indefinido. Confirmado com o usuário antes de implementar (a alternativa seria
não derivar nada automaticamente e sempre pedir override).

**Correção 5.0.1 (mesma Fase 5, antes de seguir adiante):** a primeira versão da derivação automática
contava qualquer orçamento com `status` aprovado/enviado/proposta_bling_gerada como recorrência — o
usuário apontou que isso está errado, porque esses valores descrevem o fluxo de **cotação**, não uma
**venda real** ("enviado" não prova que o cliente comprou). Corrigido: `orcamentos` ganha
`venda_confirmada_em`/`venda_confirmada_por` (migration 031), marcado manualmente via
`POST /orcamentos/:id/confirmar-venda` — um fato ortogonal ao `status` de cotação. Derivação
automática agora é "≥1 orçamento com `venda_confirmada_em` preenchido neste sistema" (continua sendo
o limite mínimo do termo, não um threshold numérico inventado). "Enviado" sozinho nunca torna
recorrente. Zero vendas confirmadas fica INDEFINIDO, não "novo". Quando o Bling for integrado, seu
histórico de pedidos passa a ser a fonte principal — ponto de extensão isolado em
`resolverClienteRecorrente` (`src/repositorios/clientes.ts`).

### 5.1 Escopo (ordem de dependências, conforme instruído)

1. Autenticação (login, JWT, papéis `admin`/`atendente`)
2. CRUD categorias
3. CRUD produtos
4. Faixas de preço dos produtos (+ detecção de sobreposição)
5. Custos especiais por tamanho
6. CRUD personalizações
7. Faixas de personalização (custo real + venda, + sobreposição)
8. CRUD grupos
9. CRUD clientes/unidades (+ override de recorrência auditado)
10. Contatos
11. Regras comerciais
12. Exceções comerciais
13. Composição de preço (`POST /simulador/calcular`) — usa exclusivamente o Motor Comercial existente
14. Orçamentos (criar via Motor Comercial + `persistirOrcamento`; consultar com snapshot)
15. Atendimentos (máquina de estados, assumir/devolver)
16. Modo global da IA
17. Human takeover (parte de 15)
18. Dashboard/resumo
19. Auditoria (transversal, tabela `auditoria` já existente)

### 5.2 Regras de arquitetura (não negociáveis, repetidas do pedido do usuário)

Nenhuma lógica comercial duplicada na API — a API valida entrada, acessa a persistência (via
`src/repositorios`), chama o Motor Comercial (`src/motor-comercial`) e persiste resultados quando
permitido (`src/orcamento/persistirOrcamento`, que já recusa persistir BLOQUEADO). Preços/custos/regras
vêm sempre do PostgreSQL. Snapshots permanecem imutáveis (comportamento validado na Fase 4,
preservado). Banco de teste isolado do de desenvolvimento (já validado na Fase 4, mesmo padrão).

### 5.3 Decisões técnicas livres de implementação para esta fase

- Framework HTTP: Express (a especificação deixa Express/Fastify livre).
- Validação de payload: Zod.
- Hash de senha: bcryptjs (puro JS — evita depender de toolchain nativo de compilação no ambiente).
- Autenticação: JWT (`jsonwebtoken`), segredo via variável de ambiente (nunca hardcoded).
- Testes HTTP: Supertest, direto contra a instância do Express (sem precisar abrir porta), banco de
  teste real (mesmo padrão da Fase 4).

**Critério de aceite:** todas as rotas do escopo 5.1 implementadas e testadas; suíte completa
(unitários + integração de persistência + integração de API) verde contra Postgres real; nenhuma rota
com fórmula de preço/custo/margem fora de `src/motor-comercial`; banco de desenvolvimento confirmado
intocado pelos testes. **Atingido — 90/90 testes passando (23 motor + 5 persistência Fase 4 + 62 API),
banco de teste zerado em todas as tabelas transacionais após a suíte (2 execuções seguidas idênticas),
banco de desenvolvimento confirmado intocado.**

### 5.4.1 Correção 5.0.1 — critério de derivação automática de recorrência

Depois da primeira entrega da Fase 5, o usuário apontou que contar orçamentos com `status`
aprovado/enviado/proposta_bling_gerada como evidência de recorrência estava errado — esses valores
descrevem o fluxo de cotação, não uma venda real. Corrigido com `migration 031`
(`orcamentos.venda_confirmada_em`/`venda_confirmada_por`, fato ortogonal ao `status`) e
`POST /orcamentos/:id/confirmar-venda`. Ver §0.2.1 da especificação e commit `301c351`.

### 5.6 Fechamento — revisão final contra o prompt original

Antes do commit final, revisão linha a linha do prompt da Fase 5 encontrou 3 gaps, todos fechados:

1. **Contatos incompletos** — só tinha criar/listar; adicionado `PUT`/`DELETE` (soft delete), como os
   demais módulos.
2. **IDs malformados retornavam 500 cru do Postgres**, não um erro de validação claro — adicionada
   validação de formato UUID em `:id`-like params via `router.param()` em cada router (descoberta
   durante a implementação: `app.param()` registrado no app **não** se propaga para routers montados
   com `app.use(path, router)` — é comportamento documentado do Express, não específico desta versão;
   por isso a validação precisa ser registrada em cada router individualmente).
3. **Dois bloqueios da lista "regras que a API não pode contornar" só tinham teste no Motor Comercial**,
   não em nível de API — adicionados testes de API para "faixa de produto não encontrada" e "conflito
   de regra comercial".

### 5.4 Migrations adicionais desta fase

- `026_reforcar_preco_venda_maior_que_zero.js` — defesa em profundidade: preço de venda = 0 vira erro
  de banco (`> 0` ou `NULL`), não só normalização no Motor Comercial.
- `027_clientes_recorrencia_override.js` — implementa a correção §0.2 (renomeia
  `cliente_recorrente` → `cliente_recorrente_override`, adiciona motivo/por/em).
- `028_atendimentos_status_anterior.js` — necessária para "devolver" restaurar literalmente o "estado
  anterior antes da assunção" que a especificação §3 descreve (faltava onde guardar esse estado).
- `029_configuracoes_sistema.js` — tabela chave/valor mínima para `GET/PUT /config/modo-ia`, que a
  especificação previa na API (§6) mas sem tabela correspondente no modelo relacional (§2).
- `030_auditoria_entidade_id_texto.js` — amplia `auditoria.entidade_id` de UUID para texto, porque nem
  toda entidade auditável tem UUID (ex.: chave `"modo_ia"` de `configuracoes_sistema`).

### 5.5 Rotas implementadas (resumo — detalhe completo no relatório da Fase 5 entregue ao usuário)

Auth (login/refresh) · categorias · produtos (+faixas +custos-tamanho) · personalizações (+faixas) ·
aplicações-posição · grupos · clientes (+contatos com CRUD completo +exceções +override de
recorrência) · regras comerciais · simulador/calcular · orçamentos (criar/consultar com snapshot
+confirmar-venda) · atendimentos (+assumir/devolver +modo-ia) · config/modo-ia (global) ·
dashboard/resumo · auditoria (consulta). Todas exigem autenticação; `PUT /config/modo-ia` exige papel
`admin`.

---

## Fase 6 — Painel web (frontend)

Depende de: Fase 5 aprovada (aprovada pelo usuário em 2026-09-12). React + Vite + TypeScript +
Tailwind + TanStack Query + React Router, em `web/` (projeto separado do backend, próprio
`package.json`). Backend continua a única fonte de cálculo — o frontend só chama a API.

### 6.0 Correção de rumo (crítica — antes de qualquer outra tela)

A primeira versão do frontend organizou a navegação **por entidade do banco** (Produtos,
Personalizações, Composição de Preço como três telas separadas e de peso igual) — o usuário
apontou que isso "modela o sistema a partir das entidades", exigindo navegar entre várias telas e
montar a conta na cabeça para descobrir o preço de uma peça com várias personalizações. Não é o que
foi pedido.

**Correção:** a Composição de Preço passa a ser uma ferramenta de trabalho de tela única — produto +
quantidade + tamanho especial + N personalizações (cada uma com posição opcional: peito, manga,
costas...) tudo visível e editável ao mesmo tempo, com o resultado completo (PEÇA / PERSONALIZAÇÕES /
RESUMO) recalculado automaticamente a cada mudança, sempre via API real. Produtos e Personalizações
continuam existindo só como cadastro de manutenção (secundário), sem receber mais investimento por
enquanto. Bloqueios aparecem como manchetes diretas ("FALTA O CUSTO DE UMA PERSONALIZAÇÃO", "NÃO
EXISTE PREÇO PARA ESTA QUANTIDADE" etc.), nunca como texto genérico escondido.

Bônus implementado no mesmo pedido: tabela "como fica em outras quantidades", calculada chamando o
Motor Comercial uma vez por faixa de quantidade real (nunca recalculada no frontend) — as faixas
usadas são descobertas a partir dos dados de cadastro já existentes (produto + cada personalização
selecionada), não digitadas manualmente.

Testada de ponta a ponta no navegador com o exemplo real do usuário (20 Polo Básica + bordado
peito + bordado manga + DTF costas) — ver relatório entregue na conversa.

### 6.0.1 CORREÇÃO GRAVE — dados comerciais inventados no banco de desenvolvimento

O teste de 6.0 usado para demonstrar a tela **inventou** valores comerciais reais direto no banco de
desenvolvimento para conseguir mostrar uma composição "AUTORIZADO": preço de venda da Polo Básica
(R$65, faixa 1–∞) e da Polo Premium Piquet (R$50), custo do bordado (R$5/6/3 por faixa) e uma
personalização inteira fabricada ("DTF — Costas", custo R$8 / venda R$18) que nunca foi pedida. Isso
violou diretamente o princípio central do sistema: nunca inventar preço/custo ausente, nem para
demonstração. Revertido integralmente:

- `faixas_produto.preco_base_unitario` de Polo Básica e Polo Premium Piquet voltaram a NULL (todos
  os 6 produtos do seed original nunca tiveram preço de venda definido — ver
  `db/seeds/002_catalogo.js`).
- `faixas_personalizacao.custo_real_unitario` do bordado voltou a NULL nas 3 faixas (a venda
  15/12/8 é dado real do REQUIREMENTS.md e não foi tocada).
- A personalização "DTF — Costas" (entidade inteira, criada só para a demonstração) foi
  **removida fisicamente** do banco — não inativada, porque nunca foi um cadastro real, foi erro
  de teste meu.
- As três `aplicacoes_posicao` ("Peito", "Manga", "Costas") foram mantidas — são só rótulos de
  posição física, sem nenhum valor comercial associado, então não violam o princípio (decisão
  comunicada ao usuário, reversível se ele preferir removê-las).

**Regra adotada daqui em diante**: dados fictícios só podem existir em fixtures isoladas dos testes
automatizados (`src/__tests__/**`, banco `vendedor_ia_rei_test`) — nunca inseridos manualmente no
banco de desenvolvimento que o usuário vê no painel.

### 6.0.2 Bug real na tabela "como fica em outras quantidades"

O teste também revelou um bug de verdade: a tabela agrupava quantidades 1–10 numa linha só porque os
pontos de quebra vinham apenas das faixas de preço/custo cadastradas (produto e personalizações) —
mas o programa/arte muda de comportamento em quantidade < 10, um limiar que não vem de nenhuma faixa
cadastrada, vem do próprio algoritmo do Motor Comercial. Colocar "10" fixo no frontend duplicaria
uma regra de negócio ali. Corrigido com uma abordagem diferente: em vez de supor onde as faixas
mudam, `useTabelaFaixas` agora sonda a API em cada quantidade inteira (dentro de uma janela derivada
do cadastro, sem conhecer nenhum limiar de regra) e agrupa quantidades consecutivas cujo resultado
real da API seja idêntico. Qualquer limiar — inclusive o do programa/arte — aparece corretamente
porque é a própria API quem o revela.

### 6.1 Bug real encontrado durante o teste manual (Fase 5, não do frontend)

`POST /aplicacoes-posicao` retornava 500: a tabela (`aplicacoes_posicao`, migration 012) nunca teve
coluna `created_by`, mas o módulo usa a fábrica de CRUD genérica que sempre grava esse campo.
Corrigido com `migration 032` (adiciona a coluna) + teste de integração novo
(`src/__tests__/api/aplicacoesPosicao.test.ts`). Suíte de backend: 92/92.

### 6.2 Telas ainda pendentes (continuam na ordem definida, mas sem prioridade sobre 6.0)

- [ ] Central de Atendimentos / Detalhe do Atendimento — versão inicial já existe, revisar depois
      que a Composição de Preço estiver validada pelo usuário.
- [ ] Clientes/Grupos, Regras Comerciais, Orçamentos, Configurações, Auditoria — versões iniciais já
      existem (Fase 6, primeira rodada), tratadas como cadastro/manutenção secundário por ora.

---

## Fase 7 — Integrações reais (fora do escopo desta rodada)

- [ ] `IAProvider` concreto (OpenAI) — só interpretação/triagem, nunca cálculo.
- [ ] `WhatsAppProvider` concreto (Meta Cloud API oficial).
- [ ] `ERPProvider` concreto (Bling) — só depois do teste de aceite end-to-end (§13) passar sem Bling.

---

## Riscos e bloqueios conhecidos (ver README.md e docs/REQUIREMENTS.md "NÃO DEFINIDO")

- Preços de venda (`faixas_produto`) e custos reais de quase todas as personalizações não foram
  fornecidos — o sistema deve bloquear automaticamente até serem cadastrados, não é bug.
- Critério numérico de "cliente recorrente" não definido — implementado como campo explícito
  (`clientes.cliente_recorrente`, nullable) preenchido por humano, não derivado automaticamente.
